-
Notifications
You must be signed in to change notification settings - Fork 1
Fix #20, #24: Add Android system notification support with notification channels #23
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
24 commits
Select commit
Hold shift + click to select a range
39be3c9
Implement notification handler and create notification channels
theMr17 60c05f8
Add notification permission handling and banner UI
theMr17 272ad97
Enhance notification permission handling by adding request state mana…
theMr17 9cdf794
Add missing imports
theMr17 8001a97
Rename NotificationPermissionBanner to NotificationPermissionPrompt a…
theMr17 f7b2698
Add documentation for AppNotificationChannel enum and GITHUB channel
theMr17 313a8a0
Add documentation for NotificationHandler class and its methods
theMr17 243d5c3
Add documentation for notification permission handling and related co…
theMr17 54abc6f
Rename NotificationPermissionPrompt to WithNotificationPermission
theMr17 40dec2b
Enhance NotificationPermissionPrompt with error handling for settings…
theMr17 b345964
Add URL handling to showNotification in NotificationHandler
theMr17 9205134
Add notification permission state management and testing utilities
theMr17 9893ff2
Update test matrix to include API level 33
theMr17 b7f2bc7
Update test configuration for Android emulator and Gradle setup
theMr17 625e891
Add SdkSuppress annotation for minimum SDK version in notification pe…
theMr17 ca02adf
Fix indentation in test matrix configuration in test.yml
theMr17 78dd125
Refactor notification permission state management to use remember for…
theMr17 dfc69a5
Remove fast-fail option from test matrix configuration in test.yml
theMr17 644e454
Update test matrix configuration in test.yml to disable fail-fast and…
theMr17 05df88f
Remove system images installation step from test matrix configuration…
theMr17 253e633
Remove system images installation step from test matrix configuration…
theMr17 b332ef6
Remove emulator-port configuration from test matrix in test.yml
theMr17 fe8f406
Update test.yml to conditionally set emulator architecture based on A…
theMr17 ef23e4c
Update test.yml to conditionally set emulator architecture based on A…
theMr17 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
18 changes: 18 additions & 0 deletions
18
...t/java/com/notifier/app/core/presentation/notification/FakeNotificationPermissionState.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| package com.notifier.app.core.presentation.notification | ||
|
|
||
| /** | ||
| * A fake implementation of [NotificationPermissionState] for use in tests. | ||
| * | ||
| * @param isGranted whether the notification permission is granted. | ||
| * @param shouldShowRationale whether the system should show a rationale for the permission. | ||
| * | ||
| * This implementation is used to simulate various permission states in UI tests without | ||
| * requiring actual system permission dialogs. | ||
| */ | ||
| class FakeNotificationPermissionState( | ||
| override val isGranted: Boolean, | ||
| override val shouldShowRationale: Boolean | ||
| ) : NotificationPermissionState { | ||
| /** No-op implementation since this is only used in tests. */ | ||
| override fun requestPermission() { /* No-op for tests */ } | ||
| } |
112 changes: 112 additions & 0 deletions
112
...st/java/com/notifier/app/core/presentation/notification/WithNotificationPermissionTest.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,112 @@ | ||
| package com.notifier.app.core.presentation.notification | ||
|
|
||
| import androidx.compose.material3.Text | ||
| import androidx.compose.ui.test.assertCountEquals | ||
| import androidx.compose.ui.test.assertIsDisplayed | ||
| import androidx.compose.ui.test.junit4.createComposeRule | ||
| import androidx.compose.ui.test.onAllNodesWithText | ||
| import androidx.compose.ui.test.onNodeWithText | ||
| import androidx.test.filters.SdkSuppress | ||
| import com.notifier.app.ui.theme.GitHubNotifierTheme | ||
| import org.junit.Rule | ||
| import org.junit.Test | ||
|
|
||
| @SdkSuppress(minSdkVersion = 33) | ||
| class WithNotificationPermissionTest { | ||
| @get:Rule | ||
| val composeTestRule = createComposeRule() | ||
|
|
||
| @Test | ||
| fun permissionNotGranted_shouldShowRationale_showsRationalePrompt() { | ||
| composeTestRule.setContent { | ||
| GitHubNotifierTheme { | ||
| WithNotificationPermission( | ||
| content = { Text("Main Content") }, | ||
| permissionState = FakeNotificationPermissionState( | ||
| isGranted = false, | ||
| shouldShowRationale = true | ||
| ) | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| // Check that rationale message is shown | ||
| composeTestRule | ||
| .onNodeWithText( | ||
| "To deliver GitHub notifications like pull requests, issues, and mentions, " + | ||
| "we need notification access. Please allow it." | ||
| ) | ||
| .assertIsDisplayed() | ||
|
|
||
| // Check that 'Allow' button is shown | ||
| composeTestRule | ||
| .onNodeWithText("Allow Notifications") | ||
| .assertIsDisplayed() | ||
|
|
||
| // Check main content is still shown | ||
| composeTestRule | ||
| .onNodeWithText("Main Content") | ||
| .assertIsDisplayed() | ||
| } | ||
|
|
||
| @Test | ||
| fun permissionNotGranted_shouldNotShowRationale_showsDeniedPrompt() { | ||
| composeTestRule.setContent { | ||
| GitHubNotifierTheme { | ||
| WithNotificationPermission( | ||
| content = { Text("Main Content") }, | ||
| permissionState = FakeNotificationPermissionState( | ||
| isGranted = false, | ||
| shouldShowRationale = false | ||
| ) | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| // Check that denied message is shown | ||
| composeTestRule | ||
| .onNodeWithText( | ||
| "Notification access is required to show updates from your GitHub activity. " + | ||
| "Enable it from system settings." | ||
| ) | ||
| .assertIsDisplayed() | ||
|
|
||
| // Check that 'Open Settings' button is shown | ||
| composeTestRule | ||
| .onNodeWithText("Open Settings") | ||
| .assertIsDisplayed() | ||
|
|
||
| // Check main content is still shown | ||
| composeTestRule | ||
| .onNodeWithText("Main Content") | ||
| .assertIsDisplayed() | ||
| } | ||
|
|
||
| @Test | ||
| fun permissionGranted_doesNotShowPrompt() { | ||
| composeTestRule.setContent { | ||
| GitHubNotifierTheme { | ||
| WithNotificationPermission( | ||
| content = { Text("Main Content") }, | ||
| permissionState = FakeNotificationPermissionState( | ||
| isGranted = true, | ||
| shouldShowRationale = false | ||
| ) | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| // Should only show main content | ||
| composeTestRule | ||
| .onNodeWithText("Main Content") | ||
| .assertIsDisplayed() | ||
|
|
||
| composeTestRule | ||
| .onAllNodesWithText("Allow Notifications") | ||
| .assertCountEquals(0) | ||
|
|
||
| composeTestRule | ||
| .onAllNodesWithText("Open Settings") | ||
| .assertCountEquals(0) | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,18 @@ | ||
| package com.notifier.app | ||
|
|
||
| import android.app.Application | ||
| import com.notifier.app.core.domain.notification.NotificationHandler | ||
| import dagger.hilt.android.HiltAndroidApp | ||
| import javax.inject.Inject | ||
|
|
||
| @HiltAndroidApp | ||
| class GithubNotifierApp : Application() | ||
| class GithubNotifierApp : Application() { | ||
| @Inject | ||
| lateinit var notificationHandler: NotificationHandler | ||
|
|
||
| override fun onCreate() { | ||
| super.onCreate() | ||
|
|
||
| notificationHandler.createNotificationChannels() | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
33 changes: 33 additions & 0 deletions
33
app/src/main/java/com/notifier/app/core/domain/notification/AppNotificationChannel.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| package com.notifier.app.core.domain.notification | ||
|
|
||
| /** | ||
| * Represents the various notification channels used within the app. | ||
| * | ||
| * Each enum entry defines the [id], [displayName], and [description] | ||
| * associated with a specific type of notification channel. | ||
| * | ||
| * These channels are used to categorize and manage notifications for the entire app. | ||
| */ | ||
| enum class AppNotificationChannel( | ||
| /** | ||
| * The unique ID for the notification channel. | ||
| * Used when creating and referencing the channel in the notification system. | ||
| */ | ||
| val id: String, | ||
|
|
||
| /** The user-visible name of the notification channel. */ | ||
| val displayName: String, | ||
|
|
||
| /** The user-visible description of what types of notifications this channel delivers. */ | ||
| val description: String, | ||
| ) { | ||
| /** | ||
| * Channel for delivering notifications related to GitHub activity, | ||
| * such as pull requests, issues, and mentions. | ||
| */ | ||
| GITHUB( | ||
| id = "github_channel", | ||
| displayName = "GitHub Notifications", | ||
| description = "Channel for GitHub related notifications" | ||
| ), | ||
| } |
90 changes: 90 additions & 0 deletions
90
app/src/main/java/com/notifier/app/core/domain/notification/NotificationHandler.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,90 @@ | ||
| package com.notifier.app.core.domain.notification | ||
|
|
||
| import android.app.NotificationChannel | ||
| import android.app.NotificationManager | ||
| import android.app.PendingIntent | ||
| import android.content.Context | ||
| import android.content.Intent | ||
| import androidx.core.app.NotificationCompat | ||
| import androidx.core.net.toUri | ||
| import com.notifier.app.R | ||
| import dagger.hilt.android.qualifiers.ApplicationContext | ||
| import javax.inject.Inject | ||
| import javax.inject.Singleton | ||
|
|
||
| /** | ||
| * Handles the creation and display of in-app notifications. | ||
| * | ||
| * This class is responsible for: | ||
| * - Creating required [NotificationChannel]s. | ||
| * - Showing notifications using [NotificationCompat.Builder]. | ||
| * | ||
| * @property applicationContext The application-level context used to access system services. | ||
| */ | ||
| @Singleton | ||
| class NotificationHandler @Inject constructor( | ||
| @ApplicationContext private val applicationContext: Context, | ||
| ) { | ||
| /** | ||
| * Displays a notification with the given details. | ||
| * | ||
| * @param id The unique ID for this notification (used to update or cancel it). | ||
| * @param title The title displayed in the notification. | ||
| * @param message The body content of the notification. | ||
| * @param channel The [AppNotificationChannel] that defines the channel for this notification. | ||
| * @param url The URL to open in the browser when the notification is clicked. | ||
| */ | ||
| fun showNotification( | ||
| id: Int, | ||
| title: String, | ||
| message: String, | ||
| channel: AppNotificationChannel, | ||
| url: String, | ||
| ) { | ||
| val pendingIntent = PendingIntent.getActivity( | ||
| /* context = */ applicationContext, | ||
| /* requestCode = */ id, | ||
| /* intent = */ Intent(Intent.ACTION_VIEW, url.toUri()), | ||
| /* flags = */ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE | ||
| ) | ||
|
|
||
| val notification = NotificationCompat.Builder(applicationContext, channel.id) | ||
| .setContentTitle(title) | ||
| .setContentText(message) | ||
| .setSmallIcon(R.drawable.ic_launcher_foreground) | ||
| .setPriority(NotificationCompat.PRIORITY_DEFAULT) | ||
| .setContentIntent(pendingIntent) | ||
| .setAutoCancel(true) | ||
| .build() | ||
|
|
||
| val notificationManager = applicationContext.getSystemService( | ||
| Context.NOTIFICATION_SERVICE | ||
| ) as NotificationManager | ||
|
|
||
| notificationManager.notify(id, notification) | ||
| } | ||
|
|
||
| /** | ||
| * Creates all required notification channels used by the app. | ||
| * | ||
| * This must be called during app startup on Android O+ (API 26+) to register | ||
| * channels with the system before sending notifications. | ||
| */ | ||
| fun createNotificationChannels() { | ||
| val notificationManager = applicationContext.getSystemService( | ||
| Context.NOTIFICATION_SERVICE | ||
| ) as NotificationManager | ||
|
|
||
| AppNotificationChannel.entries.forEach { channel -> | ||
| val notificationChannel = NotificationChannel( | ||
| channel.id, | ||
| channel.displayName, | ||
| NotificationManager.IMPORTANCE_DEFAULT | ||
| ).apply { | ||
| description = channel.description | ||
| } | ||
|
|
||
| notificationManager.createNotificationChannel(notificationChannel) | ||
| } | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.