Skip to content

Commit c5c8864

Browse files
authored
Merge pull request #278 from open-eid/MOPPAND-1658
Add support for asking permissions for notifications
2 parents 1415a23 + dbbbc99 commit c5c8864

File tree

10 files changed

+238
-152
lines changed

10 files changed

+238
-152
lines changed

app/src/androidTest/kotlin/ee/ria/DigiDoc/viewmodel/SmartIdViewModelTest.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import ee.ria.DigiDoc.libdigidoclib.init.Initialization
3030
import ee.ria.DigiDoc.network.sid.dto.response.SessionStatusResponseProcessStatus
3131
import ee.ria.DigiDoc.network.sid.dto.response.SmartIDServiceResponse
3232
import ee.ria.DigiDoc.smartId.SmartSignService
33+
import ee.ria.DigiDoc.utils.notification.NotificationUtil
3334
import junit.framework.TestCase.assertFalse
3435
import junit.framework.TestCase.assertTrue
3536
import kotlinx.coroutines.runBlocking
@@ -68,6 +69,9 @@ class SmartIdViewModelTest {
6869
@Mock
6970
lateinit var configurationRepository: ConfigurationRepository
7071

72+
@Mock
73+
lateinit var notificationUtil: NotificationUtil
74+
7175
@Mock
7276
lateinit var errorStateObserver: Observer<String?>
7377

@@ -172,6 +176,7 @@ class SmartIdViewModelTest {
172176
dataStore,
173177
smartSignService,
174178
configurationRepository,
179+
notificationUtil
175180
)
176181
viewModel.errorState.observeForever(errorStateObserver)
177182
viewModel.dialogError.observeForever(dialogErrorObserver)

app/src/main/kotlin/ee/ria/DigiDoc/di/AppModules.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ import ee.ria.DigiDoc.utils.locale.LocaleUtil
4343
import ee.ria.DigiDoc.utils.locale.LocaleUtilImpl
4444
import ee.ria.DigiDoc.utils.monitoring.CrashDetector
4545
import ee.ria.DigiDoc.utils.monitoring.CrashDetectorImpl
46+
import ee.ria.DigiDoc.utils.notification.NotificationUtil
47+
import ee.ria.DigiDoc.utils.notification.NotificationUtilImpl
4648
import ee.ria.DigiDoc.utilsLib.mimetype.MimeTypeResolver
4749
import javax.inject.Singleton
4850

@@ -144,4 +146,9 @@ class AppModules {
144146
@Provides
145147
@Singleton
146148
fun provideLocaleUtil(): LocaleUtil = LocaleUtilImpl()
149+
150+
@Provides
151+
fun provideNotificationPermissionUtil(
152+
@ApplicationContext context: Context,
153+
): NotificationUtil = NotificationUtilImpl(context)
147154
}

app/src/main/kotlin/ee/ria/DigiDoc/fragment/screen/SignatureInputScreen.kt

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ import ee.ria.DigiDoc.ui.component.menu.SettingsMenuBottomSheet
6464
import ee.ria.DigiDoc.ui.component.settings.SettingsSwitchItem
6565
import ee.ria.DigiDoc.ui.component.shared.InvisibleElement
6666
import ee.ria.DigiDoc.ui.component.shared.TopBar
67+
import ee.ria.DigiDoc.ui.component.shared.notificationPermissionRequester
6768
import ee.ria.DigiDoc.ui.component.signing.IdCardView
6869
import ee.ria.DigiDoc.ui.component.signing.MobileIdView
6970
import ee.ria.DigiDoc.ui.component.signing.NFCView
@@ -76,9 +77,12 @@ import ee.ria.DigiDoc.ui.theme.RIADigiDocTheme
7677
import ee.ria.DigiDoc.utils.Route
7778
import ee.ria.DigiDoc.utils.extensions.notAccessible
7879
import ee.ria.DigiDoc.utils.snackbar.SnackBarManager
80+
import ee.ria.DigiDoc.utilsLib.logging.LoggingUtil.Companion.debugLog
81+
import ee.ria.DigiDoc.utilsLib.logging.LoggingUtil.Companion.errorLog
7982
import ee.ria.DigiDoc.viewmodel.shared.SharedContainerViewModel
8083
import ee.ria.DigiDoc.viewmodel.shared.SharedMenuViewModel
8184
import ee.ria.DigiDoc.viewmodel.shared.SharedSettingsViewModel
85+
import kotlinx.coroutines.Dispatchers.Main
8286
import kotlinx.coroutines.launch
8387

8488
@OptIn(ExperimentalComposeUiApi::class)
@@ -90,7 +94,11 @@ fun SignatureInputScreen(
9094
sharedSettingsViewModel: SharedSettingsViewModel,
9195
sharedContainerViewModel: SharedContainerViewModel,
9296
) {
97+
val logTag = "SignatureInputScreen"
98+
9399
val context = LocalActivity.current as Activity
100+
val scope = rememberCoroutineScope()
101+
val requestNotificationPermission = notificationPermissionRequester()
94102
val isSettingsMenuBottomSheetVisible = rememberSaveable { mutableStateOf(false) }
95103
val getIsAskRoleAndAddressRequested = sharedSettingsViewModel.dataStore::getSettingsAskRoleAndAddress
96104
var rememberMe by rememberSaveable { mutableStateOf(true) }
@@ -419,13 +427,27 @@ fun SignatureInputScreen(
419427

420428
Button(
421429
onClick = {
422-
if (getIsAskRoleAndAddressRequested() && !isAddingRoleAndAddress) {
423-
isSigning = false
424-
isAddingRoleAndAddress = true
425-
} else {
426-
isSigning = true
427-
isAddingRoleAndAddress = false
428-
signAction()
430+
scope.launch(Main) {
431+
if (chosenMethod == SigningMethod.SMART_ID) {
432+
try {
433+
val isNotificationShowingGranted = requestNotificationPermission()
434+
debugLog(
435+
logTag,
436+
"Notification permission granted: $isNotificationShowingGranted",
437+
)
438+
} catch (e: Exception) {
439+
errorLog(logTag, "Permission request failed: ${e.message}")
440+
}
441+
}
442+
443+
if (getIsAskRoleAndAddressRequested() && !isAddingRoleAndAddress) {
444+
isSigning = false
445+
isAddingRoleAndAddress = true
446+
} else {
447+
isSigning = true
448+
isAddingRoleAndAddress = false
449+
signAction()
450+
}
429451
}
430452
},
431453
enabled = isValidToSign,
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
@file:Suppress("PackageName")
2+
3+
package ee.ria.DigiDoc.ui.component.shared
4+
5+
import android.Manifest
6+
import android.content.pm.PackageManager
7+
import androidx.activity.compose.rememberLauncherForActivityResult
8+
import androidx.activity.result.contract.ActivityResultContracts
9+
import androidx.compose.runtime.Composable
10+
import androidx.compose.runtime.remember
11+
import androidx.compose.ui.platform.LocalContext
12+
import androidx.core.content.ContextCompat
13+
import kotlinx.coroutines.CompletableDeferred
14+
import kotlin.coroutines.cancellation.CancellationException
15+
16+
@Composable
17+
fun notificationPermissionRequester(): suspend () -> Boolean {
18+
val context = LocalContext.current
19+
20+
val permissionResult = remember { CompletableDeferred<Boolean>() }
21+
22+
val launcher =
23+
rememberLauncherForActivityResult(
24+
ActivityResultContracts.RequestPermission(),
25+
) { isGranted ->
26+
permissionResult.complete(isGranted)
27+
}
28+
29+
return remember {
30+
suspend {
31+
if (ContextCompat.checkSelfPermission(
32+
context,
33+
Manifest.permission.POST_NOTIFICATIONS,
34+
) == PackageManager.PERMISSION_GRANTED
35+
) {
36+
true
37+
} else {
38+
if (permissionResult.isCompleted) {
39+
permissionResult.completeExceptionally(
40+
CancellationException("Permission request already completed"),
41+
)
42+
}
43+
44+
launcher.launch(Manifest.permission.POST_NOTIFICATIONS)
45+
permissionResult.await()
46+
}
47+
}
48+
}
49+
}

app/src/main/kotlin/ee/ria/DigiDoc/ui/component/signing/SmartIdView.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ import androidx.compose.ui.unit.toSize
7474
import androidx.hilt.navigation.compose.hiltViewModel
7575
import androidx.lifecycle.asFlow
7676
import ee.ria.DigiDoc.R
77+
import ee.ria.DigiDoc.common.Constant
7778
import ee.ria.DigiDoc.libdigidoclib.domain.model.RoleData
7879
import ee.ria.DigiDoc.ui.component.shared.CancelAndOkButtonRow
7980
import ee.ria.DigiDoc.ui.component.shared.HrefMessageDialog
@@ -178,12 +179,15 @@ fun SmartIdView(
178179

179180
val personalCodeWithInvisibleSpaces = TextFieldValue(addInvisibleElement(personalCode.text))
180181

182+
val smartIdChallengeNotificationId = Constant.SmartIdConstants.NOTIFICATION_PERMISSION_CODE
183+
181184
BackHandler {
182185
if (isSigning) {
183186
onError()
184187
} else {
185188
onSuccess()
186189
}
190+
smartIdViewModel.cancelNotification(smartIdChallengeNotificationId)
187191
}
188192

189193
LaunchedEffect(smartIdViewModel.status) {
@@ -214,6 +218,7 @@ fun SmartIdView(
214218
signedContainer?.let {
215219
sharedContainerViewModel.setSignedContainer(it)
216220
smartIdViewModel.resetSignedContainer()
221+
smartIdViewModel.cancelNotification(smartIdChallengeNotificationId)
217222
onSuccess()
218223
}
219224
}
@@ -387,6 +392,7 @@ fun SmartIdView(
387392
}
388393
cancelAction {
389394
smartIdViewModel.cancelSmartIdWorkRequest(signedContainer)
395+
smartIdViewModel.cancelNotification(smartIdChallengeNotificationId)
390396
}
391397
}
392398
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
@file:Suppress("PackageName")
2+
3+
package ee.ria.DigiDoc.utils.notification
4+
5+
import android.Manifest
6+
import androidx.annotation.RequiresPermission
7+
import androidx.core.app.NotificationCompat.PRIORITY_HIGH
8+
9+
interface NotificationUtil {
10+
fun hasNotificationPermission(): Boolean
11+
12+
@RequiresPermission(Manifest.permission.POST_NOTIFICATIONS)
13+
fun sendNotification(
14+
title: String,
15+
message: String,
16+
isSilent: Boolean = false,
17+
channelId: String,
18+
channelName: String,
19+
priority: Int = PRIORITY_HIGH,
20+
)
21+
22+
fun cancelNotification(notificationId: Int)
23+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
@file:Suppress("PackageName")
2+
3+
package ee.ria.DigiDoc.utils.notification
4+
5+
import android.Manifest
6+
import android.app.NotificationChannel
7+
import android.app.NotificationManager
8+
import android.content.Context
9+
import android.content.pm.PackageManager
10+
import androidx.annotation.RequiresPermission
11+
import androidx.core.app.NotificationCompat
12+
import androidx.core.app.NotificationManagerCompat
13+
import androidx.core.content.ContextCompat
14+
import ee.ria.DigiDoc.R
15+
import ee.ria.DigiDoc.common.Constant
16+
import ee.ria.DigiDoc.utilsLib.signing.PowerUtil
17+
import javax.inject.Inject
18+
19+
class NotificationUtilImpl
20+
@Inject
21+
constructor(
22+
private val context: Context,
23+
) : NotificationUtil {
24+
override fun hasNotificationPermission(): Boolean =
25+
ContextCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) ==
26+
PackageManager.PERMISSION_GRANTED
27+
28+
@RequiresPermission(Manifest.permission.POST_NOTIFICATIONS)
29+
override fun sendNotification(
30+
title: String,
31+
message: String,
32+
isSilent: Boolean,
33+
channelId: String,
34+
channelName: String,
35+
priority: Int,
36+
) {
37+
val channel =
38+
NotificationChannel(
39+
channelId,
40+
channelName,
41+
NotificationManager.IMPORTANCE_HIGH,
42+
)
43+
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
44+
notificationManager.createNotificationChannel(channel)
45+
46+
val notification =
47+
NotificationCompat
48+
.Builder(context, channelId)
49+
.setSmallIcon(R.mipmap.ic_launcher)
50+
.setContentTitle(title)
51+
.setContentText(message)
52+
.setPriority(priority)
53+
.setCategory(NotificationCompat.CATEGORY_SERVICE)
54+
.setAutoCancel(true)
55+
.setSilent(isSilent)
56+
if (PowerUtil.isPowerSavingMode(context)) {
57+
notification
58+
.setSound(null)
59+
.setVibrate(null)
60+
.setLights(0, 0, 0)
61+
}
62+
63+
NotificationManagerCompat
64+
.from(context)
65+
.notify(Constant.SmartIdConstants.NOTIFICATION_PERMISSION_CODE, notification.build())
66+
}
67+
68+
override fun cancelNotification(notificationId: Int) {
69+
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
70+
notificationManager.cancel(notificationId)
71+
}
72+
}

0 commit comments

Comments
 (0)