diff --git a/examples/build.md b/examples/build.md index b8fe9df310..e03f608c13 100644 --- a/examples/build.md +++ b/examples/build.md @@ -121,7 +121,7 @@ The Android demo **overrides the shared guide's "no repository wrapper" rule**. - `MainViewModel : AndroidViewModel` — central state with `LiveData` for every UI value. Implements `IPushSubscriptionObserver`, `IPermissionObserver`, `IUserStateObserver`, and `IUserJwtInvalidatedListener`. Holds a monotonic `private var fetchRequestSequence = 0L` that maps to the shared guide's `requestSequence` for stale-result protection in `fetchUserDataFromApi`. - `OneSignalRepository.kt` — only some methods are `suspend` + `withContext(Dispatchers.IO)`; many are synchronous wrappers and the ViewModel wraps calls in `viewModelScope.launch(Dispatchers.IO)` itself. - `OneSignalService.kt` (`object`) — REST API client described in the shared guide's Prompt 1.4. -- `SharedPreferenceUtil.kt` — backs the shared guide's PreferencesService (consent required, privacy consent, external user id, location shared, IAM paused, cached JWT token, cached identity-verification toggle). +- `SharedPreferenceUtil.kt` — backs the shared guide's PreferencesService (consent required, privacy consent, external user id, location shared, IAM paused, cached JWT token, cached identity-verification toggle, notification service extension switches). `fetchUserDataFromApi` loading sequence: the sequence is incremented first, then early returns may set `_isLoading = false` before `_isLoading = true` is set (see `MainViewModel.kt` lines ~167–192). Stale-fetch guards themselves are correct -- results are dropped when `requestId != fetchRequestSequence`, and the same guard wraps the catch branch and the final `_isLoading.value = false`. @@ -178,9 +178,20 @@ Patterns used by this demo beyond the shared guide's table: - `MainActivity` sets `semantics(mergeDescendants = false) { testTagsAsResourceId = true }` on the root `Surface` so Appium `id=` selectors map onto Compose `testTag` values (`MainActivity.kt` lines ~41–43). - `Dialogs.kt` re-applies the same via an `exposeTestTagsAsResourceId()` helper inside each dialog because Compose dialogs render in a separate window -- required for dialog-scoped test tags to be visible to UiAutomator. +### Log tags + +The demo logs through `util/DemoLog.kt` rather than `android.util.Log`. It marks both the tag and the message with `[Demo]`, so the tag stays filterable with `logcat -s` and a line is still recognizable when only the message column is in view. + +```kotlin +DemoLog.d(TAG, "Sending notification: Simple") +// D/[Demo]MainViewModel: [Demo] Sending notification: Simple +``` + +Pass the plain class name as the tag; `DemoLog` adds the prefix, so `[Demo]` is defined in exactly one place. `v`, `d`, `i`, `w`, `e`, and `e(tag, message, throwable)` are all there. Current tags are `[Demo]OneSignalExample`, `[Demo]MainViewModel`, `[Demo]OneSignalService`, `[Demo]OneSignalRepository`, `[Demo]TooltipHelper`, `[Demo]NSE`, and `[Demo]OneSignalHMS` on the Huawei flavor. + ### SDK log forwarding -`MainApplication` registers `OneSignal.Debug.addLogListener` and forwards each entry to `android.util.Log` under the `OneSignalSDK` tag, so SDK output shows up alongside app output in Android Studio's Logcat (filter `package:mine` to see both). There is no in-app log viewer — match the shared guide and other wrapper SDK demos by relying on Logcat. +`MainApplication` registers `OneSignal.Debug.addLogListener` and forwards each entry to `android.util.Log` under the `OneSignalSDK` tag, so SDK output shows up alongside app output in Android Studio's Logcat (filter `package:mine` to see both). Those five calls are the one place in the demo that still uses `android.util.Log` directly, on purpose. The lines are the SDK's, mirrored, and routing them through `DemoLog` would bury the demo's own output whenever you grep `[Demo]`. There is no in-app log viewer — match the shared guide and other wrapper SDK demos by relying on Logcat. --- @@ -193,6 +204,34 @@ The Android demo exercises a few SDK features that are not described in the shar - **UPDATE USER JWT button** (`UserSection`, `testTag = "update_user_jwt_button"`) — opens a `PairInputDialog` (External User Id + JWT Token) and calls `viewModel.updateUserJwt(...)` → `OneSignal.updateUserJwt(...)`. - **`IUserJwtInvalidatedListener`** — registered by `MainViewModel`; surfaces a log entry via `Log.w(TAG, ...)` when the SDK reports an invalidated JWT. Per Prompt 7.6 the snackbar is no longer fired from this listener. +### Notification service extension + +`DemoNotificationServiceExtension` implements `INotificationServiceExtension` and is registered from `app/src/main/AndroidManifest.xml`: + +```xml + +``` + +The SDK resolves that string with `Class.forName` (`NotificationLifecycleService.setupNotificationServiceExtension`), so a wrong class name here fails silently at runtime. Compiling the class inside `OneSignalSDK/`'s `:app` project is what turns a breaking change to `INotificationServiceExtension` or `INotificationReceivedEvent` into a CI failure, and the release build is the only place the `-keep class ** implements com.onesignal.notifications.INotificationServiceExtension` rule in `onesignal/notifications/consumer-rules.pro` gets exercised end to end. + +The UI is a single Enable Extension toggle (`nse_enabled_toggle`). Off means `onNotificationReceived` returns before touching anything, so the notifications the demo sends stay usable as a manual QA baseline and the section stays close to the other wrapper demos. + +The behavior switches have no UI. They live in `NotificationExtensionOptions`, persist through `SharedPreferenceUtil`, and are flipped in code: change the `false` defaults in `SharedPreferenceUtil.getNotificationExtensionOptions` (or call `cacheNotificationExtensionOptions`) and rebuild. The extension reads them from SharedPreferences rather than `MainViewModel`, because it runs whether or not the app is open. + +| Option | What it does | +| --- | --- | +| `logDetails` | Logs id, sent time, and the channel the SDK resolved, under the `[Demo]NSE` tag. | +| `applyExtender` | Prefixes the title with `[NSE]` through a `NotificationCompat.Extender`. | +| `forceHighImportanceChannel` | Moves the notification onto an app-owned `IMPORTANCE_HIGH` channel. | +| `delayDisplay` | `preventDefault()`, then `display()` five seconds later. | +| `discard` | `preventDefault(true)`. Takes precedence over the other switches. | + +The channel readout comes from `NotificationCompat.getChannelId(builder.build())` inside the extender, the only place an extension can see the SDK's choice. A restored notification lands on `restored_OS_notifications` no matter what the payload asked for, which the payload alone never shows. `event.restoring` is not on `INotificationReceivedEvent` yet; see the TODO in the class and SDK-5011. + +The class sets an extender only when a switch needs one rather than installing a no-op whenever the extension is on, which is about not doing work nothing asked for. An extender cannot change what displays. `NotificationGenerationProcessor.shouldDisplayNotification` does read `hasExtender()`, but `processHandlerResponse` has already dropped a push with an empty body on `canDisplay` by the time it runs. + --- ## Platform Config @@ -236,7 +275,7 @@ If the package changes you must regenerate this file from the Huawei AppGallery `src/huawei/` overlays the main source set: -- `src/huawei/AndroidManifest.xml` — declares `HmsMessageServiceAppLevel` with `android:name="com.onesignal.example.notification.HmsMessageServiceAppLevel"`. +- `src/huawei/AndroidManifest.xml` — declares `HmsMessageServiceAppLevel` with `android:name="com.onesignal.example.notification.HmsMessageServiceAppLevel"`. The notification service extension meta-data comes from the main manifest through manifest merge, so both flavors get it. - `src/huawei/java/com/onesignal/example/notification/HmsMessageServiceAppLevel.kt` — minimal `HmsMessageService` subclass that forwards messages to OneSignal. --- @@ -263,9 +302,11 @@ examples/ │ ├── java/com/onesignal/example/ │ │ ├── application/MainApplication.kt │ │ ├── data/ - │ │ │ ├── model/{NotificationType,InAppMessageType}.kt + │ │ │ ├── model/{NotificationType,InAppMessageType, + │ │ │ │ NotificationExtensionOptions}.kt │ │ │ ├── network/OneSignalService.kt │ │ │ └── repository/OneSignalRepository.kt + │ │ ├── notification/DemoNotificationServiceExtension.kt │ │ ├── ui/ │ │ │ ├── components/ # SectionCard (with DemoSection), │ │ │ │ # ToggleRow, ActionButton, ListComponents, @@ -274,7 +315,7 @@ examples/ │ │ │ ├── main/ # MainActivity, MainScreen, Sections, MainViewModel │ │ │ ├── secondary/SecondaryActivity.kt │ │ │ └── theme/ # Theme.kt, DemoLayout.kt - │ │ └── util/ # SharedPreferenceUtil, TooltipHelper + │ │ └── util/ # SharedPreferenceUtil, TooltipHelper, DemoLog │ └── res/ │ ├── values/{strings,colors,styles}.xml │ ├── raw/ # vine_boom.wav diff --git a/examples/demo/app/src/huawei/java/com/onesignal/example/notification/HmsMessageServiceAppLevel.kt b/examples/demo/app/src/huawei/java/com/onesignal/example/notification/HmsMessageServiceAppLevel.kt index e51bfb57e9..30e1fc7c56 100644 --- a/examples/demo/app/src/huawei/java/com/onesignal/example/notification/HmsMessageServiceAppLevel.kt +++ b/examples/demo/app/src/huawei/java/com/onesignal/example/notification/HmsMessageServiceAppLevel.kt @@ -1,10 +1,10 @@ package com.onesignal.example.notification import android.os.Bundle -import android.util.Log import com.huawei.hms.push.HmsMessageService import com.huawei.hms.push.RemoteMessage import com.onesignal.notifications.bridges.OneSignalHmsEventBridge +import com.onesignal.example.util.DemoLog /** * HMS Message Service for handling Huawei Push notifications. @@ -26,7 +26,7 @@ class HmsMessageServiceAppLevel : HmsMessageService() { * Otherwise, you need to start a new Job for callback processing. */ override fun onNewToken(token: String, bundle: Bundle) { - Log.d(TAG, "HmsMessageServiceAppLevel onNewToken refresh token: $token bundle: $bundle") + DemoLog.d(TAG, "HmsMessageServiceAppLevel onNewToken refresh token: $token bundle: $bundle") // Forward event on to OneSignal SDK OneSignalHmsEventBridge.onNewToken(this, token, bundle) @@ -34,7 +34,7 @@ class HmsMessageServiceAppLevel : HmsMessageService() { @Deprecated("Deprecated in Java") override fun onNewToken(token: String) { - Log.d(TAG, "HmsMessageServiceAppLevel onNewToken refresh token: $token") + DemoLog.d(TAG, "HmsMessageServiceAppLevel onNewToken refresh token: $token") // Forward event on to OneSignal SDK OneSignalHmsEventBridge.onNewToken(this, token) @@ -48,18 +48,18 @@ class HmsMessageServiceAppLevel : HmsMessageService() { * Start a new Job if more time is needed. */ override fun onMessageReceived(message: RemoteMessage) { - Log.d(TAG, "HMS onMessageReceived: $message") - Log.d(TAG, "HMS onMessageReceived.ttl: ${message.ttl}") - Log.d(TAG, "HMS onMessageReceived.data: ${message.data}") + DemoLog.d(TAG, "HMS onMessageReceived: $message") + DemoLog.d(TAG, "HMS onMessageReceived.ttl: ${message.ttl}") + DemoLog.d(TAG, "HMS onMessageReceived.data: ${message.data}") message.notification?.let { notification -> - Log.d(TAG, "HMS onMessageReceived.title: ${notification.title}") - Log.d(TAG, "HMS onMessageReceived.body: ${notification.body}") - Log.d(TAG, "HMS onMessageReceived.icon: ${notification.icon}") - Log.d(TAG, "HMS onMessageReceived.color: ${notification.color}") - Log.d(TAG, "HMS onMessageReceived.channelId: ${notification.channelId}") - Log.d(TAG, "HMS onMessageReceived.imageURL: ${notification.imageUrl}") - Log.d(TAG, "HMS onMessageReceived.tag: ${notification.tag}") + DemoLog.d(TAG, "HMS onMessageReceived.title: ${notification.title}") + DemoLog.d(TAG, "HMS onMessageReceived.body: ${notification.body}") + DemoLog.d(TAG, "HMS onMessageReceived.icon: ${notification.icon}") + DemoLog.d(TAG, "HMS onMessageReceived.color: ${notification.color}") + DemoLog.d(TAG, "HMS onMessageReceived.channelId: ${notification.channelId}") + DemoLog.d(TAG, "HMS onMessageReceived.imageURL: ${notification.imageUrl}") + DemoLog.d(TAG, "HMS onMessageReceived.tag: ${notification.tag}") } // Forward event on to OneSignal SDK diff --git a/examples/demo/app/src/main/AndroidManifest.xml b/examples/demo/app/src/main/AndroidManifest.xml index fc85fb6a46..c91d1c923a 100644 --- a/examples/demo/app/src/main/AndroidManifest.xml +++ b/examples/demo/app/src/main/AndroidManifest.xml @@ -30,6 +30,13 @@ android:name="com.amazon.device.messaging" android:required="false"/> + + + $id") + DemoLog.d(TAG, "Adding alias: $label -> $id") OneSignal.User.addAlias(label, id) } fun addAliases(aliases: Map) { - Log.d(TAG, "Adding aliases: $aliases") + DemoLog.d(TAG, "Adding aliases: $aliases") OneSignal.User.addAliases(aliases) } fun removeAlias(label: String) { - Log.d(TAG, "Removing alias: $label") + DemoLog.d(TAG, "Removing alias: $label") OneSignal.User.removeAlias(label) } fun removeAliases(labels: Collection) { - Log.d(TAG, "Removing aliases: $labels") + DemoLog.d(TAG, "Removing aliases: $labels") if (labels.isNotEmpty()) { OneSignal.User.removeAliases(labels) } @@ -60,44 +60,44 @@ class OneSignalRepository { // Email operations fun addEmail(email: String) { - Log.d(TAG, "Adding email: $email") + DemoLog.d(TAG, "Adding email: $email") OneSignal.User.addEmail(email) } fun removeEmail(email: String) { - Log.d(TAG, "Removing email: $email") + DemoLog.d(TAG, "Removing email: $email") OneSignal.User.removeEmail(email) } // SMS operations fun addSms(smsNumber: String) { - Log.d(TAG, "Adding SMS: $smsNumber") + DemoLog.d(TAG, "Adding SMS: $smsNumber") OneSignal.User.addSms(smsNumber) } fun removeSms(smsNumber: String) { - Log.d(TAG, "Removing SMS: $smsNumber") + DemoLog.d(TAG, "Removing SMS: $smsNumber") OneSignal.User.removeSms(smsNumber) } // Tag operations fun addTag(key: String, value: String) { - Log.d(TAG, "Adding tag: $key -> $value") + DemoLog.d(TAG, "Adding tag: $key -> $value") OneSignal.User.addTag(key, value) } fun addTags(tags: Map) { - Log.d(TAG, "Adding tags: $tags") + DemoLog.d(TAG, "Adding tags: $tags") OneSignal.User.addTags(tags) } fun removeTag(key: String) { - Log.d(TAG, "Removing tag: $key") + DemoLog.d(TAG, "Removing tag: $key") OneSignal.User.removeTag(key) } fun removeTags(keys: Collection) { - Log.d(TAG, "Removing tags: $keys") + DemoLog.d(TAG, "Removing tags: $keys") if (keys.isNotEmpty()) { OneSignal.User.removeTags(keys) } @@ -109,22 +109,22 @@ class OneSignalRepository { // Trigger operations fun addTrigger(key: String, value: String) { - Log.d(TAG, "Adding trigger: $key -> $value") + DemoLog.d(TAG, "Adding trigger: $key -> $value") OneSignal.InAppMessages.addTrigger(key, value) } fun addTriggers(triggers: Map) { - Log.d(TAG, "Adding triggers: $triggers") + DemoLog.d(TAG, "Adding triggers: $triggers") OneSignal.InAppMessages.addTriggers(triggers) } fun removeTrigger(key: String) { - Log.d(TAG, "Removing trigger: $key") + DemoLog.d(TAG, "Removing trigger: $key") OneSignal.InAppMessages.removeTrigger(key) } fun clearTriggers(keys: Collection) { - Log.d(TAG, "Clearing triggers: $keys") + DemoLog.d(TAG, "Clearing triggers: $keys") if (keys.isNotEmpty()) { OneSignal.InAppMessages.removeTriggers(keys) } @@ -132,23 +132,23 @@ class OneSignalRepository { // Outcome operations fun sendOutcome(name: String) { - Log.d(TAG, "Sending outcome: $name") + DemoLog.d(TAG, "Sending outcome: $name") OneSignal.Session.addOutcome(name) } fun sendUniqueOutcome(name: String) { - Log.d(TAG, "Sending unique outcome: $name") + DemoLog.d(TAG, "Sending unique outcome: $name") OneSignal.Session.addUniqueOutcome(name) } fun sendOutcomeWithValue(name: String, value: Float) { - Log.d(TAG, "Sending outcome with value: $name -> $value") + DemoLog.d(TAG, "Sending outcome with value: $name -> $value") OneSignal.Session.addOutcomeWithValue(name, value) } // Track Event fun trackEvent(name: String, properties: Map?) { - Log.d(TAG, "Tracking event: $name with properties: $properties") + DemoLog.d(TAG, "Tracking event: $name with properties: $properties") OneSignal.User.trackEvent(name, properties) } @@ -162,7 +162,7 @@ class OneSignalRepository { } fun setPushEnabled(enabled: Boolean) { - Log.d(TAG, "Setting push enabled: $enabled") + DemoLog.d(TAG, "Setting push enabled: $enabled") if (enabled) { OneSignal.User.pushSubscription.optIn() } else { @@ -176,7 +176,7 @@ class OneSignalRepository { } fun setInAppMessagesPaused(paused: Boolean) { - Log.d(TAG, "Setting in-app messages paused: $paused") + DemoLog.d(TAG, "Setting in-app messages paused: $paused") OneSignal.InAppMessages.paused = paused } @@ -186,18 +186,18 @@ class OneSignalRepository { } fun setLocationShared(shared: Boolean) { - Log.d(TAG, "Setting location shared: $shared") + DemoLog.d(TAG, "Setting location shared: $shared") OneSignal.Location.isShared = shared } suspend fun promptLocation() = withContext(Dispatchers.IO) { - Log.d(TAG, "Prompting for location permission") + DemoLog.d(TAG, "Prompting for location permission") OneSignal.Location.requestPermission() } // Notifications suspend fun promptPushPermission() = withContext(Dispatchers.IO) { - Log.d(TAG, "Prompting for push permission") + DemoLog.d(TAG, "Prompting for push permission") OneSignal.Notifications.requestPermission(true) } @@ -207,18 +207,18 @@ class OneSignalRepository { // Send notifications suspend fun sendNotification(type: NotificationType): Boolean { - Log.d(TAG, "Sending notification: ${type.title}") + DemoLog.d(TAG, "Sending notification: ${type.title}") return OneSignalService.sendNotification(type) } suspend fun sendCustomNotification(title: String, body: String): Boolean { - Log.d(TAG, "Sending custom notification: $title") + DemoLog.d(TAG, "Sending custom notification: $title") return OneSignalService.sendCustomNotification(title, body) } // Privacy consent fun setConsentRequired(required: Boolean) { - Log.d(TAG, "Setting consent required: $required") + DemoLog.d(TAG, "Setting consent required: $required") OneSignal.consentRequired = required } @@ -227,7 +227,7 @@ class OneSignalRepository { } fun setPrivacyConsent(granted: Boolean) { - Log.d(TAG, "Setting privacy consent: $granted") + DemoLog.d(TAG, "Setting privacy consent: $granted") OneSignal.consentGiven = granted } @@ -242,7 +242,7 @@ class OneSignalRepository { // Fetch user data from API suspend fun fetchUser(aliasLabel: String, aliasValue: String, jwt: String? = null): UserData? = withContext(Dispatchers.IO) { - Log.d(TAG, "Fetching user data by $aliasLabel: $aliasValue") + DemoLog.d(TAG, "Fetching user data by $aliasLabel: $aliasValue") OneSignalService.fetchUser(aliasLabel, aliasValue, jwt) } } diff --git a/examples/demo/app/src/main/java/com/onesignal/example/notification/DemoNotificationServiceExtension.kt b/examples/demo/app/src/main/java/com/onesignal/example/notification/DemoNotificationServiceExtension.kt new file mode 100644 index 0000000000..e883db43b0 --- /dev/null +++ b/examples/demo/app/src/main/java/com/onesignal/example/notification/DemoNotificationServiceExtension.kt @@ -0,0 +1,136 @@ +package com.onesignal.example.notification + +import android.app.NotificationChannel +import android.app.NotificationManager +import android.content.Context +import android.os.Build +import androidx.core.app.NotificationCompat +import com.onesignal.example.data.model.NotificationExtensionOptions +import com.onesignal.example.util.DemoLog +import com.onesignal.example.util.SharedPreferenceUtil +import com.onesignal.notifications.IDisplayableMutableNotification +import com.onesignal.notifications.INotificationReceivedEvent +import com.onesignal.notifications.INotificationServiceExtension + +/** + * The demo's notification service extension, registered in AndroidManifest.xml under the + * `com.onesignal.NotificationServiceExtension` meta-data key. The SDK resolves that string with + * Class.forName and calls the no-arg constructor, so a typo in the manifest fails silently at + * runtime instead of at build time. That reflection is also why the class needs the -keep rule + * in `onesignal/notifications/consumer-rules.pro` to survive R8. + * + * Nothing here runs until Enable Extension is turned on in the demo's Notification Service + * Extension section. An always-on extension would change the baseline for every notification the + * demo sends, and anyone chasing a grouping or channel bug would end up debugging this file + * without knowing it. That master toggle is the only UI; the behavior switches are flipped in + * code through [NotificationExtensionOptions] and SharedPreferenceUtil. + * + * The switches come from SharedPreferences, not MainViewModel. This is called whether or not the + * app is open, so there may be no ViewModel yet. + */ +class DemoNotificationServiceExtension : INotificationServiceExtension { + override fun onNotificationReceived(event: INotificationReceivedEvent) { + val options = SharedPreferenceUtil.getNotificationExtensionOptions(event.context) + if (!options.enabled) return + + val notification = event.notification + + if (options.logDetails) { + // TODO: [SDK-5011] log `event.restoring` here once it ships. Reading it next to the + // channel below is the whole diagnosis for a notification that re-alerts on reboot. + DemoLog.d( + TAG, + "received androidNotificationId=${notification.androidNotificationId}" + + " notificationId=${notification.notificationId}" + + " sentTime=${notification.sentTime}" + + " title=${notification.title}", + ) + } + + if (options.discard) { + if (options.logDetails) { + DemoLog.d(TAG, "discarding androidNotificationId=${notification.androidNotificationId}") + } + event.preventDefault(true) + return + } + + // Set an extender only when a switch needs one, rather than installing a no-op + // whenever the extension is on. This is about not doing work nothing asked for. + // An extender cannot change what displays: NotificationGenerationProcessor + // .shouldDisplayNotification does read hasExtender(), but processHandlerResponse + // has already dropped a push with an empty body on canDisplay by the time it runs. + if (options.logDetails || options.applyExtender || options.forceHighImportanceChannel) { + notification.setExtender(buildExtender(event.context, notification, options)) + } + + if (options.delayDisplay) { + event.preventDefault() + Thread { + Thread.sleep(DISPLAY_DELAY_MS) + notification.display() + }.start() + } + } + + private fun buildExtender( + context: Context, + notification: IDisplayableMutableNotification, + options: NotificationExtensionOptions, + ) = NotificationCompat.Extender { builder -> + if (options.logDetails) { + // Read the channel before anything below overwrites it. This is the only place an + // extension can see what the SDK picked. A restored notification lands on + // `restored_OS_notifications` no matter what the payload asked for, and the payload + // by itself never shows that. + DemoLog.d( + TAG, + "building androidNotificationId=${notification.androidNotificationId}" + + " channel=${NotificationCompat.getChannelId(builder.build())}", + ) + } + + if (options.applyExtender) { + builder.setContentTitle("[NSE] ${notification.title.orEmpty()}") + } + + if (options.forceHighImportanceChannel) { + forceHighImportanceChannel(context, builder) + } + + builder + } + + /** + * Moves the notification onto an IMPORTANCE_HIGH channel the app owns. This is the pattern a + * customer used to make every push heads-up, and on an SDK without the SDK-5011 fix it also + * makes every restored notification alert again after a reboot. + */ + private fun forceHighImportanceChannel( + context: Context, + builder: NotificationCompat.Builder, + ) { + builder.priority = NotificationCompat.PRIORITY_HIGH + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return + + val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + manager.createNotificationChannel( + NotificationChannel( + HIGH_IMPORTANCE_CHANNEL_ID, + "Demo high importance", + NotificationManager.IMPORTANCE_HIGH, + ), + ) + builder.setChannelId(HIGH_IMPORTANCE_CHANNEL_ID) + } + + private companion object { + const val TAG = "NSE" + const val HIGH_IMPORTANCE_CHANNEL_ID = "demo_nse_high_importance" + + // Well under the SDK's 30 second wait for the extension, and long enough to watch the + // notification arrive late on a device. + const val DISPLAY_DELAY_MS = 5_000L + } +} diff --git a/examples/demo/app/src/main/java/com/onesignal/example/ui/main/MainScreen.kt b/examples/demo/app/src/main/java/com/onesignal/example/ui/main/MainScreen.kt index 78c279f6bf..e1f0c6bbe9 100644 --- a/examples/demo/app/src/main/java/com/onesignal/example/ui/main/MainScreen.kt +++ b/examples/demo/app/src/main/java/com/onesignal/example/ui/main/MainScreen.kt @@ -38,6 +38,7 @@ import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp import com.onesignal.example.R +import com.onesignal.example.data.model.NotificationExtensionOptions import com.onesignal.example.data.model.NotificationType import com.onesignal.example.ui.components.LocalSnackbarController import com.onesignal.example.ui.components.PrimaryButton @@ -65,6 +66,7 @@ fun MainScreen(viewModel: MainViewModel) { val smsNumbers by viewModel.smsNumbers.observeAsState(emptyList()) val tags by viewModel.tags.observeAsState(emptyList()) val triggers by viewModel.triggers.observeAsState(emptyList()) + val notificationExtensionOptions by viewModel.notificationExtensionOptions.observeAsState(NotificationExtensionOptions()) val inAppMessagesPaused by viewModel.inAppMessagesPaused.observeAsState(false) val locationShared by viewModel.locationShared.observeAsState(false) val isLoading by viewModel.isLoading.observeAsState(false) @@ -161,6 +163,11 @@ fun MainScreen(viewModel: MainViewModel) { onInfoClick = { showTooltipDialog = "sendPushNotification" } ) + NotificationExtensionSection( + options = notificationExtensionOptions, + onOptionsChange = { viewModel.setNotificationExtensionOptions(it) } + ) + InAppMessagingSection( isPaused = inAppMessagesPaused, onPausedChange = { viewModel.setInAppMessagesPaused(it) }, diff --git a/examples/demo/app/src/main/java/com/onesignal/example/ui/main/MainViewModel.kt b/examples/demo/app/src/main/java/com/onesignal/example/ui/main/MainViewModel.kt index d2284ea8d6..2d6bd010fc 100644 --- a/examples/demo/app/src/main/java/com/onesignal/example/ui/main/MainViewModel.kt +++ b/examples/demo/app/src/main/java/com/onesignal/example/ui/main/MainViewModel.kt @@ -1,7 +1,6 @@ package com.onesignal.example.ui.main import android.app.Application -import android.util.Log import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData @@ -10,8 +9,10 @@ import com.onesignal.IUserJwtInvalidatedListener import com.onesignal.OneSignal import com.onesignal.UserJwtInvalidatedEvent import com.onesignal.notifications.IPermissionObserver +import com.onesignal.example.data.model.NotificationExtensionOptions import com.onesignal.example.data.model.NotificationType import com.onesignal.example.data.repository.OneSignalRepository +import com.onesignal.example.util.DemoLog import com.onesignal.example.util.SharedPreferenceUtil import com.onesignal.user.state.IUserStateObserver import com.onesignal.user.state.UserChangedState @@ -85,6 +86,10 @@ class MainViewModel(application: Application) : AndroidViewModel(application), I private val _locationShared = MutableLiveData() val locationShared: LiveData = _locationShared + // Notification service extension switches (demo app only, read by DemoNotificationServiceExtension) + private val _notificationExtensionOptions = MutableLiveData() + val notificationExtensionOptions: LiveData = _notificationExtensionOptions + // Identity Verification toggle (demo app only, controls alias used for API calls) private val _useIdentityVerification = MutableLiveData() val useIdentityVerification: LiveData = _useIdentityVerification @@ -110,14 +115,14 @@ class MainViewModel(application: Application) : AndroidViewModel(application), I private var fetchRequestSequence = 0L init { - Log.i(TAG, "App initialized") + DemoLog.i(TAG, "App initialized") loadInitialState() OneSignal.User.pushSubscription.addObserver(this) OneSignal.Notifications.addPermissionObserver(this) OneSignal.User.addObserver(this) OneSignal.addUserJwtInvalidatedListener(this) - Log.d(TAG, "init: observers registered, current onesignalId=${OneSignal.User.onesignalId}") - Log.d(TAG, "OneSignal ID: ${OneSignal.User.onesignalId ?: "not set"}") + DemoLog.d(TAG, "init: observers registered, current onesignalId=${OneSignal.User.onesignalId}") + DemoLog.d(TAG, "OneSignal ID: ${OneSignal.User.onesignalId ?: "not set"}") } // IPermissionObserver @@ -127,7 +132,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application), I // IUserStateObserver - called when user changes (login/logout) override fun onUserStateChange(state: UserChangedState) { - Log.d(TAG, "onUserStateChange fired: ${state.current.onesignalId}") + DemoLog.d(TAG, "onUserStateChange fired: ${state.current.onesignalId}") _oneSignalId.postValue(state.current.onesignalId) viewModelScope.launch(Dispatchers.Main) { loadExistingAliases() @@ -146,7 +151,8 @@ class MainViewModel(application: Application) : AndroidViewModel(application), I _inAppMessagesPaused.value = repository.isInAppMessagesPaused() _locationShared.value = repository.isLocationShared() _useIdentityVerification.value = SharedPreferenceUtil.getCachedIdentityVerification(context) - + _notificationExtensionOptions.value = SharedPreferenceUtil.getNotificationExtensionOptions(context) + val externalId = OneSignal.User.externalId _externalUserId.value = if (externalId.isEmpty()) null else externalId @@ -228,7 +234,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application), I } catch (e: CancellationException) { throw e } catch (e: Exception) { - android.util.Log.e("MainViewModel", "Error fetching user data", e) + DemoLog.e(TAG, "Error fetching user data", e) withContext(Dispatchers.Main) { if (requestId != fetchRequestSequence) return@withContext logError("Failed to fetch user data: ${e.message}") @@ -292,7 +298,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application), I repository.updateUserJwt(externalUserId, jwtToken) withContext(Dispatchers.Main) { SharedPreferenceUtil.cacheJwtToken(getApplication(), jwtToken) - Log.i(TAG, "Updated JWT for: $externalUserId") + DemoLog.i(TAG, "Updated JWT for: $externalUserId") } } } @@ -321,7 +327,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application), I fun setUseIdentityVerification(enabled: Boolean) { SharedPreferenceUtil.cacheIdentityVerification(getApplication(), enabled) _useIdentityVerification.value = enabled - Log.i(TAG, if (enabled) "Identity verification enabled" else "Identity verification disabled") + DemoLog.i(TAG, if (enabled) "Identity verification enabled" else "Identity verification disabled") } // Consent required @@ -329,7 +335,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application), I repository.setConsentRequired(required) SharedPreferenceUtil.cacheConsentRequired(getApplication(), required) _consentRequired.value = required - Log.i(TAG, if (required) "Consent required enabled" else "Consent required disabled") + DemoLog.i(TAG, if (required) "Consent required enabled" else "Consent required disabled") } // Privacy consent @@ -337,7 +343,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application), I repository.setPrivacyConsent(granted) SharedPreferenceUtil.cacheUserPrivacyConsent(getApplication(), granted) _privacyConsentGiven.value = granted - Log.i(TAG, if (granted) "Consent granted" else "Consent revoked") + DemoLog.i(TAG, if (granted) "Consent granted" else "Consent revoked") } // Alias operations (single and batch) @@ -348,7 +354,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application), I aliasesList.removeAll { it.first == label } aliasesList.add(Pair(label, id)) refreshAliases() - Log.i(TAG, "Alias added: $label") + DemoLog.i(TAG, "Alias added: $label") } } } @@ -363,7 +369,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application), I aliasesList.add(Pair(label, id)) } refreshAliases() - Log.i(TAG, "${pairs.size} alias(es) added") + DemoLog.i(TAG, "${pairs.size} alias(es) added") } } } @@ -374,7 +380,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application), I withContext(Dispatchers.Main) { aliasesList.removeAll { it.first == label } refreshAliases() - Log.i(TAG, "Alias removed: $label") + DemoLog.i(TAG, "Alias removed: $label") } } } @@ -385,7 +391,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application), I withContext(Dispatchers.Main) { aliasesList.removeAll { it.first in labels } refreshAliases() - Log.i(TAG, "${labels.size} alias(es) removed") + DemoLog.i(TAG, "${labels.size} alias(es) removed") } } } @@ -399,7 +405,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application), I emailsList.add(email) refreshEmails() } - Log.i(TAG, "Email added: $email") + DemoLog.i(TAG, "Email added: $email") } } } @@ -410,7 +416,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application), I withContext(Dispatchers.Main) { emailsList.remove(email) refreshEmails() - Log.i(TAG, "Email removed: $email") + DemoLog.i(TAG, "Email removed: $email") } } } @@ -424,7 +430,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application), I smsNumbersList.add(smsNumber) refreshSmsNumbers() } - Log.i(TAG, "SMS added: $smsNumber") + DemoLog.i(TAG, "SMS added: $smsNumber") } } } @@ -435,7 +441,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application), I withContext(Dispatchers.Main) { smsNumbersList.remove(smsNumber) refreshSmsNumbers() - Log.i(TAG, "SMS removed: $smsNumber") + DemoLog.i(TAG, "SMS removed: $smsNumber") } } } @@ -446,7 +452,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application), I repository.addTag(key, value) withContext(Dispatchers.Main) { loadExistingTags() - Log.i(TAG, "Tag added: $key") + DemoLog.i(TAG, "Tag added: $key") } } } @@ -457,7 +463,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application), I repository.addTags(map) withContext(Dispatchers.Main) { loadExistingTags() - Log.i(TAG, "${pairs.size} tag(s) added") + DemoLog.i(TAG, "${pairs.size} tag(s) added") } } } @@ -467,7 +473,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application), I repository.removeTag(key) withContext(Dispatchers.Main) { loadExistingTags() - Log.i(TAG, "Tag removed: $key") + DemoLog.i(TAG, "Tag removed: $key") } } } @@ -477,7 +483,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application), I repository.removeTags(keys) withContext(Dispatchers.Main) { loadExistingTags() - Log.i(TAG, "${keys.size} tag(s) removed") + DemoLog.i(TAG, "${keys.size} tag(s) removed") } } } @@ -490,7 +496,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application), I triggersList.removeAll { it.first == key } triggersList.add(Pair(key, value)) refreshTriggers() - Log.i(TAG, "Trigger added: $key") + DemoLog.i(TAG, "Trigger added: $key") } } } @@ -505,7 +511,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application), I triggersList.add(Pair(key, value)) } refreshTriggers() - Log.i(TAG, "${pairs.size} trigger(s) added") + DemoLog.i(TAG, "${pairs.size} trigger(s) added") } } } @@ -516,7 +522,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application), I withContext(Dispatchers.Main) { triggersList.removeAll { it.first == key } refreshTriggers() - Log.i(TAG, "Trigger removed: $key") + DemoLog.i(TAG, "Trigger removed: $key") } } } @@ -527,7 +533,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application), I withContext(Dispatchers.Main) { triggersList.removeAll { it.first in keys } refreshTriggers() - Log.i(TAG, "${keys.size} trigger(s) removed") + DemoLog.i(TAG, "${keys.size} trigger(s) removed") } } } @@ -539,7 +545,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application), I withContext(Dispatchers.Main) { triggersList.clear() refreshTriggers() - Log.i(TAG, "All triggers cleared") + DemoLog.i(TAG, "All triggers cleared") } } } @@ -548,21 +554,21 @@ class MainViewModel(application: Application) : AndroidViewModel(application), I fun sendOutcome(name: String) { viewModelScope.launch(Dispatchers.IO) { repository.sendOutcome(name) - withContext(Dispatchers.Main) { Log.i(TAG, "Outcome sent: $name") } + withContext(Dispatchers.Main) { DemoLog.i(TAG, "Outcome sent: $name") } } } fun sendUniqueOutcome(name: String) { viewModelScope.launch(Dispatchers.IO) { repository.sendUniqueOutcome(name) - withContext(Dispatchers.Main) { Log.i(TAG, "Unique outcome sent: $name") } + withContext(Dispatchers.Main) { DemoLog.i(TAG, "Unique outcome sent: $name") } } } fun sendOutcomeWithValue(name: String, value: Float) { viewModelScope.launch(Dispatchers.IO) { repository.sendOutcomeWithValue(name, value) - withContext(Dispatchers.Main) { Log.i(TAG, "Outcome sent: $name = $value") } + withContext(Dispatchers.Main) { DemoLog.i(TAG, "Outcome sent: $name = $value") } } } @@ -570,7 +576,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application), I fun trackEvent(name: String, properties: Map?) { viewModelScope.launch(Dispatchers.IO) { repository.trackEvent(name, properties) - withContext(Dispatchers.Main) { Log.i(TAG, "Event tracked: $name") } + withContext(Dispatchers.Main) { DemoLog.i(TAG, "Event tracked: $name") } } } @@ -580,7 +586,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application), I repository.setPushEnabled(enabled) withContext(Dispatchers.Main) { _pushEnabled.value = enabled - Log.i(TAG, if (enabled) "Push enabled" else "Push disabled") + DemoLog.i(TAG, if (enabled) "Push enabled" else "Push disabled") } } } @@ -608,7 +614,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application), I repository.setInAppMessagesPaused(paused) SharedPreferenceUtil.cacheInAppMessagingPausedStatus(getApplication(), paused) _inAppMessagesPaused.value = paused - Log.i(TAG, if (paused) "In-app messages paused" else "In-app messages resumed") + DemoLog.i(TAG, if (paused) "In-app messages paused" else "In-app messages resumed") } // Location @@ -616,19 +622,19 @@ class MainViewModel(application: Application) : AndroidViewModel(application), I repository.setLocationShared(shared) SharedPreferenceUtil.cacheLocationSharedStatus(getApplication(), shared) _locationShared.value = shared - Log.i(TAG, if (shared) "Location sharing enabled" else "Location sharing disabled") + DemoLog.i(TAG, if (shared) "Location sharing enabled" else "Location sharing disabled") } fun checkLocationShared(): Boolean { val shared = repository.isLocationShared() - Log.i(TAG, "Location shared: $shared") + DemoLog.i(TAG, "Location shared: $shared") return shared } fun promptLocation() { viewModelScope.launch(Dispatchers.IO) { repository.promptLocation() - withContext(Dispatchers.Main) { Log.i(TAG, "Location permission requested") } + withContext(Dispatchers.Main) { DemoLog.i(TAG, "Location permission requested") } } } @@ -639,7 +645,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application), I val success = repository.sendNotification(type) withContext(Dispatchers.Main) { if (success) { - Log.i(TAG, "Notification sent: ${type.title}") + DemoLog.i(TAG, "Notification sent: ${type.title}") } else { logError("Failed to send notification: ${type.title}") } @@ -653,7 +659,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application), I val success = repository.sendCustomNotification(title, body) withContext(Dispatchers.Main) { if (success) { - Log.i(TAG, "Notification sent: $title") + DemoLog.i(TAG, "Notification sent: $title") } else { logError("Failed to send notification: $title") } @@ -663,7 +669,14 @@ class MainViewModel(application: Application) : AndroidViewModel(application), I fun clearAllNotifications() { OneSignal.Notifications.clearAllNotifications() - Log.i(TAG, "All notifications cleared") + DemoLog.i(TAG, "All notifications cleared") + } + + // Notification service extension + fun setNotificationExtensionOptions(options: NotificationExtensionOptions) { + SharedPreferenceUtil.cacheNotificationExtensionOptions(getApplication(), options) + _notificationExtensionOptions.value = options + DemoLog.i(TAG, "Notification service extension options: $options") } fun sendInAppMessage(title: String, triggerKey: String, triggerValue: String) { @@ -673,13 +686,13 @@ class MainViewModel(application: Application) : AndroidViewModel(application), I triggersList.removeAll { it.first == triggerKey } triggersList.add(Pair(triggerKey, triggerValue)) refreshTriggers() - Log.i(TAG, "Sent In-App Message: $title") + DemoLog.i(TAG, "Sent In-App Message: $title") } } } - private fun logError(message: String) = Log.e(TAG, message) - private fun logDebug(message: String) = Log.d(TAG, message) + private fun logError(message: String) = DemoLog.e(TAG, message) + private fun logDebug(message: String) = DemoLog.d(TAG, message) override fun onPushSubscriptionChange(state: PushSubscriptionChangedState) { _pushSubscriptionId.postValue(state.current.id) @@ -687,7 +700,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application), I } override fun onUserJwtInvalidated(event: UserJwtInvalidatedEvent) { - Log.w(TAG, "JWT invalidated for externalId: ${event.externalId}") + DemoLog.w(TAG, "JWT invalidated for externalId: ${event.externalId}") } override fun onCleared() { diff --git a/examples/demo/app/src/main/java/com/onesignal/example/ui/main/Sections.kt b/examples/demo/app/src/main/java/com/onesignal/example/ui/main/Sections.kt index 3673320125..44287c61dd 100644 --- a/examples/demo/app/src/main/java/com/onesignal/example/ui/main/Sections.kt +++ b/examples/demo/app/src/main/java/com/onesignal/example/ui/main/Sections.kt @@ -23,6 +23,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import com.onesignal.example.data.model.InAppMessageType +import com.onesignal.example.data.model.NotificationExtensionOptions import com.onesignal.example.ui.components.CardKvRow import com.onesignal.example.ui.components.CollapsibleSingleList import com.onesignal.example.ui.components.CustomNotificationDialog @@ -290,6 +291,28 @@ fun SendPushSection( } } +@Composable +fun NotificationExtensionSection( + options: NotificationExtensionOptions, + onOptionsChange: (NotificationExtensionOptions) -> Unit, +) { + DemoSection { + SectionCard(title = "Notification Service Extension", sectionKey = "nse") { + // Only the master switch gets UI, keeping the section close to the other wrapper + // demos. The behavior switches stay code-level; flip the defaults in + // SharedPreferenceUtil.getNotificationExtensionOptions and rebuild. + ToggleRow( + label = "Enable Extension", + description = "Run the demo's extension on received pushes", + checked = options.enabled, + onCheckedChange = { onOptionsChange(options.copy(enabled = it)) }, + testTag = "nse_enabled_toggle", + contentDescription = "Enable notification service extension", + ) + } + } +} + @Composable fun InAppMessagingSection( isPaused: Boolean, diff --git a/examples/demo/app/src/main/java/com/onesignal/example/util/DemoLog.kt b/examples/demo/app/src/main/java/com/onesignal/example/util/DemoLog.kt new file mode 100644 index 0000000000..ac6985b447 --- /dev/null +++ b/examples/demo/app/src/main/java/com/onesignal/example/util/DemoLog.kt @@ -0,0 +1,34 @@ +package com.onesignal.example.util + +import android.util.Log + +/** + * Logging for the demo app. Marks both halves of every line with `[Demo]`, so `logcat -s` can + * filter on the tag and a line is still recognizable when only the message column is in view. + * + * ``` + * DemoLog.d(TAG, "Sending notification: Simple") + * // D/[Demo]MainViewModel: [Demo] Sending notification: Simple + * ``` + * + * Pass the plain class name as the tag. This adds the prefix. + * + * SDK output that MainApplication's log listener forwards does not come through here. Those + * lines belong to the SDK, and marking them would bury the demo's own output when you grep. + */ +object DemoLog { + private const val PREFIX = "[Demo]" + + fun v(tag: String, message: String) = Log.v(PREFIX + tag, "$PREFIX $message") + + fun d(tag: String, message: String) = Log.d(PREFIX + tag, "$PREFIX $message") + + fun i(tag: String, message: String) = Log.i(PREFIX + tag, "$PREFIX $message") + + fun w(tag: String, message: String) = Log.w(PREFIX + tag, "$PREFIX $message") + + fun e(tag: String, message: String) = Log.e(PREFIX + tag, "$PREFIX $message") + + fun e(tag: String, message: String, throwable: Throwable) = + Log.e(PREFIX + tag, "$PREFIX $message", throwable) +} diff --git a/examples/demo/app/src/main/java/com/onesignal/example/util/SharedPreferenceUtil.kt b/examples/demo/app/src/main/java/com/onesignal/example/util/SharedPreferenceUtil.kt index 86edcf6167..5c94620b62 100644 --- a/examples/demo/app/src/main/java/com/onesignal/example/util/SharedPreferenceUtil.kt +++ b/examples/demo/app/src/main/java/com/onesignal/example/util/SharedPreferenceUtil.kt @@ -2,6 +2,7 @@ package com.onesignal.example.util import android.content.Context import android.content.SharedPreferences +import com.onesignal.example.data.model.NotificationExtensionOptions object SharedPreferenceUtil { @@ -14,6 +15,15 @@ object SharedPreferenceUtil { private const val IDENTITY_VERIFICATION_PREF = "IDENTITY_VERIFICATION_PREF" private const val JWT_TOKEN_PREF = "JWT_TOKEN_PREF" + // Notification service extension switches. DemoNotificationServiceExtension reads these + // directly because it runs whether or not the app is open. + private const val NSE_ENABLED_PREF = "NSE_ENABLED_PREF" + private const val NSE_LOG_DETAILS_PREF = "NSE_LOG_DETAILS_PREF" + private const val NSE_APPLY_EXTENDER_PREF = "NSE_APPLY_EXTENDER_PREF" + private const val NSE_FORCE_HIGH_IMPORTANCE_PREF = "NSE_FORCE_HIGH_IMPORTANCE_PREF" + private const val NSE_DELAY_DISPLAY_PREF = "NSE_DELAY_DISPLAY_PREF" + private const val NSE_DISCARD_PREF = "NSE_DISCARD_PREF" + private fun getSharedPreference(context: Context): SharedPreferences { return context.getSharedPreferences(APP_SHARED_PREFS, Context.MODE_PRIVATE) } @@ -80,4 +90,29 @@ object SharedPreferenceUtil { fun cacheJwtToken(context: Context, token: String?) { getSharedPreference(context).edit().putString(JWT_TOKEN_PREF, token).apply() } + + // Every switch defaults to false so a fresh install behaves as if no extension were + // registered. See NotificationExtensionOptions. + fun getNotificationExtensionOptions(context: Context): NotificationExtensionOptions { + val prefs = getSharedPreference(context) + return NotificationExtensionOptions( + enabled = prefs.getBoolean(NSE_ENABLED_PREF, false), + logDetails = prefs.getBoolean(NSE_LOG_DETAILS_PREF, false), + applyExtender = prefs.getBoolean(NSE_APPLY_EXTENDER_PREF, false), + forceHighImportanceChannel = prefs.getBoolean(NSE_FORCE_HIGH_IMPORTANCE_PREF, false), + delayDisplay = prefs.getBoolean(NSE_DELAY_DISPLAY_PREF, false), + discard = prefs.getBoolean(NSE_DISCARD_PREF, false), + ) + } + + fun cacheNotificationExtensionOptions(context: Context, options: NotificationExtensionOptions) { + getSharedPreference(context).edit() + .putBoolean(NSE_ENABLED_PREF, options.enabled) + .putBoolean(NSE_LOG_DETAILS_PREF, options.logDetails) + .putBoolean(NSE_APPLY_EXTENDER_PREF, options.applyExtender) + .putBoolean(NSE_FORCE_HIGH_IMPORTANCE_PREF, options.forceHighImportanceChannel) + .putBoolean(NSE_DELAY_DISPLAY_PREF, options.delayDisplay) + .putBoolean(NSE_DISCARD_PREF, options.discard) + .apply() + } } diff --git a/examples/demo/app/src/main/java/com/onesignal/example/util/TooltipHelper.kt b/examples/demo/app/src/main/java/com/onesignal/example/util/TooltipHelper.kt index 95031977e4..7097827328 100644 --- a/examples/demo/app/src/main/java/com/onesignal/example/util/TooltipHelper.kt +++ b/examples/demo/app/src/main/java/com/onesignal/example/util/TooltipHelper.kt @@ -72,7 +72,7 @@ object TooltipHelper { } } catch (e: Exception) { // Tooltips are non-critical; log and continue - android.util.Log.w("TooltipHelper", "Failed to fetch tooltip content: ${e.message}") + DemoLog.w("TooltipHelper", "Failed to fetch tooltip content: ${e.message}") } } }