Skip to content

Commit edcb779

Browse files
committed
Refactor: Implement runtime notification permission for Android 13+
This commit introduces runtime permission handling for notifications, a requirement for apps targeting Android 13 (API level 33) and higher. Key changes: - Added new string resources for notification permission dialog. - Created ADB shell scripts to grant, revoke, and clear notification permissions for testing purposes. - Updated `MainService.kt` to check for `POST_NOTIFICATIONS` permission before attempting to show or update a notification on Android 13+. - Modified `MainActivity.kt` to: - Request the `POST_NOTIFICATIONS` permission at runtime. - Provide an educational UI to explain why the permission is needed if the user initially denies it or has previously denied it. - Offer a shortcut to the app's settings screen for the user to grant the permission. - Increased Gradle JVM arguments for improved build performance.
1 parent a25b34e commit edcb779

8 files changed

Lines changed: 124 additions & 21 deletions

File tree

adb_clear_perm_notifications.sh

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export PACKAGE_NAME=com.softartdev.conwaysgameoflife
2+
export PERM_NAME=android.permission.POST_NOTIFICATIONS
3+
# App is newly installed on a device that runs Android 13 or higher:
4+
adb shell pm revoke $PACKAGE_NAME $PERM_NAME
5+
adb shell pm clear-permission-flags $PACKAGE_NAME $PERM_NAME user-set
6+
adb shell pm clear-permission-flags $PACKAGE_NAME $PERM_NAME user-fixed
7+
# Print the current permission status
8+
adb shell dumpsys package $PACKAGE_NAME | grep $PERM_NAME:

adb_grant_perm_notifications.sh

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export PACKAGE_NAME=com.softartdev.conwaysgameoflife
2+
export PERM_NAME=android.permission.POST_NOTIFICATIONS
3+
# The user keeps notifications enabled when the app is installed on a device that runs 12L or lower, then the device upgrades to Android 13 or higher:
4+
adb shell pm grant $PACKAGE_NAME $PERM_NAME
5+
adb shell pm set-permission-flags $PACKAGE_NAME $PERM_NAME user-set
6+
adb shell pm clear-permission-flags $PACKAGE_NAME $PERM_NAME user-fixed
7+
# Print the current permission status
8+
adb shell dumpsys package $PACKAGE_NAME | grep $PERM_NAME:

adb_revoke_perm_notifications.sh

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export PACKAGE_NAME=com.softartdev.conwaysgameoflife
2+
export PERM_NAME=android.permission.POST_NOTIFICATIONS
3+
# The user manually disables notifications when the app is installed on a device that runs 12L or lower, then the device upgrades to Android 13 or higher:
4+
adb shell pm revoke $PACKAGE_NAME $PERM_NAME
5+
adb shell pm set-permission-flags $PACKAGE_NAME $PERM_NAME user-set
6+
adb shell pm clear-permission-flags $PACKAGE_NAME $PERM_NAME user-fixed
7+
# Print the current permission status
8+
adb shell dumpsys package $PACKAGE_NAME | grep $PERM_NAME:
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export PACKAGE_NAME=com.softartdev.conwaysgameoflife
2+
export PERM_NAME=android.permission.POST_NOTIFICATIONS
3+
# The user manually disables notifications:
4+
adb shell pm revoke $PACKAGE_NAME $PERM_NAME
5+
adb shell pm set-permission-flags $PACKAGE_NAME $PERM_NAME user-set
6+
adb shell pm set-permission-flags $PACKAGE_NAME $PERM_NAME user-fixed
7+
# Print the current permission status
8+
adb shell dumpsys package $PACKAGE_NAME | grep $PERM_NAME:

app/src/main/java/com/softartdev/conwaysgameoflife/MainService.kt

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,24 @@
11
package com.softartdev.conwaysgameoflife
22

3-
import android.app.*
3+
import android.Manifest
4+
import android.app.Notification
5+
import android.app.NotificationChannel
6+
import android.app.NotificationManager
7+
import android.app.PendingIntent
8+
import android.app.Service
49
import android.content.Context
510
import android.content.Intent
11+
import android.content.pm.PackageManager
612
import android.content.pm.ServiceInfo
7-
import android.os.*
13+
import android.os.Binder
14+
import android.os.Build
15+
import android.os.Handler
16+
import android.os.IBinder
17+
import android.os.Looper
818
import androidx.core.app.NotificationCompat
919
import androidx.core.app.NotificationManagerCompat
1020
import androidx.core.app.ServiceCompat
21+
import androidx.core.content.ContextCompat
1122
import com.softartdev.conwaysgameoflife.model.CellState
1223
import com.softartdev.conwaysgameoflife.model.ICellState
1324
import com.softartdev.conwaysgameoflife.ui.MainActivity
@@ -40,8 +51,7 @@ class MainService : Service() {
4051
if (iCellState.isGoNextGeneration) {
4152
val processed = iCellState.processNextGeneration()
4253
if (serviceRunningInForeground) {
43-
val notification = createNotification(applicationContext, iCellState.countGeneration, notificationBuilder)
44-
notificationManager.notify(NOTIFICATION_ID, notification)
54+
updateNotificationSafely(applicationContext, iCellState, notificationBuilder, notificationManager)
4555
} else {
4656
uiHandler.post { uiRepaint?.invoke(processed) }
4757
}
@@ -142,11 +152,25 @@ class MainService : Service() {
142152
applicationContext: Context,
143153
stepCount: Int,
144154
notificationBuilder: NotificationCompat.Builder
145-
): Notification {
146-
val contentText = applicationContext.getString(R.string.steps, stepCount)
147-
return notificationBuilder
148-
.setContentText(contentText)
149-
.build()
155+
): Notification = notificationBuilder
156+
.setContentText(applicationContext.getString(R.string.steps, stepCount))
157+
.build()
158+
159+
private fun updateNotificationSafely(
160+
applicationContext: Context,
161+
iCellState: ICellState,
162+
notificationBuilder: NotificationCompat.Builder,
163+
notificationManager: NotificationManagerCompat,
164+
) {
165+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
166+
&& ContextCompat.checkSelfPermission(applicationContext, Manifest.permission.POST_NOTIFICATIONS)
167+
!= PackageManager.PERMISSION_GRANTED
168+
) {
169+
Timber.w("No notification permission, cannot update notification")
170+
return
171+
}
172+
val notification = createNotification(applicationContext, iCellState.countGeneration, notificationBuilder)
173+
notificationManager.notify(NOTIFICATION_ID, notification)
150174
}
151175
}
152176
}

app/src/main/java/com/softartdev/conwaysgameoflife/ui/MainActivity.kt

Lines changed: 54 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,21 @@
11
package com.softartdev.conwaysgameoflife.ui
22

3-
import android.content.Context
3+
import android.Manifest
44
import android.content.Intent
5+
import android.content.pm.PackageManager
6+
import android.net.Uri
7+
import android.os.Build
58
import android.os.Bundle
9+
import android.provider.Settings
610
import android.view.Menu
711
import android.view.MenuItem
812
import android.widget.SeekBar
913
import androidx.activity.enableEdgeToEdge
14+
import androidx.activity.result.contract.ActivityResultContracts
1015
import androidx.appcompat.app.AlertDialog
1116
import androidx.appcompat.app.AppCompatActivity
17+
import androidx.core.app.ActivityCompat
18+
import androidx.core.content.ContextCompat
1219
import androidx.core.view.ViewCompat
1320
import androidx.core.view.WindowInsetsCompat
1421
import com.softartdev.conwaysgameoflife.MainService
@@ -39,8 +46,7 @@ class MainActivity : AppCompatActivity() {
3946
}
4047
updateStartButtonText()
4148
binding.mainStartButton.setOnClickListener {
42-
with(iCellState) { if (toggleGoNextGeneration()) cancelTimer() else resumeTimer() }
43-
updateStartButtonText()
49+
if (checkNotificationPermissionGranted(toggle = true)) toggleGame()
4450
}
4551
binding.mainStepButton.setOnClickListener {
4652
val processed = iCellState.processNextGeneration() ?: return@setOnClickListener
@@ -68,6 +74,7 @@ class MainActivity : AppCompatActivity() {
6874
override fun onStartTrackingTouch(seekBar: SeekBar?) = Unit
6975
override fun onStopTrackingTouch(seekBar: SeekBar?) = Unit
7076
})
77+
checkNotificationPermissionGranted()
7178
}
7279

7380
override fun onStart() {
@@ -89,6 +96,31 @@ class MainActivity : AppCompatActivity() {
8996
}
9097
}
9198

99+
private fun checkNotificationPermissionGranted(toggle: Boolean = false): Boolean = when {
100+
Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU -> when {
101+
ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED -> true
102+
ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.POST_NOTIFICATIONS) -> {
103+
showNotificationPermissionExplanation()
104+
false
105+
}
106+
else -> {
107+
registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted: Boolean ->
108+
when {
109+
isGranted -> if (toggle) toggleGame()
110+
else -> showNotificationPermissionExplanation()
111+
}
112+
}.launch(Manifest.permission.POST_NOTIFICATIONS)
113+
false
114+
}
115+
}
116+
else -> true
117+
}
118+
119+
private fun toggleGame() {
120+
with(iCellState) { if (toggleGoNextGeneration()) cancelTimer() else resumeTimer() }
121+
updateStartButtonText()
122+
}
123+
92124
fun updateStartButtonText() {
93125
val textResId: Int = if (iCellState.isGoNextGeneration) R.string.stop else R.string.start
94126
binding.mainStartButton.text = getString(textResId)
@@ -105,15 +137,26 @@ class MainActivity : AppCompatActivity() {
105137
}
106138

107139
override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {
108-
R.id.action_rules -> {
109-
AlertDialog.Builder(this)
110-
.setTitle(R.string.rules_title)
111-
.setMessage(R.string.rules_text)
112-
.setPositiveButton(android.R.string.ok, null)
113-
.show()
114-
true
115-
}
140+
R.id.action_rules -> showRules()
116141
else -> super.onOptionsItemSelected(item)
117142
}
118143

144+
private fun showRules(): Boolean = AlertDialog.Builder(this)
145+
.setTitle(R.string.rules_title)
146+
.setMessage(R.string.rules_text)
147+
.setPositiveButton(android.R.string.ok, null)
148+
.show() != null
149+
150+
private fun showNotificationPermissionExplanation(): AlertDialog? = AlertDialog.Builder(this)
151+
.setTitle(getString(R.string.notification_permission_title))
152+
.setMessage(getString(R.string.notification_permission_message))
153+
.setPositiveButton(getString(R.string.open_settings)) { _, _ -> openAppSettings() }
154+
.setNegativeButton(getString(R.string.cancel)) { _, _ -> /* Do nothing */ }
155+
.show()
156+
157+
private fun openAppSettings() {
158+
val uri = Uri.fromParts("package", packageName, null)
159+
val appSettingsIntent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, uri)
160+
startActivity(appSettingsIntent)
161+
}
119162
}

app/src/main/res/values/strings.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,8 @@
1616
<string name="notification_channel_name">Game notification channel</string>
1717
<string name="notification_title">Generation in progress</string>
1818
<string name="period">Period: %1$d ms</string>
19+
<string name="notification_permission_title">Notification Permission Required</string>
20+
<string name="notification_permission_message">This app needs notification permission to show game progress when you leave the app. You can grant this permission in the app\'s settings.</string>
21+
<string name="open_settings">Open Settings</string>
22+
<string name="cancel">Cancel</string>
1923
</resources>

gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
# Specifies the JVM arguments used for the daemon process.
1111
# The setting is particularly useful for tweaking memory settings.
1212
# Default value: -Xmx10248m -XX:MaxPermSize=256m
13-
org.gradle.jvmargs=-Xmx2048m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 -Dkotlin.daemon.jvm.options\="-Xmx2048m" -XX:+UseParallelGC
13+
org.gradle.jvmargs=-Xmx16g -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 -Dkotlin.daemon.jvm.options\="-Xmx16g" -XX:+UseParallelGC
1414

1515
# When configured, Gradle will run in incubating parallel mode.
1616
# This option should only be used with decoupled projects. More details, visit

0 commit comments

Comments
 (0)