diff --git a/OneSignalSDK/build.gradle b/OneSignalSDK/build.gradle index c457b7663b..878c7ec981 100644 --- a/OneSignalSDK/build.gradle +++ b/OneSignalSDK/build.gradle @@ -15,8 +15,8 @@ buildscript { huaweiHMSLocationVersion = '4.0.0.300' kotlinVersion = '1.7.10' kotestVersion = '5.5.0' - ktlintPluginVersion = '11.3.1' - ktlintVersion = '0.48.2' + ktlintPluginVersion = '11.6.1' + ktlintVersion = '1.0.1' junitVersion = '4.13.2' } diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/JSONObjectExtensions.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/JSONObjectExtensions.kt index 519bb08c6e..06ae8dcce4 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/JSONObjectExtensions.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/JSONObjectExtensions.kt @@ -34,9 +34,9 @@ fun JSONObject.safeLong(name: String): Long? { } /** - * Retrieve an [Double] from the [JSONObject] safely. + * Retrieve a [Double] from the [JSONObject] safely. * - * @param name The name of the attribute that contains an [Int] value. + * @param name The name of the attribute that contains a [Double] value. * * @return The [Double] value if it exists, null otherwise. */ diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/events/EventProducer.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/events/EventProducer.kt index a024be9a0f..1e39c656b6 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/events/EventProducer.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/events/EventProducer.kt @@ -16,19 +16,25 @@ open class EventProducer : IEventNotifier { private val subscribers: MutableList = Collections.synchronizedList(mutableListOf()) override fun subscribe(handler: THandler) { - subscribers.add(handler) + synchronized(subscribers) { + subscribers.add(handler) + } } override fun unsubscribe(handler: THandler) { - subscribers.remove(handler) + synchronized(subscribers) { + subscribers.remove(handler) + } } /** * Subscribe all from an existing producer to this subscriber. */ fun subscribeAll(from: EventProducer) { - for (s in from.subscribers) { - subscribe(s) + synchronized(subscribers) { + for (s in from.subscribers) { + subscribe(s) + } } } diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/modeling/ModelStore.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/modeling/ModelStore.kt index 083afccbe0..5c9c7b6c67 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/modeling/ModelStore.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/modeling/ModelStore.kt @@ -40,10 +40,11 @@ abstract class ModelStore( model: TModel, tag: String, ) { - val oldModel = models.firstOrNull { it.id == model.id } - if (oldModel != null) { - removeItem(oldModel, tag) - } + synchronized(models) { + val oldModel = models.firstOrNull { it.id == model.id } + if (oldModel != null) { + removeItem(oldModel, tag) + } addItem(model, tag) } @@ -54,10 +55,11 @@ abstract class ModelStore( model: TModel, tag: String, ) { - val oldModel = models.firstOrNull { it.id == model.id } - if (oldModel != null) { - removeItem(oldModel, tag) - } + synchronized(models) { + val oldModel = models.firstOrNull { it.id == model.id } + if (oldModel != null) { + removeItem(oldModel, tag) + } addItem(model, tag, index) } @@ -75,8 +77,10 @@ abstract class ModelStore( id: String, tag: String, ) { - val model = models.firstOrNull { it.id == id } ?: return - removeItem(model, tag) + synchronized(models) { + val model = models.firstOrNull { it.id == id } ?: return + removeItem(model, tag) + } } override fun onChanged( @@ -91,7 +95,8 @@ abstract class ModelStore( models: List, tag: String, ) { - clear(tag) + synchronized(models) { + clear(tag) for (model in models) { add(model, tag) @@ -118,11 +123,12 @@ abstract class ModelStore( tag: String, index: Int? = null, ) { - if (index != null) { - models.add(index, model) - } else { - models.add(model) - } + synchronized(models) { + if (index != null) { + models.add(index, model) + } else { + models.add(model) + } // listen for changes to this model model.subscribe(this) @@ -136,7 +142,8 @@ abstract class ModelStore( model: TModel, tag: String, ) { - models.remove(model) + synchronized(models) { + models.remove(model) // no longer listen for changes to this model model.unsubscribe(this) @@ -147,23 +154,29 @@ abstract class ModelStore( } protected fun load() { - if (name != null && _prefs != null) { - val str = _prefs.getString(PreferenceStores.ONESIGNAL, PreferenceOneSignalKeys.MODEL_STORE_PREFIX + name, "[]") - val jsonArray = JSONArray(str) - for (index in 0 until jsonArray.length()) { - val newModel = create(jsonArray.getJSONObject(index)) ?: continue - models.add(newModel) - // listen for changes to this model - newModel.subscribe(this) + synchronized(models) { + if (name != null && _prefs != null) { + val str = _prefs.getString(PreferenceStores.ONESIGNAL, PreferenceOneSignalKeys.MODEL_STORE_PREFIX + name, "[]") + val jsonArray = JSONArray(str) + for (index in 0 until jsonArray.length()) { + val newModel = create(jsonArray.getJSONObject(index)) ?: continue + models.add(newModel) + // listen for changes to this model + newModel.subscribe(this) + } } } } fun persist() { - if (name != null && _prefs != null) { - val jsonArray = JSONArray() - for (model in models) { - jsonArray.put(model.toJSON()) + synchronized(models) { + if (name != null && _prefs != null) { + val jsonArray = JSONArray() + for (model in models) { + jsonArray.put(model.toJSON()) + } + + _prefs.saveString(PreferenceStores.ONESIGNAL, PreferenceOneSignalKeys.MODEL_STORE_PREFIX + name, jsonArray.toString()) } } } diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/modeling/SingletonModelStore.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/modeling/SingletonModelStore.kt index 5bffa828bc..b00410eeb9 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/modeling/SingletonModelStore.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/common/modeling/SingletonModelStore.kt @@ -20,15 +20,17 @@ open class SingletonModelStore( override val model: TModel get() { - val model = store.get(singletonId) - if (model != null) { - return model - } + synchronized(this) { + val model = store.get(singletonId) + if (model != null) { + return model + } - val createdModel = store.create() ?: throw Exception("Unable to initialize model from store $store") - createdModel.id = singletonId - store.add(createdModel) - return createdModel + val createdModel = store.create() ?: throw Exception("Unable to initialize model from store $store") + createdModel.id = singletonId + store.add(createdModel) + return createdModel + } } override fun replace( diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/internal/OneSignalImp.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/internal/OneSignalImp.kt index de9debf0ea..a8c2b4611a 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/internal/OneSignalImp.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/internal/OneSignalImp.kt @@ -51,6 +51,7 @@ import com.onesignal.user.internal.subscriptions.SubscriptionModel import com.onesignal.user.internal.subscriptions.SubscriptionModelStore import com.onesignal.user.internal.subscriptions.SubscriptionStatus import com.onesignal.user.internal.subscriptions.SubscriptionType +import org.json.JSONObject internal class OneSignalImp : IOneSignal, IServiceProvider { override val sdkVersion: String = OneSignalUtils.SDK_VERSION diff --git a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/impl/executors/LoginUserOperationExecutor.kt b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/impl/executors/LoginUserOperationExecutor.kt index 1c34da6f51..3f29b90975 100644 --- a/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/impl/executors/LoginUserOperationExecutor.kt +++ b/OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/impl/executors/LoginUserOperationExecutor.kt @@ -137,7 +137,8 @@ internal class LoginUserOperationExecutor( var identities = mapOf() var subscriptions = mapOf() val properties = mutableMapOf() - properties["timezone_id"] = TimeUtils.getTimeZoneId() + properties["timezone_id"] = TimeUtils.getTimeZoneId()!! + properties["language"] = _languageContext.language if (createUserOperation.externalId != null) { val mutableIdentities = identities.toMutableMap() diff --git a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/operations/RefreshUserOperationExecutorTests.kt b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/operations/RefreshUserOperationExecutorTests.kt index b8f5a51c1f..c2ee861d8e 100644 --- a/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/operations/RefreshUserOperationExecutorTests.kt +++ b/OneSignalSDK/onesignal/core/src/test/java/com/onesignal/user/internal/operations/RefreshUserOperationExecutorTests.kt @@ -43,7 +43,7 @@ class RefreshUserOperationExecutorTests : FunSpec({ CreateUserResponse( mapOf(IdentityConstants.ONESIGNAL_ID to remoteOneSignalId, "aliasLabel1" to "aliasValue1"), PropertiesObject(country = "US"), - listOf(SubscriptionObject(remoteSubscriptionId1, SubscriptionObjectType.ANDROID_PUSH, enabled = true, token = "pushToken"), SubscriptionObject(remoteSubscriptionId2, SubscriptionObjectType.EMAIL, token = "name@company.com")), + listOf(SubscriptionObject(existingSubscriptionId1, SubscriptionObjectType.ANDROID_PUSH, enabled = true, token = "pushToken1"), SubscriptionObject(remoteSubscriptionId1, SubscriptionObjectType.ANDROID_PUSH, enabled = true, token = "pushToken2"), SubscriptionObject(remoteSubscriptionId2, SubscriptionObjectType.EMAIL, token = "name@company.com")), ) // Given diff --git a/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/permissions/impl/NavigateToAndroidSettingsForNotifications.kt b/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/permissions/impl/NavigateToAndroidSettingsForNotifications.kt index eff10a74a2..9f0d9752ca 100644 --- a/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/permissions/impl/NavigateToAndroidSettingsForNotifications.kt +++ b/OneSignalSDK/onesignal/notifications/src/main/java/com/onesignal/notifications/internal/permissions/impl/NavigateToAndroidSettingsForNotifications.kt @@ -38,10 +38,7 @@ internal object NavigateToAndroidSettingsForNotifications { // for Android 5-7 intent.putExtra("app_package", context.getPackageName()) - val applicationInfo = ApplicationInfoHelper.getInfo(context) - if (applicationInfo != null) { - intent.putExtra("app_uid", applicationInfo.uid) - } + intent.putExtra("app_uid", context.getApplicationInfo().uid) // for Android 8 and above intent.putExtra("android.provider.extra.APP_PACKAGE", context.getPackageName()) diff --git a/OneSignalSDK/onesignal/src/main/java/com/onesignal/ActivityLifecycleHandler.java b/OneSignalSDK/onesignal/src/main/java/com/onesignal/ActivityLifecycleHandler.java deleted file mode 100644 index 64a7d0fa51..0000000000 --- a/OneSignalSDK/onesignal/src/main/java/com/onesignal/ActivityLifecycleHandler.java +++ /dev/null @@ -1,288 +0,0 @@ -/** - * Modified MIT License - *

- * Copyright 2017 OneSignal - *

- * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - *

- * 1. The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - *

- * 2. All copies of substantial portions of the Software may only be used in connection - * with services provided by OneSignal. - *

- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ - -package com.onesignal; - -import android.annotation.SuppressLint; -import android.app.Activity; -import android.content.pm.ActivityInfo; -import android.content.res.Configuration; -import android.os.Build; -import android.view.ViewTreeObserver; - -import androidx.annotation.NonNull; - -import org.jetbrains.annotations.NotNull; - -import java.lang.ref.WeakReference; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -class ActivityLifecycleHandler implements OSSystemConditionController.OSSystemConditionHandler { - - abstract static class ActivityAvailableListener { - void available(@NonNull Activity activity) { - } - - void stopped(@NonNull Activity activity) { - } - - void lostFocus() { - } - } - - private static final Map sActivityAvailableListeners = new ConcurrentHashMap<>(); - private static final Map sSystemConditionObservers = new ConcurrentHashMap<>(); - private static final Map sKeyboardListeners = new ConcurrentHashMap<>(); - - private static final String FOCUS_LOST_WORKER_TAG = "FOCUS_LOST_WORKER_TAG"; - // We want to perform a on_focus sync as soon as the session is done to report the time - private static final int SYNC_AFTER_BG_DELAY_MS = 2000; - - private final OSFocusHandler focusHandler; - - @SuppressLint("StaticFieldLeak") - private Activity curActivity = null; - private boolean nextResumeIsFirstActivity = false; - - public ActivityLifecycleHandler(OSFocusHandler focusHandler) { - this.focusHandler = focusHandler; - } - - void onConfigurationChanged(Configuration newConfig, Activity activity) { - // If Activity contains the configChanges orientation flag, re-create the view this way - if (curActivity != null && OSUtils.hasConfigChangeFlag(curActivity, ActivityInfo.CONFIG_ORIENTATION)) { - logOrientationChange(newConfig.orientation, activity); - onOrientationChanged(activity); - } - } - - void onActivityCreated(Activity activity) { - } - - void onActivityStarted(Activity activity) { - focusHandler.startOnStartFocusWork(); - } - - void onActivityResumed(Activity activity) { - OneSignal.Log(OneSignal.LOG_LEVEL.DEBUG, "onActivityResumed: " + activity); - setCurActivity(activity); - logCurActivity(); - handleFocus(); - } - - void onActivityPaused(Activity activity) { - OneSignal.Log(OneSignal.LOG_LEVEL.DEBUG, "onActivityPaused: " + activity); - if (activity == curActivity) { - curActivity = null; - handleLostFocus(); - } - - logCurActivity(); - } - - void onActivityStopped(Activity activity) { - OneSignal.Log(OneSignal.LOG_LEVEL.DEBUG, "onActivityStopped: " + activity); - - if (activity == curActivity) { - curActivity = null; - handleLostFocus(); - } - - for (Map.Entry entry : sActivityAvailableListeners.entrySet()) { - entry.getValue().stopped(activity); - } - - logCurActivity(); - - if (curActivity == null) - focusHandler.startOnStopFocusWork(); - } - - void onActivityDestroyed(Activity activity) { - OneSignal.Log(OneSignal.LOG_LEVEL.DEBUG, "onActivityDestroyed: " + activity); - sKeyboardListeners.clear(); - - if (activity == curActivity) { - curActivity = null; - handleLostFocus(); - } - - logCurActivity(); - } - - private void logCurActivity() { - OneSignal.Log(OneSignal.LOG_LEVEL.DEBUG, "curActivity is NOW: " + (curActivity != null ? "" + curActivity.getClass().getName() + ":" + curActivity : "null")); - } - - private void logOrientationChange(int orientation, Activity activity) { - // Log device orientation change - if (orientation == Configuration.ORIENTATION_LANDSCAPE) - OneSignal.onesignalLog(OneSignal.LOG_LEVEL.DEBUG, "Configuration Orientation Change: LANDSCAPE (" + orientation + ") on activity: " + activity); - else if (orientation == Configuration.ORIENTATION_PORTRAIT) - OneSignal.onesignalLog(OneSignal.LOG_LEVEL.DEBUG, "Configuration Orientation Change: PORTRAIT (" + orientation + ") on activity: " + activity); - } - - /** - * Takes pieces from onActivityResumed and onActivityStopped to recreate the view when the - * phones orientation is changed from manual detection using the onConfigurationChanged callback - * This fix was originally implemented for In App Messages not being re-shown when orientation - * was changed on wrapper SDK apps - */ - private void onOrientationChanged(Activity activity) { - // Remove view - handleLostFocus(); - for (Map.Entry entry : sActivityAvailableListeners.entrySet()) { - entry.getValue().stopped(activity); - } - - // Show view - for (Map.Entry entry : sActivityAvailableListeners.entrySet()) { - entry.getValue().available(curActivity); - } - - ViewTreeObserver treeObserver = curActivity.getWindow().getDecorView().getViewTreeObserver(); - for (Map.Entry entry : sSystemConditionObservers.entrySet()) { - KeyboardListener keyboardListener = new KeyboardListener(this, entry.getValue(), entry.getKey()); - treeObserver.addOnGlobalLayoutListener(keyboardListener); - sKeyboardListeners.put(entry.getKey(), keyboardListener); - } - handleFocus(); - } - - private void handleLostFocus() { - OneSignal.onesignalLog(OneSignal.LOG_LEVEL.DEBUG, "ActivityLifecycleHandler Handling lost focus"); - if (focusHandler == null || focusHandler.hasBackgrounded() && !focusHandler.hasCompleted()) - return; - - new Thread() { - public void run() { - // Run on it's own thread since both these calls do disk I/O - // which could contribute a significant amount to ANRs. - OneSignal.getFocusTimeController().appStopped(); - focusHandler.startOnLostFocusWorker(FOCUS_LOST_WORKER_TAG, SYNC_AFTER_BG_DELAY_MS, OneSignal.appContext); - } - }.start(); - } - - private void handleFocus() { - OneSignal.onesignalLog(OneSignal.LOG_LEVEL.DEBUG, "ActivityLifecycleHandler handleFocus, nextResumeIsFirstActivity: " + nextResumeIsFirstActivity); - if (focusHandler.hasBackgrounded() || nextResumeIsFirstActivity) { - OneSignal.onesignalLog(OneSignal.LOG_LEVEL.DEBUG, "ActivityLifecycleHandler reset background state, call app focus"); - nextResumeIsFirstActivity = false; - focusHandler.startOnFocusWork(); - } else { - OneSignal.onesignalLog(OneSignal.LOG_LEVEL.DEBUG, "ActivityLifecycleHandler cancel background lost focus worker"); - focusHandler.cancelOnLostFocusWorker(FOCUS_LOST_WORKER_TAG, OneSignal.appContext); - } - } - - void addSystemConditionObserver(String key, OSSystemConditionController.OSSystemConditionObserver systemConditionObserver) { - if (curActivity != null) { - ViewTreeObserver treeObserver = curActivity.getWindow().getDecorView().getViewTreeObserver(); - KeyboardListener keyboardListener = new KeyboardListener(this, systemConditionObserver, key); - treeObserver.addOnGlobalLayoutListener(keyboardListener); - sKeyboardListeners.put(key, keyboardListener); - } - sSystemConditionObservers.put(key, systemConditionObserver); - } - - public void removeSystemConditionObserver(@NotNull String key, @NotNull KeyboardListener keyboardListener) { - if (curActivity != null) { - ViewTreeObserver treeObserver = curActivity.getWindow().getDecorView().getViewTreeObserver(); - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) { - treeObserver.removeGlobalOnLayoutListener(keyboardListener); - } else { - treeObserver.removeOnGlobalLayoutListener(keyboardListener); - } - } - - sKeyboardListeners.remove(key); - sSystemConditionObservers.remove(key); - } - - void addActivityAvailableListener(String key, ActivityAvailableListener activityAvailableListener) { - sActivityAvailableListeners.put(key, activityAvailableListener); - if (curActivity != null) - activityAvailableListener.available(curActivity); - } - - void removeActivityAvailableListener(String key) { - sActivityAvailableListeners.remove(key); - } - - public Activity getCurActivity() { - return curActivity; - } - - public void setCurActivity(Activity activity) { - curActivity = activity; - for (Map.Entry entry : sActivityAvailableListeners.entrySet()) { - entry.getValue().available(curActivity); - } - - try { - ViewTreeObserver treeObserver = curActivity.getWindow().getDecorView().getViewTreeObserver(); - for (Map.Entry entry : sSystemConditionObservers.entrySet()) { - KeyboardListener keyboardListener = new KeyboardListener(this, entry.getValue(), entry.getKey()); - treeObserver.addOnGlobalLayoutListener(keyboardListener); - sKeyboardListeners.put(entry.getKey(), keyboardListener); - } - } catch (RuntimeException e) { - // Related to Unity Issue #239 on Github - // https://github.com/OneSignal/OneSignal-Unity-SDK/issues/239 - // RuntimeException at ActivityLifecycleHandler.setCurActivity on Android (Unity 2.9.0) - e.printStackTrace(); - } - } - - void setNextResumeIsFirstActivity(boolean nextResumeIsFirstActivity) { - this.nextResumeIsFirstActivity = nextResumeIsFirstActivity; - } - - static class KeyboardListener implements ViewTreeObserver.OnGlobalLayoutListener { - - private final OSSystemConditionController.OSSystemConditionObserver observer; - private final OSSystemConditionController.OSSystemConditionHandler systemConditionListener; - private final String key; - - private KeyboardListener(OSSystemConditionController.OSSystemConditionHandler systemConditionListener, OSSystemConditionController.OSSystemConditionObserver observer, String key) { - this.systemConditionListener = systemConditionListener; - this.observer = observer; - this.key = key; - } - - @Override - public void onGlobalLayout() { - boolean keyboardUp = OSViewUtils.isKeyboardUp(new WeakReference<>(OneSignal.getCurrentActivity())); - if (!keyboardUp) { - systemConditionListener.removeSystemConditionObserver(key, this); - observer.systemConditionChanged(); - } - } - } -} \ No newline at end of file diff --git a/OneSignalSDK/onesignal/src/main/java/com/onesignal/ApplicationInfoHelper.kt b/OneSignalSDK/onesignal/src/main/java/com/onesignal/ApplicationInfoHelper.kt deleted file mode 100644 index 907a2e3252..0000000000 --- a/OneSignalSDK/onesignal/src/main/java/com/onesignal/ApplicationInfoHelper.kt +++ /dev/null @@ -1,41 +0,0 @@ -package com.onesignal - -import android.annotation.TargetApi -import android.content.Context -import android.content.pm.ApplicationInfo -import android.content.pm.PackageManager -import android.os.DeadSystemException -import android.util.AndroidException - -class ApplicationInfoHelper { - companion object { - // Safe to cache as nothing can change the app while it is running. - private var cachedInfo: ApplicationInfo? = null - - @TargetApi(24) - fun getInfo(context: Context): ApplicationInfo? { - if (cachedInfo != null) { - return cachedInfo - } - - val packageManager = context.packageManager - return try { - // Using this instead of context.applicationInfo as it's metaData is always null - cachedInfo = packageManager.getApplicationInfo( - context.packageName, - PackageManager.GET_META_DATA, - ) - cachedInfo - } catch (e: AndroidException) { - // Suppressing DeadSystemException as the app is already dying for - // another reason and allowing this exception to bubble up would - // create a red herring for app developers. We still re-throw - // others, as we don't want to silently hide other issues. - if (e !is DeadSystemException) { - throw e - } - null - } - } - } -} diff --git a/OneSignalSDK/onesignal/src/main/java/com/onesignal/BadgeCountUpdater.java b/OneSignalSDK/onesignal/src/main/java/com/onesignal/BadgeCountUpdater.java deleted file mode 100644 index 49aeef50cd..0000000000 --- a/OneSignalSDK/onesignal/src/main/java/com/onesignal/BadgeCountUpdater.java +++ /dev/null @@ -1,130 +0,0 @@ -/** - * Modified MIT License - * - * Copyright 2017 OneSignal - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * 1. The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * 2. All copies of substantial portions of the Software may only be used in connection - * with services provided by OneSignal. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ - -package com.onesignal; - -import android.content.Context; -import android.content.pm.ApplicationInfo; -import android.database.Cursor; -import android.os.Build; -import android.os.Bundle; -import android.service.notification.StatusBarNotification; -import androidx.annotation.RequiresApi; - -import com.onesignal.OneSignalDbContract.NotificationTable; -import com.onesignal.shortcutbadger.ShortcutBadgeException; -import com.onesignal.shortcutbadger.ShortcutBadger; - -import static com.onesignal.NotificationLimitManager.MAX_NUMBER_OF_NOTIFICATIONS_STR; - -class BadgeCountUpdater { - - // Cache for manifest setting. - private static int badgesEnabled = -1; - - private static boolean areBadgeSettingsEnabled(Context context) { - if (badgesEnabled != -1) - return (badgesEnabled == 1); - - ApplicationInfo ai = ApplicationInfoHelper.Companion.getInfo(context); - if (ai == null) { - badgesEnabled = 0; - OneSignal.Log(OneSignal.LOG_LEVEL.ERROR, "Error reading meta-data tag 'com.onesignal.BadgeCount'. Disabling badge setting."); - return false; - } - - Bundle bundle = ai.metaData; - if (bundle != null) { - String defaultStr = bundle.getString("com.onesignal.BadgeCount"); - badgesEnabled = "DISABLE".equals(defaultStr) ? 0 : 1; - } - else - badgesEnabled = 1; - - return (badgesEnabled == 1); - } - - private static boolean areBadgesEnabled(Context context) { - return areBadgeSettingsEnabled(context) && OSUtils.areNotificationsEnabled(context); - } - - static void update(OneSignalDb db, Context context) { - if (!areBadgesEnabled(context)) - return; - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) - updateStandard(context); - else - updateFallback(db, context); - } - - @RequiresApi(api = Build.VERSION_CODES.M) - private static void updateStandard(Context context) { - StatusBarNotification[] activeNotifs = OneSignalNotificationManager.getActiveNotifications(context); - - int runningCount = 0; - for (StatusBarNotification activeNotif : activeNotifs) { - if (NotificationLimitManager.isGroupSummary(activeNotif)) - continue; - runningCount++; - } - - updateCount(runningCount, context); - } - - private static void updateFallback(OneSignalDb db, Context context) { - Cursor cursor = db.query( - NotificationTable.TABLE_NAME, - null, - OneSignalDbHelper.recentUninteractedWithNotificationsWhere().toString(), - null, // Where args - null, // group by - null, // filter by row groups - null, // sort order, new to old - MAX_NUMBER_OF_NOTIFICATIONS_STR - ); - - int notificationCount = cursor.getCount(); - cursor.close(); - - updateCount(notificationCount, context); - } - - static void updateCount(int count, Context context) { - if (!areBadgeSettingsEnabled(context)) - return; - - try { - ShortcutBadger.applyCountOrThrow(context, count); - } catch (ShortcutBadgeException e) { - // Suppress error as there are normal cases where this will throw - // Can throw if: - // - Badges are not support on the device. - // - App does not have a default launch Activity. - } - } -} diff --git a/OneSignalSDK/onesignal/src/main/java/com/onesignal/CallbackThreadManager.kt b/OneSignalSDK/onesignal/src/main/java/com/onesignal/CallbackThreadManager.kt deleted file mode 100644 index d6389433ee..0000000000 --- a/OneSignalSDK/onesignal/src/main/java/com/onesignal/CallbackThreadManager.kt +++ /dev/null @@ -1,59 +0,0 @@ -/** - * Modified MIT License - *

- * Copyright 2023 OneSignal - *

- * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - *

- * 1. The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - *

- * 2. All copies of substantial portions of the Software may only be used in connection - * with services provided by OneSignal. - *

- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ - -package com.onesignal - -import kotlin.concurrent.thread - -/** - * Provides a public API to allow changing which thread callbacks and observers - * should fire on. - * - * Initial motivation for this is to allow the OneSignal-Unity-SDK to config - * the SDK to fire off the main thread. This is to avoid cases where Unity may - * cause the main UI thread to wait on a background thread when calling back - * into Unity. - * - * Usage: CallbackThreadManager.preference = UseThread.Background - */ -class CallbackThreadManager { - enum class UseThread { - MainUI, - Background - } - - companion object { - var preference = UseThread.MainUI - - fun runOnPreferred(runnable: Runnable) { - when (preference) { - UseThread.MainUI -> OSUtils.runOnMainUIThread(runnable) - UseThread.Background -> thread { runnable.run() } - } - } - } -} diff --git a/OneSignalSDK/onesignal/src/main/java/com/onesignal/GenerateNotification.java b/OneSignalSDK/onesignal/src/main/java/com/onesignal/GenerateNotification.java deleted file mode 100644 index d68d27445f..0000000000 --- a/OneSignalSDK/onesignal/src/main/java/com/onesignal/GenerateNotification.java +++ /dev/null @@ -1,1136 +0,0 @@ -/** - * Modified MIT License - * - * Copyright 2017 OneSignal - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * 1. The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * 2. All copies of substantial portions of the Software may only be used in connection - * with services provided by OneSignal. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ - -package com.onesignal; - -import android.R.drawable; -import android.app.Notification; -import android.app.PendingIntent; -import android.content.ContentValues; -import android.content.Context; -import android.content.Intent; -import android.content.pm.ApplicationInfo; -import android.content.res.Resources; -import android.database.Cursor; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.net.Uri; -import android.os.Build; -import android.service.notification.StatusBarNotification; -import android.text.SpannableString; -import android.text.style.StyleSpan; -import android.widget.RemoteViews; - -import androidx.annotation.RequiresApi; -import androidx.annotation.WorkerThread; -import androidx.core.app.NotificationCompat; -import androidx.core.app.NotificationManagerCompat; - -import com.onesignal.OneSignalDbContract.NotificationTable; - -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -import java.lang.reflect.Field; -import java.math.BigInteger; -import java.net.URL; -import java.security.SecureRandom; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.List; -import java.util.Random; - -import static com.onesignal.OSUtils.getResourceString; - -class GenerateNotification { - - public static final String OS_SHOW_NOTIFICATION_THREAD = "OS_SHOW_NOTIFICATION_THREAD"; - - public static final String BUNDLE_KEY_ANDROID_NOTIFICATION_ID = "androidNotificationId"; - public static final String BUNDLE_KEY_ACTION_ID = "actionId"; - // Bundle key the whole OneSignal payload will be placed into as JSON and attached to the - // notification Intent. - public static final String BUNDLE_KEY_ONESIGNAL_DATA = "onesignalData"; - - private static Class notificationOpenedClass = NotificationOpenedReceiver.class; - private static Class notificationDismissedClass = NotificationDismissReceiver.class; - private static Resources contextResources = null; - private static Context currentContext = null; - private static String packageName = null; - private static Integer groupAlertBehavior = null; - - private static class OneSignalNotificationBuilder { - NotificationCompat.Builder compatBuilder; - boolean hasLargeIcon; - } - - // NotificationCompat unfortunately doesn't correctly support some features - // such as sounds and heads-up notifications with GROUP_ALERT_CHILDREN on - // Android 6.0 and older. - // This includes: - // Android 6.0 - No Sound or heads-up - // Android 5.0 - Sound, but no heads-up - private static void initGroupAlertBehavior() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) - groupAlertBehavior = NotificationCompat.GROUP_ALERT_CHILDREN; - else - groupAlertBehavior = NotificationCompat.GROUP_ALERT_SUMMARY; - } - - private static void setStatics(Context inContext) { - currentContext = inContext; - packageName = currentContext.getPackageName(); - contextResources = currentContext.getResources(); - } - - @WorkerThread - static boolean displayNotification(OSNotificationGenerationJob notificationJob) { - setStatics(notificationJob.getContext()); - - isRunningOnMainThreadCheck(); - - initGroupAlertBehavior(); - - return showNotification(notificationJob); - } - - static boolean displayIAMPreviewNotification(OSNotificationGenerationJob notificationJob) { - setStatics(notificationJob.getContext()); - - return showNotification(notificationJob); - } - - /** - * For shadow test purpose - */ - static void isRunningOnMainThreadCheck() { - // Runtime check against showing the notification from the main thread - if (OSUtils.isRunningOnMainThread()) - throw new OSThrowable.OSMainThreadException("Process for showing a notification should never been done on Main Thread!"); - } - - private static CharSequence getApplicationLabel() { - ApplicationInfo applicationInfo = ApplicationInfoHelper.Companion.getInfo(currentContext); - if (applicationInfo == null) { - return ""; - } - - return currentContext.getPackageManager().getApplicationLabel(applicationInfo); - } - - private static CharSequence getTitle(JSONObject fcmJson) { - CharSequence title = fcmJson.optString("title", null); - - if (title != null) - return title; - - return getApplicationLabel(); - } - - private static PendingIntent getNewDismissActionPendingIntent(int requestCode, Intent intent) { - return PendingIntent.getBroadcast(currentContext, requestCode, intent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); - } - - private static Intent getNewBaseDismissIntent(int notificationId) { - return new Intent(currentContext, notificationDismissedClass) - .putExtra(BUNDLE_KEY_ANDROID_NOTIFICATION_ID, notificationId) - .putExtra("dismissed", true); - } - - private static OneSignalNotificationBuilder getBaseOneSignalNotificationBuilder(OSNotificationGenerationJob notificationJob) { - JSONObject fcmJson = notificationJob.getJsonPayload(); - OneSignalNotificationBuilder oneSignalNotificationBuilder = new OneSignalNotificationBuilder(); - - NotificationCompat.Builder notificationBuilder; - try { - String channelId = NotificationChannelManager.createNotificationChannel(notificationJob); - // Will throw if app is using 26.0.0-beta1 or older of the support library. - notificationBuilder = new NotificationCompat.Builder(currentContext, channelId); - } catch(Throwable t) { - notificationBuilder = new NotificationCompat.Builder(currentContext); - } - - String message = fcmJson.optString("alert", null); - - notificationBuilder - .setAutoCancel(true) - .setSmallIcon(getSmallIconId(fcmJson)) - .setStyle(new NotificationCompat.BigTextStyle().bigText(message)) - .setContentText(message) - .setTicker(message); - - // If title is blank; Set to app name if less than Android 7. - // Android 7.0 always displays the app title now in it's own section - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N || - !fcmJson.optString("title").equals("")) - notificationBuilder.setContentTitle(getTitle(fcmJson)); - - try { - BigInteger accentColor = getAccentColor(fcmJson); - if (accentColor != null) - notificationBuilder.setColor(accentColor.intValue()); - } catch (Throwable t) {} // Can throw if an old android support lib is used. - - try { - int lockScreenVisibility = NotificationCompat.VISIBILITY_PUBLIC; - if (fcmJson.has("vis")) - lockScreenVisibility = Integer.parseInt(fcmJson.optString("vis")); - notificationBuilder.setVisibility(lockScreenVisibility); - } catch (Throwable t) {} // Can throw if an old android support lib is used or parse error - - Bitmap largeIcon = getLargeIcon(fcmJson); - if (largeIcon != null) { - oneSignalNotificationBuilder.hasLargeIcon = true; - notificationBuilder.setLargeIcon(largeIcon); - } - - Bitmap bigPictureIcon = getBitmap(fcmJson.optString("bicon", null)); - if (bigPictureIcon != null) - notificationBuilder.setStyle(new NotificationCompat.BigPictureStyle().bigPicture(bigPictureIcon).setSummaryText(message)); - - if (notificationJob.getShownTimeStamp() != null) { - try { - notificationBuilder.setWhen(notificationJob.getShownTimeStamp() * 1_000L); - } catch (Throwable t) {} // Can throw if an old android support lib is used. - } - - setAlertnessOptions(fcmJson, notificationBuilder); - - oneSignalNotificationBuilder.compatBuilder = notificationBuilder; - return oneSignalNotificationBuilder; - } - - // Sets visibility options including; Priority, LED, Sounds, and Vibration. - private static void setAlertnessOptions(JSONObject fcmJson, NotificationCompat.Builder notifBuilder) { - int payloadPriority = fcmJson.optInt("pri", 6); - int androidPriority = convertOSToAndroidPriority(payloadPriority); - notifBuilder.setPriority(androidPriority); - boolean lowDisplayPriority = androidPriority < NotificationCompat.PRIORITY_DEFAULT; - - // If notification is a low priority don't set Sound, Vibration, or LED - if (lowDisplayPriority) - return; - - int notificationDefaults = 0; - - if (fcmJson.has("ledc") && fcmJson.optInt("led", 1) == 1) { - try { - BigInteger ledColor = new BigInteger(fcmJson.optString("ledc"), 16); - notifBuilder.setLights(ledColor.intValue(), 2000, 5000); - } catch (Throwable t) { - notificationDefaults |= Notification.DEFAULT_LIGHTS; - } // Can throw if an old android support lib is used or parse error. - } else { - notificationDefaults |= Notification.DEFAULT_LIGHTS; - } - - if (fcmJson.optInt("vib", 1) == 1) { - if (fcmJson.has("vib_pt")) { - long[] vibrationPattern = OSUtils.parseVibrationPattern(fcmJson); - if (vibrationPattern != null) - notifBuilder.setVibrate(vibrationPattern); - } - else - notificationDefaults |= Notification.DEFAULT_VIBRATE; - } - - if (isSoundEnabled(fcmJson)) { - Uri soundUri = OSUtils.getSoundUri(currentContext, fcmJson.optString("sound", null)); - if (soundUri != null) - notifBuilder.setSound(soundUri); - else - notificationDefaults |= Notification.DEFAULT_SOUND; - } - - notifBuilder.setDefaults(notificationDefaults); - } - - private static void removeNotifyOptions(NotificationCompat.Builder builder) { - builder.setOnlyAlertOnce(true) - .setDefaults(0) - .setSound(null) - .setVibrate(null) - .setTicker(null); - } - - // Put the message into a notification and post it. - private static boolean showNotification(OSNotificationGenerationJob notificationJob) { - int notificationId = notificationJob.getAndroidId(); - JSONObject fcmJson = notificationJob.getJsonPayload(); - String group = fcmJson.optString("grp", null); - - IntentGeneratorForAttachingToNotifications intentGenerator = new IntentGeneratorForAttachingToNotifications( - currentContext - ); - - ArrayList grouplessNotifs = new ArrayList<>(); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - /* Android 7.0 auto groups 4 or more notifications so we find these groupless active - * notifications and add a generic group to them */ - grouplessNotifs = OneSignalNotificationManager.getActiveGrouplessNotifications(currentContext); - // If the null this makes the 4th notification and we want to check that 3 or more active groupless exist - if (group == null && grouplessNotifs.size() >= 3) { - group = OneSignalNotificationManager.getGrouplessSummaryKey(); - OneSignalNotificationManager.assignGrouplessNotifications(currentContext, grouplessNotifs); - } - } - - OneSignalNotificationBuilder oneSignalNotificationBuilder = getBaseOneSignalNotificationBuilder(notificationJob); - NotificationCompat.Builder notifBuilder = oneSignalNotificationBuilder.compatBuilder; - - addNotificationActionButtons( - fcmJson, - intentGenerator, - notifBuilder, - notificationId, - null - ); - - try { - addBackgroundImage(fcmJson, notifBuilder); - } catch (Throwable t) { - OneSignal.Log(OneSignal.LOG_LEVEL.ERROR, "Could not set background notification image!", t); - } - - applyNotificationExtender(notificationJob, notifBuilder); - - // Keeps notification from playing sound + vibrating again - if (notificationJob.isRestoring()) - removeNotifyOptions(notifBuilder); - - int makeRoomFor = 1; - if (group != null) - makeRoomFor = 2; - NotificationLimitManager.clearOldestOverLimit(currentContext, makeRoomFor); - - Notification notification; - if (group != null) { - createGenericPendingIntentsForGroup( - notifBuilder, - intentGenerator, - fcmJson, - group, - notificationId - ); - notification = createSingleNotificationBeforeSummaryBuilder(notificationJob, notifBuilder); - - // Create PendingIntents for notifications in a groupless or defined summary - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && - group.equals(OneSignalNotificationManager.getGrouplessSummaryKey())) { - createGrouplessSummaryNotification( - notificationJob, - intentGenerator, - grouplessNotifs.size() + 1 - ); - } - else - createSummaryNotification(notificationJob, oneSignalNotificationBuilder); - } else { - notification = createGenericPendingIntentsForNotif( - notifBuilder, - intentGenerator, - fcmJson, - notificationId - ); - } - // NotificationManagerCompat does not auto omit the individual notification on the device when using - // stacked notifications on Android 4.2 and older - // The benefits of calling notify for individual notifications in-addition to the summary above it is shows - // each notification in a stack on Android Wear and each one is actionable just like the Gmail app does per email. - // Note that on Android 7.0 this is the opposite. Only individual notifications will show and mBundle / group is - // created by Android itself. - if (group == null || Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN_MR1) { - addXiaomiSettings(oneSignalNotificationBuilder, notification); - NotificationManagerCompat.from(currentContext).notify(notificationId, notification); - } - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) - return OneSignalNotificationManager.areNotificationsEnabled(currentContext, notification.getChannelId()); - return true; - } - - private static Notification createGenericPendingIntentsForNotif( - NotificationCompat.Builder notifBuilder, - IntentGeneratorForAttachingToNotifications intentGenerator, - JSONObject gcmBundle, - int notificationId - ) { - Random random = new SecureRandom(); - PendingIntent contentIntent = intentGenerator.getNewActionPendingIntent( - random.nextInt(), - intentGenerator.getNewBaseIntent(notificationId).putExtra(BUNDLE_KEY_ONESIGNAL_DATA, gcmBundle.toString()) - ); - notifBuilder.setContentIntent(contentIntent); - PendingIntent deleteIntent = getNewDismissActionPendingIntent(random.nextInt(), getNewBaseDismissIntent(notificationId)); - notifBuilder.setDeleteIntent(deleteIntent); - return notifBuilder.build(); - } - - private static void createGenericPendingIntentsForGroup( - NotificationCompat.Builder notifBuilder, - IntentGeneratorForAttachingToNotifications intentGenerator, - JSONObject gcmBundle, - String group, - int notificationId - ) { - Random random = new SecureRandom(); - PendingIntent contentIntent = intentGenerator.getNewActionPendingIntent( - random.nextInt(), - intentGenerator.getNewBaseIntent(notificationId).putExtra(BUNDLE_KEY_ONESIGNAL_DATA, gcmBundle.toString()).putExtra("grp", group) - ); - notifBuilder.setContentIntent(contentIntent); - PendingIntent deleteIntent = getNewDismissActionPendingIntent(random.nextInt(), getNewBaseDismissIntent(notificationId).putExtra("grp", group)); - notifBuilder.setDeleteIntent(deleteIntent); - notifBuilder.setGroup(group); - - try { - notifBuilder.setGroupAlertBehavior(groupAlertBehavior); - } catch (Throwable t) { - //do nothing in this case...Android support lib 26 isn't in the project - } - } - - private static void applyNotificationExtender( - OSNotificationGenerationJob notificationJob, - NotificationCompat.Builder notificationBuilder) { - if (!notificationJob.hasExtender()) - return; - - try { - Field mNotificationField = NotificationCompat.Builder.class.getDeclaredField("mNotification"); - mNotificationField.setAccessible(true); - Notification mNotification = (Notification)mNotificationField.get(notificationBuilder); - - notificationJob.setOrgFlags(mNotification.flags); - notificationJob.setOrgSound(mNotification.sound); - notificationBuilder.extend(notificationJob.getNotification().getNotificationExtender()); - - mNotification = (Notification)mNotificationField.get(notificationBuilder); - - Field mContentTextField = NotificationCompat.Builder.class.getDeclaredField("mContentText"); - mContentTextField.setAccessible(true); - CharSequence mContentText = (CharSequence)mContentTextField.get(notificationBuilder); - - Field mContentTitleField = NotificationCompat.Builder.class.getDeclaredField("mContentTitle"); - mContentTitleField.setAccessible(true); - CharSequence mContentTitle = (CharSequence)mContentTitleField.get(notificationBuilder); - - notificationJob.setOverriddenBodyFromExtender(mContentText); - notificationJob.setOverriddenTitleFromExtender(mContentTitle); - if (!notificationJob.isRestoring()) { - notificationJob.setOverriddenFlags(mNotification.flags); - notificationJob.setOverriddenSound(mNotification.sound); - } - } catch (Throwable t) { - t.printStackTrace(); - } - - } - - // Removes custom sound set from the extender from non-summary notification before building it. - // This prevents the sound from playing twice or both the default sound + a custom one. - private static Notification createSingleNotificationBeforeSummaryBuilder(OSNotificationGenerationJob notificationJob, NotificationCompat.Builder notifBuilder) { - // Includes Android 4.3 through 6.0.1. Android 7.1 handles this correctly without this. - // Android 4.2 and older just post the summary only. - boolean singleNotifWorkArounds = Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN_MR1 && Build.VERSION.SDK_INT < Build.VERSION_CODES.N - && !notificationJob.isRestoring(); - - if (singleNotifWorkArounds) { - if (notificationJob.getOverriddenSound() != null && !notificationJob.getOverriddenSound().equals(notificationJob.getOrgSound())) - notifBuilder.setSound(null); - } - - Notification notification = notifBuilder.build(); - - if (singleNotifWorkArounds) - notifBuilder.setSound(notificationJob.getOverriddenSound()); - - return notification; - } - - // Xiaomi requires the following to show a custom notification icons. - // Without this MIUI 8 will only show the app icon on the left. - // When a large icon is set the small icon will no longer show. - private static void addXiaomiSettings(OneSignalNotificationBuilder oneSignalNotificationBuilder, Notification notification) { - // Don't use unless a large icon is set. - // The small white notification icon is hard to see with MIUI default light theme. - if (!oneSignalNotificationBuilder.hasLargeIcon) - return; - - try { - Object miuiNotification = Class.forName("android.app.MiuiNotification").newInstance(); - Field customizedIconField = miuiNotification.getClass().getDeclaredField("customizedIcon"); - customizedIconField.setAccessible(true); - customizedIconField.set(miuiNotification, true); - - Field extraNotificationField = notification.getClass().getField("extraNotification"); - extraNotificationField.setAccessible(true); - extraNotificationField.set(notification, miuiNotification); - } catch (Throwable t) {} // Ignore if not a Xiaomi device - } - - static void updateSummaryNotification(OSNotificationGenerationJob notificationJob) { - setStatics(notificationJob.getContext()); - createSummaryNotification(notificationJob, null); - } - - // This summary notification will be visible instead of the normal one on pre-Android 7.0 devices. - private static void createSummaryNotification(OSNotificationGenerationJob notificationJob, OneSignalNotificationBuilder notifBuilder) { - boolean updateSummary = notificationJob.isRestoring(); - JSONObject fcmJson = notificationJob.getJsonPayload(); - IntentGeneratorForAttachingToNotifications intentGenerator = new IntentGeneratorForAttachingToNotifications( - currentContext - ); - - String group = fcmJson.optString("grp", null); - - SecureRandom random = new SecureRandom(); - PendingIntent summaryDeleteIntent = getNewDismissActionPendingIntent(random.nextInt(), getNewBaseDismissIntent(0).putExtra("summary", group)); - - Notification summaryNotification; - Integer summaryNotificationId = null; - - String firstFullData = null; - Collection summaryList = null; - - OneSignalDbHelper dbHelper = OneSignalDbHelper.getInstance(currentContext); - Cursor cursor = null; - - try { - String[] retColumn = { NotificationTable.COLUMN_NAME_ANDROID_NOTIFICATION_ID, - NotificationTable.COLUMN_NAME_FULL_DATA, - NotificationTable.COLUMN_NAME_IS_SUMMARY, - NotificationTable.COLUMN_NAME_TITLE, - NotificationTable.COLUMN_NAME_MESSAGE }; - - String whereStr = NotificationTable.COLUMN_NAME_GROUP_ID + " = ? AND " + // Where String - NotificationTable.COLUMN_NAME_DISMISSED + " = 0 AND " + - NotificationTable.COLUMN_NAME_OPENED + " = 0"; - String[] whereArgs = { group }; - - // Make sure to omit any old existing matching android ids in-case we are replacing it. - if (!updateSummary) - whereStr += " AND " + NotificationTable.COLUMN_NAME_ANDROID_NOTIFICATION_ID + " <> " + notificationJob.getAndroidId(); - - cursor = dbHelper.query( - NotificationTable.TABLE_NAME, - retColumn, - whereStr, - whereArgs, - null, // group by - null, // filter by row groups - NotificationTable._ID + " DESC" // sort order, new to old - ); - - if (cursor.moveToFirst()) { - SpannableString spannableString; - summaryList = new ArrayList<>(); - - do { - if (cursor.getInt(cursor.getColumnIndex(NotificationTable.COLUMN_NAME_IS_SUMMARY)) == 1) - summaryNotificationId = cursor.getInt(cursor.getColumnIndex(NotificationTable.COLUMN_NAME_ANDROID_NOTIFICATION_ID)); - else { - String title = cursor.getString(cursor.getColumnIndex(NotificationTable.COLUMN_NAME_TITLE)); - if (title == null) - title = ""; - else - title += " "; - - String msg = cursor.getString(cursor.getColumnIndex(NotificationTable.COLUMN_NAME_MESSAGE)); - - spannableString = new SpannableString(title + msg); - if (title.length() > 0) - spannableString.setSpan(new StyleSpan(android.graphics.Typeface.BOLD), 0, title.length(), 0); - summaryList.add(spannableString); - - if (firstFullData == null) - firstFullData = cursor.getString(cursor.getColumnIndex(NotificationTable.COLUMN_NAME_FULL_DATA)); - } - } while (cursor.moveToNext()); - - if (updateSummary && firstFullData != null) { - try { - fcmJson = new JSONObject(firstFullData); - } catch (JSONException e) { - e.printStackTrace(); - } - } - } - } - finally { - if (cursor != null && !cursor.isClosed()) - cursor.close(); - } - - if (summaryNotificationId == null) { - summaryNotificationId = random.nextInt(); - createSummaryIdDatabaseEntry(dbHelper, group, summaryNotificationId); - } - - PendingIntent summaryContentIntent = intentGenerator.getNewActionPendingIntent( - random.nextInt(), - createBaseSummaryIntent(summaryNotificationId, intentGenerator, fcmJson, group) - ); - - // 2 or more notifications with a group received, group them together as a single notification. - if (summaryList != null && - ((updateSummary && summaryList.size() > 1) || - (!updateSummary && summaryList.size() > 0))) { - int notificationCount = summaryList.size() + (updateSummary ? 0 : 1); - - String summaryMessage = fcmJson.optString("grp_msg", null); - if (summaryMessage == null) - summaryMessage = notificationCount + " new messages"; - else - summaryMessage = summaryMessage.replace("$[notif_count]", "" + notificationCount); - - NotificationCompat.Builder summaryBuilder = getBaseOneSignalNotificationBuilder(notificationJob).compatBuilder; - if (updateSummary) - removeNotifyOptions(summaryBuilder); - else { - if (notificationJob.getOverriddenSound() != null) - summaryBuilder.setSound(notificationJob.getOverriddenSound()); - - if (notificationJob.getOverriddenFlags() != null) - summaryBuilder.setDefaults(notificationJob.getOverriddenFlags()); - } - - // The summary is designed to fit all notifications. - // Default small and large icons are used instead of the payload options to enforce this. - summaryBuilder.setContentIntent(summaryContentIntent) - .setDeleteIntent(summaryDeleteIntent) - .setContentTitle(getApplicationLabel()) - .setContentText(summaryMessage) - .setNumber(notificationCount) - .setSmallIcon(getDefaultSmallIconId()) - .setLargeIcon(getDefaultLargeIcon()) - .setOnlyAlertOnce(updateSummary) - .setAutoCancel(false) - .setGroup(group) - .setGroupSummary(true); - - try { - summaryBuilder.setGroupAlertBehavior(groupAlertBehavior); - } - catch (Throwable t) { - //do nothing in this case...Android support lib 26 isn't in the project - } - - if (!updateSummary) - summaryBuilder.setTicker(summaryMessage); - - NotificationCompat.InboxStyle inboxStyle = new NotificationCompat.InboxStyle(); - - // Add the latest notification to the summary - if (!updateSummary) { - String line1Title = null; - - if (notificationJob.getTitle() != null) - line1Title = notificationJob.getTitle().toString(); - - if (line1Title == null) - line1Title = ""; - else - line1Title += " "; - - String message = notificationJob.getBody().toString(); - - SpannableString spannableString = new SpannableString(line1Title + message); - if (line1Title.length() > 0) - spannableString.setSpan(new StyleSpan(android.graphics.Typeface.BOLD), 0, line1Title.length(), 0); - inboxStyle.addLine(spannableString); - } - - for (SpannableString line : summaryList) - inboxStyle.addLine(line); - inboxStyle.setBigContentTitle(summaryMessage); - summaryBuilder.setStyle(inboxStyle); - - summaryNotification = summaryBuilder.build(); - } - else { - // First notification with this group key, post like a normal notification. - NotificationCompat.Builder summaryBuilder = notifBuilder.compatBuilder; - - // TODO: We are re-using the notifBuilder from the normal notification so if a developer as an - // extender setup all the settings will carry over. - // Note: However their buttons will not carry over as we need to be setup with this new summaryNotificationId. - summaryBuilder.mActions.clear(); - addNotificationActionButtons( - fcmJson, - intentGenerator, - summaryBuilder, - summaryNotificationId, - group - ); - - summaryBuilder.setContentIntent(summaryContentIntent) - .setDeleteIntent(summaryDeleteIntent) - .setOnlyAlertOnce(updateSummary) - .setAutoCancel(false) - .setGroup(group) - .setGroupSummary(true); - - try { - summaryBuilder.setGroupAlertBehavior(groupAlertBehavior); - } - catch (Throwable t) { - //do nothing in this case...Android support lib 26 isn't in the project - } - - summaryNotification = summaryBuilder.build(); - addXiaomiSettings(notifBuilder, summaryNotification); - } - - NotificationManagerCompat.from(currentContext).notify(summaryNotificationId, summaryNotification); - } - - @RequiresApi(api = Build.VERSION_CODES.M) - private static void createGrouplessSummaryNotification( - OSNotificationGenerationJob notificationJob, - IntentGeneratorForAttachingToNotifications intentGenerator, - int grouplessNotifCount - ) { - JSONObject fcmJson = notificationJob.getJsonPayload(); - - Notification summaryNotification; - - SecureRandom random = new SecureRandom(); - String group = OneSignalNotificationManager.getGrouplessSummaryKey(); - String summaryMessage = grouplessNotifCount + " new messages"; - int summaryNotificationId = OneSignalNotificationManager.getGrouplessSummaryId(); - OneSignalDbHelper dbHelper = OneSignalDbHelper.getInstance(currentContext); - createSummaryIdDatabaseEntry(dbHelper, group, summaryNotificationId); - - PendingIntent summaryContentIntent = intentGenerator.getNewActionPendingIntent( - random.nextInt(), - createBaseSummaryIntent(summaryNotificationId,intentGenerator, fcmJson, group) - ); - PendingIntent summaryDeleteIntent = getNewDismissActionPendingIntent(random.nextInt(), getNewBaseDismissIntent(0).putExtra("summary", group)); - - NotificationCompat.Builder summaryBuilder = getBaseOneSignalNotificationBuilder(notificationJob).compatBuilder; - if (notificationJob.getOverriddenSound() != null) - summaryBuilder.setSound(notificationJob.getOverriddenSound()); - - if (notificationJob.getOverriddenFlags() != null) - summaryBuilder.setDefaults(notificationJob.getOverriddenFlags()); - - // The summary is designed to fit all notifications. - // Default small and large icons are used instead of the payload options to enforce this. - summaryBuilder.setContentIntent(summaryContentIntent) - .setDeleteIntent(summaryDeleteIntent) - .setContentTitle(getApplicationLabel()) - .setContentText(summaryMessage) - .setNumber(grouplessNotifCount) - .setSmallIcon(getDefaultSmallIconId()) - .setLargeIcon(getDefaultLargeIcon()) - .setOnlyAlertOnce(true) - .setAutoCancel(false) - .setGroup(group) - .setGroupSummary(true); - - try { - summaryBuilder.setGroupAlertBehavior(groupAlertBehavior); - } - catch (Throwable t) { - // Do nothing in this case... Android support lib 26 isn't in the project - } - - NotificationCompat.InboxStyle inboxStyle = new NotificationCompat.InboxStyle(); - - inboxStyle.setBigContentTitle(summaryMessage); - summaryBuilder.setStyle(inboxStyle); - summaryNotification = summaryBuilder.build(); - - NotificationManagerCompat.from(currentContext).notify(summaryNotificationId, summaryNotification); - } - - private static Intent createBaseSummaryIntent( - int summaryNotificationId, - IntentGeneratorForAttachingToNotifications intentGenerator, - JSONObject fcmJson, - String group - ) { - return intentGenerator.getNewBaseIntent(summaryNotificationId).putExtra(BUNDLE_KEY_ONESIGNAL_DATA, fcmJson.toString()).putExtra("summary", group); - } - - private static void createSummaryIdDatabaseEntry(OneSignalDbHelper dbHelper, String group, int id) { - // There currently isn't a visible notification from for this group_id. - // Save the group summary notification id so it can be updated later. - ContentValues values = new ContentValues(); - values.put(NotificationTable.COLUMN_NAME_ANDROID_NOTIFICATION_ID, id); - values.put(NotificationTable.COLUMN_NAME_GROUP_ID, group); - values.put(NotificationTable.COLUMN_NAME_IS_SUMMARY, 1); - dbHelper.insertOrThrow(NotificationTable.TABLE_NAME, null, values); - } - - // Keep 'throws Throwable' as 'onesignal_bgimage_notif_layout' may not be available - // This maybe the case if a jar is used instead of an aar. - private static void addBackgroundImage(JSONObject fcmJson, NotificationCompat.Builder notifBuilder) throws Throwable { - // Not adding Background Images to API Versions < 16 or >= 31 - if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN || - android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - OneSignal.Log(OneSignal.LOG_LEVEL.VERBOSE, - "Cannot use background images in notifications for device on version: " + android.os.Build.VERSION.SDK_INT); - return; - } - - Bitmap bg_image = null; - JSONObject jsonBgImage = null; - String jsonStrBgImage = fcmJson.optString("bg_img", null); - - if (jsonStrBgImage != null) { - jsonBgImage = new JSONObject(jsonStrBgImage); - bg_image = getBitmap(jsonBgImage.optString("img", null)); - } - - if (bg_image == null) - bg_image = getBitmapFromAssetsOrResourceName("onesignal_bgimage_default_image"); - - if (bg_image != null) { - RemoteViews customView = new RemoteViews(currentContext.getPackageName(), R.layout.onesignal_bgimage_notif_layout); - customView.setTextViewText(R.id.os_bgimage_notif_title, getTitle(fcmJson)); - customView.setTextViewText(R.id.os_bgimage_notif_body, fcmJson.optString("alert")); - setTextColor(customView, jsonBgImage, R.id.os_bgimage_notif_title, "tc", "onesignal_bgimage_notif_title_color"); - setTextColor(customView, jsonBgImage, R.id.os_bgimage_notif_body, "bc", "onesignal_bgimage_notif_body_color"); - - String alignSetting = null; - if (jsonBgImage != null && jsonBgImage.has("img_align")) - alignSetting = jsonBgImage.getString("img_align"); - else { - int iAlignSetting = contextResources.getIdentifier("onesignal_bgimage_notif_image_align", "string", packageName); - if (iAlignSetting != 0) - alignSetting = contextResources.getString(iAlignSetting); - } - - if ("right".equals(alignSetting)) { - // Use os_bgimage_notif_bgimage_right_aligned instead of os_bgimage_notif_bgimage - // which has scaleType="fitEnd" set. - // This is required as setScaleType can not be called through RemoteViews as it is an enum. - customView.setViewPadding(R.id.os_bgimage_notif_bgimage_align_layout, -5000, 0, 0, 0); - customView.setImageViewBitmap(R.id.os_bgimage_notif_bgimage_right_aligned, bg_image); - customView.setViewVisibility(R.id.os_bgimage_notif_bgimage_right_aligned, 0); // visible - customView.setViewVisibility(R.id.os_bgimage_notif_bgimage, 2); // gone - } - else - customView.setImageViewBitmap(R.id.os_bgimage_notif_bgimage, bg_image); - - notifBuilder.setContent(customView); - - // Remove style to prevent expanding by the user. - // Future: Create an extended image option, will need its own layout. - notifBuilder.setStyle(null); - } - } - - private static void setTextColor(RemoteViews customView, JSONObject fcmJson, int viewId, String colorPayloadKey, String colorDefaultResource) { - Integer color = safeGetColorFromHex(fcmJson, colorPayloadKey); - if (color != null) - customView.setTextColor(viewId, color); - else { - int colorId = contextResources.getIdentifier(colorDefaultResource, "color", packageName); - if (colorId != 0) - customView.setTextColor(viewId, AndroidSupportV4Compat.ContextCompat.getColor(currentContext, colorId)); - } - } - - private static Integer safeGetColorFromHex(JSONObject fcmJson, String colorKey) { - try { - if (fcmJson != null && fcmJson.has(colorKey)) { - return new BigInteger(fcmJson.optString(colorKey), 16).intValue(); - } - } catch (Throwable t) {} - return null; - } - - private static Bitmap getLargeIcon(JSONObject fcmJson) { - Bitmap bitmap = getBitmap(fcmJson.optString("licon")); - if (bitmap == null) - bitmap = getBitmapFromAssetsOrResourceName("ic_onesignal_large_icon_default"); - - if (bitmap == null) - return null; - - return resizeBitmapForLargeIconArea(bitmap); - } - - private static Bitmap getDefaultLargeIcon() { - Bitmap bitmap = getBitmapFromAssetsOrResourceName("ic_onesignal_large_icon_default"); - return resizeBitmapForLargeIconArea(bitmap); - } - - // Resize to prevent extra cropping and boarders. - private static Bitmap resizeBitmapForLargeIconArea(Bitmap bitmap) { - if (bitmap == null) - return null; - - try { - int systemLargeIconHeight = (int) contextResources.getDimension(android.R.dimen.notification_large_icon_height); - int systemLargeIconWidth = (int) contextResources.getDimension(android.R.dimen.notification_large_icon_width); - int bitmapHeight = bitmap.getHeight(); - int bitmapWidth = bitmap.getWidth(); - - if (bitmapWidth > systemLargeIconWidth || bitmapHeight > systemLargeIconHeight) { - int newWidth = systemLargeIconWidth, newHeight = systemLargeIconHeight; - if (bitmapHeight > bitmapWidth) { - float ratio = (float) bitmapWidth / (float) bitmapHeight; - newWidth = (int) (newHeight * ratio); - } else if (bitmapWidth > bitmapHeight) { - float ratio = (float) bitmapHeight / (float) bitmapWidth; - newHeight = (int) (newWidth * ratio); - } - - return Bitmap.createScaledBitmap(bitmap, newWidth, newHeight, true); - } - } catch (Throwable t) {} - - return bitmap; - } - - private static Bitmap getBitmapFromAssetsOrResourceName(String bitmapStr) { - try { - Bitmap bitmap = null; - - try { - bitmap = BitmapFactory.decodeStream(currentContext.getAssets().open(bitmapStr)); - } catch (Throwable t) {} - - if (bitmap != null) - return bitmap; - - final List image_extensions = Arrays.asList(".png", ".webp", ".jpg", ".gif", ".bmp"); - for (String extension : image_extensions) { - try { - bitmap = BitmapFactory.decodeStream(currentContext.getAssets().open(bitmapStr + extension)); - } catch (Throwable t) {} - if (bitmap != null) - return bitmap; - } - - int bitmapId = getResourceIcon(bitmapStr); - if (bitmapId != 0) - return BitmapFactory.decodeResource(contextResources, bitmapId); - } catch (Throwable t) {} - - return null; - } - - private static Bitmap getBitmapFromURL(String location) { - try { - return BitmapFactory.decodeStream(new URL(location).openConnection().getInputStream()); - } catch (Throwable t) { - OneSignal.Log(OneSignal.LOG_LEVEL.WARN, "Could not download image!", t); - } - - return null; - } - - private static Bitmap getBitmap(String name) { - if (name == null) - return null; - String trimmedName = name.trim(); - - if (trimmedName.startsWith("http://") || trimmedName.startsWith("https://")) - return getBitmapFromURL(trimmedName); - - return getBitmapFromAssetsOrResourceName(name); - } - - private static int getResourceIcon(String iconName) { - if (iconName == null) - return 0; - - String trimmedIconName = iconName.trim(); - if (!OSUtils.isValidResourceName(trimmedIconName)) - return 0; - - int notificationIcon = getDrawableId(trimmedIconName); - if (notificationIcon != 0) - return notificationIcon; - - // Get system icon resource - try { - return drawable.class.getField(iconName).getInt(null); - } catch (Throwable t) {} - - return 0; - } - - private static int getSmallIconId(JSONObject fcmJson) { - int notificationIcon = getResourceIcon(fcmJson.optString("sicon", null)); - if (notificationIcon != 0) - return notificationIcon; - - return getDefaultSmallIconId(); - } - - private static int getDefaultSmallIconId() { - int notificationIcon = getDrawableId("ic_stat_onesignal_default"); - if (notificationIcon != 0) - return notificationIcon; - - notificationIcon = getDrawableId("corona_statusbar_icon_default"); - if (notificationIcon != 0) - return notificationIcon; - - notificationIcon = getDrawableId("ic_os_notification_fallback_white_24dp"); - if (notificationIcon != 0) - return notificationIcon; - - return drawable.ic_popup_reminder; - } - - private static int getDrawableId(String name) { - return contextResources.getIdentifier(name, "drawable", packageName); - } - - private static boolean isSoundEnabled(JSONObject fcmJson) { - String sound = fcmJson.optString("sound", null); - return !"null".equals(sound) && !"nil".equals(sound); - } - - // Android 5.0 accent color to use, only works when AndroidManifest.xml is targetSdkVersion >= 21 - static BigInteger getAccentColor(JSONObject fcmJson) { - try { - if (fcmJson.has("bgac")) - return new BigInteger(fcmJson.optString("bgac", null), 16); - } catch (Throwable t) {} // Can throw a parse error. - - // Try to get "onesignal_notification_accent_color" from resources - // This will get the correct color for day and dark modes - try { - String defaultColor = getResourceString(OneSignal.appContext, "onesignal_notification_accent_color", null); - if (defaultColor != null) { - return new BigInteger(defaultColor, 16); - } - } catch (Throwable t) {} // Can throw a parse error. - - // Get accent color from Manifest - try { - String defaultColor = OSUtils.getManifestMeta(OneSignal.appContext, "com.onesignal.NotificationAccentColor.DEFAULT"); - if (defaultColor != null) { - return new BigInteger(defaultColor, 16); - } - } catch (Throwable t) {} // Can throw a parse error. - - return null; - } - - private static void addNotificationActionButtons( - JSONObject fcmJson, - IntentGeneratorForAttachingToNotifications intentGenerator, - NotificationCompat.Builder mBuilder, - int notificationId, - String groupSummary - ) { - try { - JSONObject customJson = new JSONObject(fcmJson.optString("custom")); - - if (!customJson.has("a")) - return; - - JSONObject additionalDataJSON = customJson.getJSONObject("a"); - if (!additionalDataJSON.has("actionButtons")) - return; - - JSONArray buttons = additionalDataJSON.getJSONArray("actionButtons"); - - for (int i = 0; i < buttons.length(); i++) { - JSONObject button = buttons.optJSONObject(i); - JSONObject bundle = new JSONObject(fcmJson.toString()); - - Intent buttonIntent = intentGenerator.getNewBaseIntent(notificationId); - buttonIntent.setAction("" + i); // Required to keep each action button from replacing extras of each other - buttonIntent.putExtra("action_button", true); - bundle.put(BUNDLE_KEY_ACTION_ID, button.optString("id")); - buttonIntent.putExtra(BUNDLE_KEY_ONESIGNAL_DATA, bundle.toString()); - if (groupSummary != null) - buttonIntent.putExtra("summary", groupSummary); - else if (fcmJson.has("grp")) - buttonIntent.putExtra("grp", fcmJson.optString("grp")); - - PendingIntent buttonPIntent = intentGenerator.getNewActionPendingIntent(notificationId, buttonIntent); - - int buttonIcon = 0; - if (button.has("icon")) - buttonIcon = getResourceIcon(button.optString("icon")); - - mBuilder.addAction(buttonIcon, button.optString("text"), buttonPIntent); - } - } catch (Throwable t) { - t.printStackTrace(); - } - } - - private static void addAlertButtons(Context context, JSONObject fcmJson, List buttonsLabels, List buttonsIds) { - try { - addCustomAlertButtons(fcmJson, buttonsLabels, buttonsIds); - } catch (Throwable t) { - OneSignal.Log(OneSignal.LOG_LEVEL.ERROR, "Failed to parse JSON for custom buttons for alert dialog.", t); - } - - if (buttonsLabels.size() == 0 || buttonsLabels.size() < 3) { - buttonsLabels.add(getResourceString(context, "onesignal_in_app_alert_ok_button_text", "Ok")); - buttonsIds.add(NotificationBundleProcessor.DEFAULT_ACTION); - } - } - - private static void addCustomAlertButtons(JSONObject fcmJson, List buttonsLabels, List buttonsIds) throws JSONException { - JSONObject customJson = new JSONObject(fcmJson.optString("custom")); - - if (!customJson.has("a")) - return; - - JSONObject additionalDataJSON = customJson.getJSONObject("a"); - if (!additionalDataJSON.has("actionButtons")) - return; - - JSONArray buttons = additionalDataJSON.optJSONArray("actionButtons"); - - for (int i = 0; i < buttons.length(); i++) { - JSONObject button = buttons.getJSONObject(i); - - buttonsLabels.add(button.optString("text")); - buttonsIds.add(button.optString("id")); - } - } - - private static int convertOSToAndroidPriority(int priority) { - if (priority > 9) - return NotificationCompat.PRIORITY_MAX; - if (priority > 7) - return NotificationCompat.PRIORITY_HIGH; - if (priority > 4) - return NotificationCompat.PRIORITY_DEFAULT; - if (priority > 2) - return NotificationCompat.PRIORITY_LOW; - - return NotificationCompat.PRIORITY_MIN; - } -} diff --git a/OneSignalSDK/onesignal/src/main/java/com/onesignal/GooglePlayServicesUpgradePrompt.java b/OneSignalSDK/onesignal/src/main/java/com/onesignal/GooglePlayServicesUpgradePrompt.java deleted file mode 100644 index 4be0e57cc7..0000000000 --- a/OneSignalSDK/onesignal/src/main/java/com/onesignal/GooglePlayServicesUpgradePrompt.java +++ /dev/null @@ -1,100 +0,0 @@ -package com.onesignal; - -import android.app.Activity; -import android.app.AlertDialog; -import android.app.PendingIntent; -import android.content.DialogInterface; -import android.content.pm.PackageManager; - -import com.google.android.gms.common.GoogleApiAvailability; - -import static com.onesignal.OSUtils.getResourceString; - -class GooglePlayServicesUpgradePrompt { - private static final int PLAY_SERVICES_RESOLUTION_REQUEST = 9_000; - private static boolean isGooglePlayStoreInstalled() { - GetPackageInfoResult result = - PackageInfoHelper.Companion.getInfo( - OneSignal.appContext, - GoogleApiAvailability.GOOGLE_PLAY_SERVICES_PACKAGE, - PackageManager.GET_META_DATA - ); - if (!result.getSuccessful()) { - return false; - } - if (result.getPackageInfo() == null) { - return false; - } - - PackageManager pm = OneSignal.appContext.getPackageManager(); - String label = (String) result.getPackageInfo().applicationInfo.loadLabel(pm); - return !label.equals("Market"); - } - - static void showUpdateGPSDialog() { - if (!OSUtils.isAndroidDeviceType()) - return; - - if (!isGooglePlayStoreInstalled() || OneSignal.getDisableGMSMissingPrompt()) - return; - - boolean userSelectedSkip = - OneSignalPrefs.getBool( - OneSignalPrefs.PREFS_ONESIGNAL, - OneSignalPrefs.PREFS_GT_DO_NOT_SHOW_MISSING_GPS, - false - ); - if (userSelectedSkip) - return; - - OSUtils.runOnMainUIThread(new Runnable() { - @Override - public void run() { - final Activity activity = OneSignal.getCurrentActivity(); - if (activity == null) - return; - - // Load resource strings so a developer can customize this dialog - String alertBodyText = getResourceString(activity, "onesignal_gms_missing_alert_text", "To receive push notifications please press 'Update' to enable 'Google Play services'."); - String alertButtonUpdate = getResourceString(activity, "onesignal_gms_missing_alert_button_update", "Update"); - String alertButtonSkip = getResourceString(activity, "onesignal_gms_missing_alert_button_skip", "Skip"); - String alertButtonClose = getResourceString(activity, "onesignal_gms_missing_alert_button_close", "Close"); - - AlertDialog.Builder builder = new AlertDialog.Builder(activity); - builder.setMessage(alertBodyText).setPositiveButton(alertButtonUpdate, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - OpenPlayStoreToApp(activity); - } - }).setNegativeButton(alertButtonSkip, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - OneSignalPrefs.saveBool(OneSignalPrefs.PREFS_ONESIGNAL, - OneSignalPrefs.PREFS_GT_DO_NOT_SHOW_MISSING_GPS,true); - - } - }).setNeutralButton(alertButtonClose, null).create().show(); - } - }); - } - - // Take the user to the Google Play store to update or enable the Google Play Services app - private static void OpenPlayStoreToApp(Activity activity) { - try { - GoogleApiAvailability apiAvailability = GoogleApiAvailability.getInstance(); - int resultCode = apiAvailability.isGooglePlayServicesAvailable(OneSignal.appContext); - // Send the Intent to trigger opening the store - PendingIntent pendingIntent = - apiAvailability.getErrorResolutionPendingIntent( - activity, - resultCode, - PLAY_SERVICES_RESOLUTION_REQUEST - ); - if (pendingIntent != null) - pendingIntent.send(); - } catch (PendingIntent.CanceledException e) { - e.printStackTrace(); - } - } - -} diff --git a/OneSignalSDK/onesignal/src/main/java/com/onesignal/LocationController.java b/OneSignalSDK/onesignal/src/main/java/com/onesignal/LocationController.java deleted file mode 100644 index c8a6436837..0000000000 --- a/OneSignalSDK/onesignal/src/main/java/com/onesignal/LocationController.java +++ /dev/null @@ -1,425 +0,0 @@ -/** - * Modified MIT License - * - * Copyright 2018 OneSignal - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * 1. The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * 2. All copies of substantial portions of the Software may only be used in connection - * with services provided by OneSignal. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ - -package com.onesignal; - -import android.content.Context; -import android.content.pm.PackageManager; -import android.location.Location; -import android.os.Build; -import android.os.Handler; -import android.os.HandlerThread; - -import com.onesignal.AndroidSupportV4Compat.ContextCompat; - -import java.math.BigDecimal; -import java.math.RoundingMode; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.concurrent.ConcurrentHashMap; - -class LocationController { - - private static final long TIME_FOREGROUND_SEC = 5 * 60; - private static final long TIME_BACKGROUND_SEC = 10 * 60; - static final long FOREGROUND_UPDATE_TIME_MS = (TIME_FOREGROUND_SEC - 30) * 1_000; - static final long BACKGROUND_UPDATE_TIME_MS = (TIME_BACKGROUND_SEC - 30) * 1_000; - - private static final List promptHandlers = new ArrayList<>(); - private static ConcurrentHashMap locationHandlers = new ConcurrentHashMap<>(); - private static boolean locationCoarse; - - static final Object syncLock = new Object() {}; - - private static LocationHandlerThread locationHandlerThread; - static LocationHandlerThread getLocationHandlerThread() { - if (locationHandlerThread == null) { - synchronized (syncLock) { - if (locationHandlerThread == null) - locationHandlerThread = new LocationHandlerThread(); - } - } - return locationHandlerThread; - } - - static Thread fallbackFailThread; - static Context classContext; - static Location lastLocation; - - static String requestPermission; - - enum PermissionType { - STARTUP, PROMPT_LOCATION, SYNC_SERVICE; - } - - static class LocationPoint { - Double lat; - Double log; - Float accuracy; - Integer type; - Boolean bg; - Long timeStamp; - - @Override - public String toString() { - return "LocationPoint{" + - "lat=" + lat + - ", log=" + log + - ", accuracy=" + accuracy + - ", type=" + type + - ", bg=" + bg + - ", timeStamp=" + timeStamp + - '}'; - } - } - - interface LocationHandler { - PermissionType getType(); - void onComplete(LocationPoint point); - } - - abstract static class LocationPromptCompletionHandler implements LocationHandler { - void onAnswered(OneSignal.PromptActionResult result) {} - } - - static boolean scheduleUpdate(Context context) { - if (!hasLocationPermission(context)) { - OneSignal.onesignalLog(OneSignal.LOG_LEVEL.DEBUG, "LocationController scheduleUpdate not possible, location permission not enabled"); - return false; - } - if (!OneSignal.isLocationShared()) { - OneSignal.onesignalLog(OneSignal.LOG_LEVEL.DEBUG, "LocationController scheduleUpdate not possible, location shared not enabled"); - return false; - } - long lastTime = OneSignal.getTime().getCurrentTimeMillis() - getLastLocationTime(); - long minTime = 1_000 * (OneSignal.isInForeground() ? TIME_FOREGROUND_SEC : TIME_BACKGROUND_SEC); - - OneSignal.onesignalLog(OneSignal.LOG_LEVEL.DEBUG, "LocationController scheduleUpdate lastTime: " + lastTime + " minTime: " + minTime); - long scheduleTime = minTime - lastTime; - - OSSyncService.getInstance().scheduleLocationUpdateTask(context, scheduleTime); - return true; - } - - private static void setLastLocationTime(long time) { - OneSignalPrefs.saveLong(OneSignalPrefs.PREFS_ONESIGNAL, - OneSignalPrefs.PREFS_OS_LAST_LOCATION_TIME, time); - } - - private static long getLastLocationTime() { - return OneSignalPrefs.getLong( - OneSignalPrefs.PREFS_ONESIGNAL, - OneSignalPrefs.PREFS_OS_LAST_LOCATION_TIME, - TIME_BACKGROUND_SEC * -1_000 - ); - } - - private static boolean hasLocationPermission(Context context) { - return ContextCompat.checkSelfPermission(context, "android.permission.ACCESS_FINE_LOCATION") == PackageManager.PERMISSION_GRANTED - || ContextCompat.checkSelfPermission(context, "android.permission.ACCESS_COARSE_LOCATION") == PackageManager.PERMISSION_GRANTED; - } - - private static void addPromptHandlerIfAvailable(LocationHandler handler) { - if (handler instanceof LocationPromptCompletionHandler) { - synchronized (promptHandlers) { - promptHandlers.add((LocationPromptCompletionHandler) handler); - } - } - } - - static void sendAndClearPromptHandlers(boolean promptLocation, OneSignal.PromptActionResult result) { - if (!promptLocation) { - OneSignal.onesignalLog(OneSignal.LOG_LEVEL.DEBUG, "LocationController sendAndClearPromptHandlers from non prompt flow"); - return; - } - - synchronized (promptHandlers) { - OneSignal.onesignalLog(OneSignal.LOG_LEVEL.DEBUG, "LocationController calling prompt handlers"); - for (LocationPromptCompletionHandler promptHandler : promptHandlers) { - promptHandler.onAnswered(result); - } - // We only call the prompt handlers once - promptHandlers.clear(); - } - } - - /** - * This method handle location and permission location flows and border cases. - * For each flow we need to trigger location prompts listener, - * in that way all listener will now that location request completed, even if its showing a prompt - * - * Cases managed: - * - If app doesn't have location sharing activated, then location will not attributed - * - For API less than 23, prompt permission aren't needed - * - For API greater or equal than 23 - * - Ask for permission if needed, this will prompt PermissionActivity - * - If permission granted, then trigger location attribution - * - If permission denied, then trigger fail flow - * - If location service is disable, then trigger fail flow - * - If the user approved for location and has disable location this will continue triggering fails flows - * - * For all cases we are calling prompt listeners. - */ - static void getLocation(Context context, boolean promptLocation, boolean fallbackToSettings, LocationHandler handler) { - addPromptHandlerIfAvailable(handler); - classContext = context; - locationHandlers.put(handler.getType(), handler); - - if (!OneSignal.isLocationShared()) { - sendAndClearPromptHandlers(promptLocation, OneSignal.PromptActionResult.ERROR); - fireFailedComplete(); - return; - } - - int locationBackgroundPermission = PackageManager.PERMISSION_DENIED; - int locationCoarsePermission = PackageManager.PERMISSION_DENIED; - - int locationFinePermission = ContextCompat.checkSelfPermission(context, "android.permission.ACCESS_FINE_LOCATION"); - if (locationFinePermission == PackageManager.PERMISSION_DENIED) { - locationCoarsePermission = ContextCompat.checkSelfPermission(context, "android.permission.ACCESS_COARSE_LOCATION"); - locationCoarse = true; - } - if (Build.VERSION.SDK_INT >= 29) - locationBackgroundPermission = ContextCompat.checkSelfPermission(context, "android.permission.ACCESS_BACKGROUND_LOCATION"); - - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { - if (locationFinePermission != PackageManager.PERMISSION_GRANTED && locationCoarsePermission != PackageManager.PERMISSION_GRANTED) { - // Permission missing on manifest - sendAndClearPromptHandlers(promptLocation, OneSignal.PromptActionResult.LOCATION_PERMISSIONS_MISSING_MANIFEST); - - handler.onComplete(null); - return; - } - sendAndClearPromptHandlers(promptLocation, OneSignal.PromptActionResult.PERMISSION_GRANTED); - startGetLocation(); - } else { // Android 6.0+ - if (locationFinePermission != PackageManager.PERMISSION_GRANTED) { - GetPackageInfoResult packageResult = - PackageInfoHelper.Companion.getInfo( - context, - context.getPackageName(), - PackageManager.GET_PERMISSIONS - ); - - if (!packageResult.getSuccessful() || packageResult.getPackageInfo() == null) { - sendAndClearPromptHandlers(promptLocation, OneSignal.PromptActionResult.ERROR); - return; - } - - List permissionList = Arrays.asList(packageResult.getPackageInfo().requestedPermissions); - OneSignal.PromptActionResult result = OneSignal.PromptActionResult.PERMISSION_DENIED; - - if (permissionList.contains("android.permission.ACCESS_FINE_LOCATION")) { - // ACCESS_FINE_LOCATION permission defined on Manifest, prompt for permission - // If permission already given prompt will return positive, otherwise will prompt again or show settings - requestPermission = "android.permission.ACCESS_FINE_LOCATION"; - } else if (permissionList.contains("android.permission.ACCESS_COARSE_LOCATION")) { - if (locationCoarsePermission != PackageManager.PERMISSION_GRANTED) { - // ACCESS_COARSE_LOCATION permission defined on Manifest, prompt for permission - // If permission already given prompt will return positive, otherwise will prompt again or show settings - requestPermission = "android.permission.ACCESS_COARSE_LOCATION"; - } else if (Build.VERSION.SDK_INT >= 29 && permissionList.contains("android.permission.ACCESS_BACKGROUND_LOCATION")) { - // ACCESS_BACKGROUND_LOCATION permission defined on Manifest, prompt for permission - requestPermission = "android.permission.ACCESS_BACKGROUND_LOCATION"; - } - } else { - OneSignal.onesignalLog(OneSignal.LOG_LEVEL.INFO, "Location permissions not added on AndroidManifest file"); - result = OneSignal.PromptActionResult.LOCATION_PERMISSIONS_MISSING_MANIFEST; - } - - // We handle the following cases: - // 1 - If needed and available then prompt for permissions - // - Request permission can be ACCESS_COARSE_LOCATION or ACCESS_FINE_LOCATION - // 2 - If the permission were already granted then start getting location - // 3 - If permission wasn't granted then trigger fail flow - // - // For each case, we call the prompt handlers - if (requestPermission != null && promptLocation) { - LocationPermissionController.INSTANCE.prompt(fallbackToSettings, requestPermission); - } else if (locationCoarsePermission == PackageManager.PERMISSION_GRANTED) { - sendAndClearPromptHandlers(promptLocation, OneSignal.PromptActionResult.PERMISSION_GRANTED); - startGetLocation(); - } else { - sendAndClearPromptHandlers(promptLocation, result); - fireFailedComplete(); - } - } else if (Build.VERSION.SDK_INT >= 29 && locationBackgroundPermission != PackageManager.PERMISSION_GRANTED) { - backgroundLocationPermissionLogic(context, promptLocation, fallbackToSettings); - } else { - sendAndClearPromptHandlers(promptLocation, OneSignal.PromptActionResult.PERMISSION_GRANTED); - startGetLocation(); - } - } - } - - /** - * On Android 10 background location permission is needed - * On Android 11 and greater, background location should be asked after fine and coarse permission - * If background permission is asked at the same time as fine and coarse then both permission request are ignored - * */ - private static void backgroundLocationPermissionLogic(Context context, boolean promptLocation, boolean fallbackToSettings) { - GetPackageInfoResult result = - PackageInfoHelper.Companion.getInfo( - context, - context.getPackageName(), - PackageManager.GET_PERMISSIONS - ); - - if (!result.getSuccessful() || result.getPackageInfo() == null) { - sendAndClearPromptHandlers(promptLocation, OneSignal.PromptActionResult.ERROR); - return; - } - - List permissionList = Arrays.asList(result.getPackageInfo().requestedPermissions); - - if (permissionList.contains("android.permission.ACCESS_BACKGROUND_LOCATION")) { - // ACCESS_BACKGROUND_LOCATION permission defined on Manifest, prompt for permission - requestPermission = "android.permission.ACCESS_BACKGROUND_LOCATION"; - } - - if (requestPermission != null && promptLocation) { - LocationPermissionController.INSTANCE.prompt(fallbackToSettings, requestPermission); - } else { - // Fine permission already granted - sendAndClearPromptHandlers(promptLocation, OneSignal.PromptActionResult.PERMISSION_GRANTED); - startGetLocation(); - } - } - - // Started from this class or PermissionActivity - static void startGetLocation() { - OneSignal.Log(OneSignal.LOG_LEVEL.DEBUG, "LocationController startGetLocation with lastLocation: " + lastLocation); - - try { - if (isGooglePlayServicesAvailable()) { - GMSLocationController.startGetLocation(); - } else if (isHMSAvailable()) { - HMSLocationController.startGetLocation(); - } else { - OneSignal.Log(OneSignal.LOG_LEVEL.WARN, "LocationController startGetLocation not possible, no location dependency found"); - fireFailedComplete(); - } - } catch (Throwable t) { - OneSignal.Log(OneSignal.LOG_LEVEL.WARN, "Location permission exists but there was an error initializing: ", t); - fireFailedComplete(); - } - } - - static void onFocusChange() { - synchronized (syncLock) { - if (isGooglePlayServicesAvailable()) { - GMSLocationController.onFocusChange(); - return; - } - - if (isHMSAvailable()) - HMSLocationController.onFocusChange(); - } - } - - // If we are using device type Android for push we can safely assume we are using Google Play services - static boolean isGooglePlayServicesAvailable() { - return OSUtils.isAndroidDeviceType() && OSUtils.hasGMSLocationLibrary(); - } - - // If we are using device type Huawei for push we can safely assume we are using HMS Core - static boolean isHMSAvailable() { - return OSUtils.isHuaweiDeviceType() && OSUtils.hasHMSLocationLibrary(); - } - - static void fireFailedComplete() { - synchronized (syncLock) { - if (isGooglePlayServicesAvailable()) - GMSLocationController.fireFailedComplete(); - else if (isHMSAvailable()) - HMSLocationController.fireFailedComplete(); - } - fireComplete(null); - } - - private static void fireComplete(LocationPoint point) { - // create local copies of fields in thread-safe way - HashMap _locationHandlers = new HashMap<>(); - Thread _fallbackFailThread; - synchronized (LocationController.class) { - _locationHandlers.putAll(LocationController.locationHandlers); - LocationController.locationHandlers.clear(); - _fallbackFailThread = LocationController.fallbackFailThread; - } - - // execute race-independent logic - for(PermissionType type : _locationHandlers.keySet()) - _locationHandlers.get(type).onComplete(point); - if (_fallbackFailThread != null && !Thread.currentThread().equals(_fallbackFailThread)) - _fallbackFailThread.interrupt(); - - // clear fallbackFailThread in thread-safe way - if (_fallbackFailThread == LocationController.fallbackFailThread) { - synchronized (LocationController.class) { - if (_fallbackFailThread == LocationController.fallbackFailThread) - LocationController.fallbackFailThread = null; - } - } - // Save last time so even if a failure we trigger the same schedule update - setLastLocationTime(OneSignal.getTime().getCurrentTimeMillis()); - } - - protected static void fireCompleteForLocation(Location location) { - OneSignal.Log(OneSignal.LOG_LEVEL.DEBUG, "LocationController fireCompleteForLocation with location: " + location); - LocationPoint point = new LocationPoint(); - - point.accuracy = location.getAccuracy(); - point.bg = !OneSignal.isInForeground(); - point.type = locationCoarse ? 0 : 1; - point.timeStamp = location.getTime(); - - // Coarse always gives out 14 digits and has an accuracy 2000. - // Always rounding to 7 as this is what fine returns. - if (locationCoarse) { - point.lat = new BigDecimal(location.getLatitude()).setScale(7, RoundingMode.HALF_UP).doubleValue(); - point.log = new BigDecimal(location.getLongitude()).setScale(7, RoundingMode.HALF_UP).doubleValue(); - } else { - point.lat = location.getLatitude(); - point.log = location.getLongitude(); - } - - fireComplete(point); - scheduleUpdate(classContext); - } - - protected static class LocationHandlerThread extends HandlerThread { - Handler mHandler; - - LocationHandlerThread() { - super("OSH_LocationHandlerThread"); - start(); - mHandler = new Handler(getLooper()); - } - } -} diff --git a/OneSignalSDK/onesignal/src/main/java/com/onesignal/NotificationChannelManager.java b/OneSignalSDK/onesignal/src/main/java/com/onesignal/NotificationChannelManager.java deleted file mode 100644 index ab0ad74409..0000000000 --- a/OneSignalSDK/onesignal/src/main/java/com/onesignal/NotificationChannelManager.java +++ /dev/null @@ -1,292 +0,0 @@ -/** - * Modified MIT License - * - * Copyright 2017 OneSignal - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * 1. The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * 2. All copies of substantial portions of the Software may only be used in connection - * with services provided by OneSignal. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ - -package com.onesignal; - -import android.app.Notification; -import android.app.NotificationChannel; -import android.app.NotificationChannelGroup; -import android.app.NotificationManager; -import android.content.Context; -import android.net.Uri; -import android.os.Build; -import android.os.DeadSystemException; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.RequiresApi; -import androidx.core.app.NotificationManagerCompat; - -import com.onesignal.language.LanguageContext; - -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -import java.math.BigInteger; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -class NotificationChannelManager { - - // Can't create a channel with the id 'miscellaneous' as an exception is thrown. - // Using it results in the notification not being displayed. - // private static final String DEFAULT_CHANNEL_ID = "miscellaneous"; // NotificationChannel.DEFAULT_CHANNEL_ID; - - private static final String DEFAULT_CHANNEL_ID = "fcm_fallback_notification_channel"; - private static final String RESTORE_CHANNEL_ID = "restored_OS_notifications"; - private static final String CHANNEL_PREFIX = "OS_"; - private static final Pattern hexPattern = Pattern.compile("^([A-Fa-f0-9]{8})$"); - - static String createNotificationChannel(OSNotificationGenerationJob notificationJob) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) - return DEFAULT_CHANNEL_ID; - - Context context = notificationJob.getContext(); - JSONObject jsonPayload = notificationJob.getJsonPayload(); - - NotificationManager notificationManager = OneSignalNotificationManager.getNotificationManager(context); - - if (notificationJob.isRestoring()) - return createRestoreChannel(notificationManager); - - // Allow channels created outside the SDK - if (jsonPayload.has("oth_chnl")) { - String otherChannel = jsonPayload.optString("oth_chnl"); - if (notificationManager.getNotificationChannel(otherChannel) != null) - return otherChannel; - } - - if (!jsonPayload.has("chnl")) - return createDefaultChannel(notificationManager); - - try { - return createChannel(context, notificationManager, jsonPayload); - } catch (JSONException e) { - OneSignal.Log(OneSignal.LOG_LEVEL.ERROR, "Could not create notification channel due to JSON payload error!", e); - } - - return DEFAULT_CHANNEL_ID; - } - - // Creates NotificationChannel and NotificationChannelGroup based on a json payload. - // Returns channel id after it is created. - // Language dependent fields will be passed localized - @RequiresApi(api = Build.VERSION_CODES.O) - private static String createChannel(Context context, NotificationManager notificationManager, JSONObject payload) throws JSONException { - // 'chnl' will be a string if coming from FCM and it will be a JSONObject when coming from - // a cold start sync. - Object objChannelPayload = payload.opt("chnl"); - JSONObject channelPayload = null; - if (objChannelPayload instanceof String) - channelPayload = new JSONObject((String)objChannelPayload); - else - channelPayload = (JSONObject)objChannelPayload; - - String channel_id = channelPayload.optString("id", DEFAULT_CHANNEL_ID); - // Ensure we don't try to use the system reserved id - if (channel_id.equals(NotificationChannel.DEFAULT_CHANNEL_ID)) - channel_id = DEFAULT_CHANNEL_ID; - - JSONObject payloadWithText = channelPayload; - if (channelPayload.has("langs")) { - JSONObject langList = channelPayload.getJSONObject("langs"); - String language = LanguageContext.getInstance().getLanguage(); - if (langList.has(language)) - payloadWithText = langList.optJSONObject(language); - } - - String channel_name = payloadWithText.optString("nm", "Miscellaneous"); - - int importance = priorityToImportance(payload.optInt("pri", 6)); - NotificationChannel channel = new NotificationChannel(channel_id, channel_name, importance); - channel.setDescription(payloadWithText.optString("dscr", null)); - - if (channelPayload.has("grp_id")) { - String group_id = channelPayload.optString("grp_id"); - CharSequence group_name = payloadWithText.optString("grp_nm"); - notificationManager.createNotificationChannelGroup(new NotificationChannelGroup(group_id, group_name)); - channel.setGroup(group_id); - } - - if (payload.has("ledc")) { - String ledc = payload.optString("ledc"); - Matcher matcher = hexPattern.matcher(ledc); - BigInteger ledColor; - - if (!matcher.matches()) { - OneSignal.Log(OneSignal.LOG_LEVEL.WARN, "OneSignal LED Color Settings: ARGB Hex value incorrect format (E.g: FF9900FF)"); - ledc = "FFFFFFFF"; - } - - try { - ledColor = new BigInteger(ledc, 16); - channel.setLightColor(ledColor.intValue()); - } catch (Throwable t) { - OneSignal.Log(OneSignal.LOG_LEVEL.ERROR, "Couldn't convert ARGB Hex value to BigInteger:", t); - } - } - channel.enableLights(payload.optInt("led", 1) == 1); - - if (payload.has("vib_pt")) { - long[] vibrationPattern = OSUtils.parseVibrationPattern(payload); - if (vibrationPattern != null) - channel.setVibrationPattern(vibrationPattern); - } - channel.enableVibration(payload.optInt("vib", 1) == 1); - - if (payload.has("sound")) { - // Sound will only play if Importance is set to High or Urgent - String sound = payload.optString("sound", null); - Uri uri = OSUtils.getSoundUri(context, sound); - if (uri != null) - channel.setSound(uri, null); - else if ("null".equals(sound) || "nil".equals(sound)) - channel.setSound(null, null); - // null = None for a sound. - } - // Setting sound to null makes it 'None' in the Settings. - // Otherwise not calling setSound makes it the default notification sound. - - channel.setLockscreenVisibility(payload.optInt("vis", Notification.VISIBILITY_PRIVATE)); - channel.setShowBadge(payload.optInt("bdg", 1) == 1); - channel.setBypassDnd(payload.optInt("bdnd", 0) == 1); - - OneSignal.onesignalLog(OneSignal.LOG_LEVEL.VERBOSE, "Creating notification channel with channel:\n" + channel.toString()); - try { - notificationManager.createNotificationChannel(channel); - } catch (IllegalArgumentException e) { - // TODO: Remove this try-catch once it is figured out which argument is causing Issue #895 - // try-catch added to prevent crashing from the illegal argument - // Added logging above this try-catch so we can evaluate the payload of the next victim - // to report a stacktrace - // https://github.com/OneSignal/OneSignal-Android-SDK/issues/895 - e.printStackTrace(); - } - return channel_id; - } - - @RequiresApi(api = Build.VERSION_CODES.O) - private static String createDefaultChannel(NotificationManager notificationManager) { - NotificationChannel channel = new NotificationChannel(DEFAULT_CHANNEL_ID, - "Miscellaneous", - NotificationManager.IMPORTANCE_DEFAULT); - - channel.enableLights(true); - channel.enableVibration(true); - notificationManager.createNotificationChannel(channel); - - return DEFAULT_CHANNEL_ID; - } - - @RequiresApi(api = Build.VERSION_CODES.O) - private static String createRestoreChannel(NotificationManager notificationManager) { - NotificationChannel channel = new NotificationChannel(RESTORE_CHANNEL_ID, - "Restored", - NotificationManager.IMPORTANCE_LOW); - - notificationManager.createNotificationChannel(channel); - - return RESTORE_CHANNEL_ID; - } - - static void processChannelList(@NonNull Context context, @Nullable JSONArray list) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) - return; - - if (list == null || list.length() == 0) - return; - - NotificationManager notificationManager = OneSignalNotificationManager.getNotificationManager(context); - - Set syncedChannelSet = new HashSet<>(); - int jsonArraySize = list.length(); - for (int i = 0; i < jsonArraySize; i++) { - try { - syncedChannelSet.add(createChannel(context, notificationManager, list.getJSONObject(i))); - } catch (JSONException e) { - OneSignal.Log(OneSignal.LOG_LEVEL.ERROR, "Could not create notification channel due to JSON payload error!", e); - } - } - - if (syncedChannelSet.isEmpty()) - return; - - List existingChannels = getChannelList(notificationManager); - if (existingChannels == null) { - return; - } - - // Delete old channels - Payload will include all changes for the app. Any extra OS_ ones must - // have been deleted from the dashboard and should be removed. - for (NotificationChannel existingChannel : existingChannels) { - String id = existingChannel.getId(); - if (id.startsWith(CHANNEL_PREFIX) && !syncedChannelSet.contains(id)) - notificationManager.deleteNotificationChannel(id); - } - } - - @RequiresApi(api = Build.VERSION_CODES.O) - private static List getChannelList(NotificationManager notificationManager) { - try { - return notificationManager.getNotificationChannels(); - } catch (NullPointerException e) { - // Catching known Android bug, Sometimes throws, - // "Attempt to invoke virtual method 'boolean android.app.NotificationChannel.isDeleted()' on a null object reference" - // https://github.com/OneSignal/OneSignal-Android-SDK/issues/1291 - OneSignal.onesignalLog(OneSignal.LOG_LEVEL.ERROR, "Error when trying to delete notification channel: " + e.getMessage()); - } - catch (Exception e) { - // Suppressing DeadSystemException as the app is already dying for - // another reason and allowing this exception to bubble up would - // create a red herring for app developers. We still re-throw - // others, as we don't want to silently hide other issues. - if (!(e instanceof DeadSystemException)) { - throw e; - } - } - return null; - } - - private static int priorityToImportance(int priority) { - if (priority > 9) - return NotificationManagerCompat.IMPORTANCE_MAX; - if (priority > 7) - return NotificationManagerCompat.IMPORTANCE_HIGH; - if (priority > 5) - return NotificationManagerCompat.IMPORTANCE_DEFAULT; - if (priority > 3) - return NotificationManagerCompat.IMPORTANCE_LOW; - if (priority > 1) - return NotificationManagerCompat.IMPORTANCE_MIN; - - return NotificationManagerCompat.IMPORTANCE_NONE; - } -} diff --git a/OneSignalSDK/onesignal/src/main/java/com/onesignal/NotificationPermissionController.kt b/OneSignalSDK/onesignal/src/main/java/com/onesignal/NotificationPermissionController.kt deleted file mode 100644 index 5b7f41ff36..0000000000 --- a/OneSignalSDK/onesignal/src/main/java/com/onesignal/NotificationPermissionController.kt +++ /dev/null @@ -1,125 +0,0 @@ -/** - * Modified MIT License - * - * Copyright 2022 OneSignal - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * 1. The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * 2. All copies of substantial portions of the Software may only be used in connection - * with services provided by OneSignal. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ - -package com.onesignal - -import android.os.Build - -object NotificationPermissionController : PermissionsActivity.PermissionCallback { - private const val PERMISSION_TYPE = "NOTIFICATION" - private const val ANDROID_PERMISSION_STRING = "android.permission.POST_NOTIFICATIONS" - - private val callbacks: - MutableSet = HashSet() - private var awaitingForReturnFromSystemSettings = false - - init { - PermissionsActivity.registerAsCallback(PERMISSION_TYPE, this) - } - - private val supportsNativePrompt: Boolean by lazy { - Build.VERSION.SDK_INT > 32 && - OSUtils.getTargetSdkVersion(OneSignal.appContext) > 32 - } - - fun prompt( - fallbackToSettings: Boolean, - callback: OneSignal.PromptForPushNotificationPermissionResponseHandler? - ) { - if (callback != null) callbacks.add(callback) - - if (notificationsEnabled()) { - fireCallBacks(true) - return - } - - if (!supportsNativePrompt) { - if (fallbackToSettings) { - showFallbackAlertDialog() - } else { - fireCallBacks(false) - } - return - } - - PermissionsActivity.startPrompt( - fallbackToSettings, - PERMISSION_TYPE, - ANDROID_PERMISSION_STRING, - this::class.java - ) - } - - override fun onAccept() { - OneSignal.refreshNotificationPermissionState() - fireCallBacks(true) - } - - override fun onReject(fallbackToSettings: Boolean) { - val fallbackShown = - if (fallbackToSettings) { - showFallbackAlertDialog() - } else { - false - } - if (!fallbackShown) fireCallBacks(false) - } - - // Returns true if dialog was shown - private fun showFallbackAlertDialog(): Boolean { - val activity = OneSignal.getCurrentActivity() ?: return false - AlertDialogPrepromptForAndroidSettings.show( - activity, - activity.getString(R.string.notification_permission_name_for_title), - activity.getString(R.string.notification_permission_settings_message), - object : AlertDialogPrepromptForAndroidSettings.Callback { - override fun onAccept() { - NavigateToAndroidSettingsForNotifications.show(activity) - awaitingForReturnFromSystemSettings = true - } - override fun onDecline() { - fireCallBacks(false) - } - } - ) - return true - } - - // Fires callbacks and clears them to ensure each is only called once. - private fun fireCallBacks(accepted: Boolean) { - callbacks.forEach { it.response(accepted) } - callbacks.clear() - } - - fun onAppForegrounded() { - if (!awaitingForReturnFromSystemSettings) return - awaitingForReturnFromSystemSettings = false - fireCallBacks(notificationsEnabled()) - } - - private fun notificationsEnabled() = OSUtils.areNotificationsEnabled(OneSignal.appContext) -} diff --git a/OneSignalSDK/onesignal/src/main/java/com/onesignal/NotificationSummaryManager.java b/OneSignalSDK/onesignal/src/main/java/com/onesignal/NotificationSummaryManager.java deleted file mode 100644 index 5829595230..0000000000 --- a/OneSignalSDK/onesignal/src/main/java/com/onesignal/NotificationSummaryManager.java +++ /dev/null @@ -1,224 +0,0 @@ -package com.onesignal; - -import android.app.NotificationManager; -import android.content.ContentValues; -import android.content.Context; -import android.database.Cursor; - -import com.onesignal.OneSignalDbContract.NotificationTable; - -import org.json.JSONException; -import org.json.JSONObject; - -class NotificationSummaryManager { - - // A notification was just dismissed, check if it was a child to a summary notification and update it. - static void updatePossibleDependentSummaryOnDismiss(Context context, OneSignalDb db, int androidNotificationId) { - Cursor cursor = db.query( - NotificationTable.TABLE_NAME, - new String[] { NotificationTable.COLUMN_NAME_GROUP_ID }, // retColumn - NotificationTable.COLUMN_NAME_ANDROID_NOTIFICATION_ID + " = " + androidNotificationId, - null, null, null, null); - - if (cursor.moveToFirst()) { - String group = cursor.getString(cursor.getColumnIndex(NotificationTable.COLUMN_NAME_GROUP_ID)); - cursor.close(); - - if (group != null) - updateSummaryNotificationAfterChildRemoved(context, db, group, true); - } - else - cursor.close(); - } - - // Called from an opened / dismissed / cancel event of a single notification to update it's parent the summary notification. - static void updateSummaryNotificationAfterChildRemoved(Context context, OneSignalDb db, String group, boolean dismissed) { - Cursor cursor = null; - try { - cursor = internalUpdateSummaryNotificationAfterChildRemoved(context, db, group, dismissed); - } catch (Throwable t) { - OneSignal.Log(OneSignal.LOG_LEVEL.ERROR, "Error running updateSummaryNotificationAfterChildRemoved!", t); - } finally { - if (cursor != null && !cursor.isClosed()) - cursor.close(); - } - } - - private static Cursor internalUpdateSummaryNotificationAfterChildRemoved(Context context, OneSignalDb db, String group, boolean dismissed) { - Cursor cursor = db.query( - NotificationTable.TABLE_NAME, - new String[] { - NotificationTable.COLUMN_NAME_ANDROID_NOTIFICATION_ID, - NotificationTable.COLUMN_NAME_CREATED_TIME, - NotificationTable.COLUMN_NAME_FULL_DATA, - }, - NotificationTable.COLUMN_NAME_GROUP_ID + " = ? AND " + // Where String - NotificationTable.COLUMN_NAME_DISMISSED + " = 0 AND " + - NotificationTable.COLUMN_NAME_OPENED + " = 0 AND " + - NotificationTable.COLUMN_NAME_IS_SUMMARY + " = 0" , - new String[] { group }, // whereArgs - null, null, - NotificationTable._ID + " DESC"); // sort order, new to old); - - int notificationsInGroup = cursor.getCount(); - - // If all individual notifications consumed - // - Remove summary notification from the shade. - // - Mark summary notification as consumed. - if (notificationsInGroup == 0 && !group.equals(OneSignalNotificationManager.getGrouplessSummaryKey())) { - cursor.close(); - - Integer androidNotifId = getSummaryNotificationId(db, group); - if (androidNotifId == null) - return cursor; - - // Remove the summary notification from the shade. - NotificationManager notificationManager = OneSignalNotificationManager.getNotificationManager(context); - notificationManager.cancel(androidNotifId); - - // Mark the summary notification as opened or dismissed. - ContentValues values = new ContentValues(); - values.put(dismissed ? NotificationTable.COLUMN_NAME_DISMISSED : NotificationTable.COLUMN_NAME_OPENED, 1); - db.update(NotificationTable.TABLE_NAME, - values, - NotificationTable.COLUMN_NAME_ANDROID_NOTIFICATION_ID + " = " + androidNotifId, - null); - return cursor; - } - - // Only a single notification now in the group - // - Need to recreate a summary notification so it looks like a normal notifications since we - // only have one notification now. - if (notificationsInGroup == 1) { - cursor.close(); - Integer androidNotifId = getSummaryNotificationId(db, group); - if (androidNotifId == null) - return cursor; - restoreSummary(context, group); - return cursor; - } - - // 2 or more still left in the group - // - Just need to update the summary notification. - // - Don't need start a broadcast / service as the extender doesn't support overriding - // the summary notification. - try { - cursor.moveToFirst(); - Long datetime = cursor.getLong(cursor.getColumnIndex(NotificationTable.COLUMN_NAME_CREATED_TIME)); - String jsonStr = cursor.getString(cursor.getColumnIndex(NotificationTable.COLUMN_NAME_FULL_DATA)); - cursor.close(); - - Integer androidNotifId = getSummaryNotificationId(db, group); - if (androidNotifId == null) - return cursor; - - OSNotificationGenerationJob notificationJob = new OSNotificationGenerationJob(context); - notificationJob.setRestoring(true); - notificationJob.setShownTimeStamp(datetime); - notificationJob.setJsonPayload(new JSONObject(jsonStr)); - - GenerateNotification.updateSummaryNotification(notificationJob); - } catch (JSONException e) { - e.printStackTrace(); - } - - return cursor; - } - - private static void restoreSummary(Context context, String group) { - OneSignalDbHelper dbHelper = OneSignalDbHelper.getInstance(context); - - Cursor cursor = null; - - String[] whereArgs = { group }; - - try { - cursor = dbHelper.query( - NotificationTable.TABLE_NAME, - OSNotificationRestoreWorkManager.COLUMNS_FOR_RESTORE, - NotificationTable.COLUMN_NAME_GROUP_ID + " = ? AND " + - NotificationTable.COLUMN_NAME_DISMISSED + " = 0 AND " + - NotificationTable.COLUMN_NAME_OPENED + " = 0 AND " + - NotificationTable.COLUMN_NAME_IS_SUMMARY + " = 0", - whereArgs, - null, // group by - null, // filter by row groups - null - ); - - OSNotificationRestoreWorkManager.showNotificationsFromCursor(context, cursor, 0); - } catch (Throwable t) { - OneSignal.Log(OneSignal.LOG_LEVEL.ERROR, "Error restoring notification records! ", t); - } finally { - if (cursor != null && !cursor.isClosed()) - cursor.close(); - } - } - - static Integer getSummaryNotificationId(OneSignalDb db, String group) { - Integer androidNotifId = null; - Cursor cursor = null; - - String whereStr = NotificationTable.COLUMN_NAME_GROUP_ID + " = ? AND " + - NotificationTable.COLUMN_NAME_DISMISSED + " = 0 AND " + - NotificationTable.COLUMN_NAME_OPENED + " = 0 AND " + - NotificationTable.COLUMN_NAME_IS_SUMMARY + " = 1"; - String[] whereArgs = new String[] { group }; - - try { - // Get the Android Notification ID of the summary notification - cursor = db.query( - NotificationTable.TABLE_NAME, - new String[] { NotificationTable.COLUMN_NAME_ANDROID_NOTIFICATION_ID }, // retColumn - whereStr, - whereArgs, - null, - null, - null); - - boolean hasRecord = cursor.moveToFirst(); - if (!hasRecord) { - cursor.close(); - return null; - } - androidNotifId = cursor.getInt(cursor.getColumnIndex(NotificationTable.COLUMN_NAME_ANDROID_NOTIFICATION_ID)); - cursor.close(); - } catch (Throwable t) { - OneSignal.Log(OneSignal.LOG_LEVEL.ERROR, "Error getting android notification id for summary notification group: " + group, t); - } finally { - if (cursor != null && !cursor.isClosed()) - cursor.close(); - } - - return androidNotifId; - } - - /** - * Clears notifications from the status bar based on a few parameters - */ - static void clearNotificationOnSummaryClick(Context context, OneSignalDbHelper dbHelper, String group) { - // Obtain the group to clear notifications from - Integer groupId = NotificationSummaryManager.getSummaryNotificationId(dbHelper, group); - boolean isGroupless = group.equals(OneSignalNotificationManager.getGrouplessSummaryKey()); - - NotificationManager notificationManager = OneSignalNotificationManager.getNotificationManager(context); - // Obtain the most recent notification id - Integer mostRecentId = OneSignalNotificationManager.getMostRecentNotifIdFromGroup(dbHelper, group, isGroupless); - if (mostRecentId != null) { - boolean shouldDismissAll = OneSignal.getClearGroupSummaryClick(); - if (shouldDismissAll) { - - // If the group is groupless, obtain the hardcoded groupless summary id - if (isGroupless) - groupId = OneSignalNotificationManager.getGrouplessSummaryId(); - - // Clear the entire notification summary - if (groupId != null) - notificationManager.cancel(groupId); - } else { - // Clear the most recent notification from the status bar summary - OneSignal.removeNotification(mostRecentId); - } - } - } -} \ No newline at end of file diff --git a/OneSignalSDK/onesignal/src/main/java/com/onesignal/OSFocusHandler.kt b/OneSignalSDK/onesignal/src/main/java/com/onesignal/OSFocusHandler.kt deleted file mode 100644 index b0c4fc4ffa..0000000000 --- a/OneSignalSDK/onesignal/src/main/java/com/onesignal/OSFocusHandler.kt +++ /dev/null @@ -1,148 +0,0 @@ -/** - * Modified MIT License - *

- * Copyright 2022 OneSignal - *

- * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - *

- * 1. The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - *

- * 2. All copies of substantial portions of the Software may only be used in connection - * with services provided by OneSignal. - *

- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ - -package com.onesignal - -import android.content.Context -import androidx.work.Constraints -import androidx.work.ExistingWorkPolicy -import androidx.work.NetworkType -import androidx.work.OneTimeWorkRequest -import androidx.work.Worker -import androidx.work.WorkerParameters -import java.util.concurrent.TimeUnit - -class OSFocusHandler { - - private var stopRunnable: Runnable? = null - - fun hasBackgrounded() = backgrounded - - fun hasCompleted() = completed - - fun startOnFocusWork() { - resetBackgroundState() - OneSignal.onesignalLog( - OneSignal.LOG_LEVEL.DEBUG, - "OSFocusHandler running onAppFocus" - ) - OneSignal.onAppFocus() - } - - fun startOnStartFocusWork() { - if (stopped) { - stopped = false - stopRunnable = null - OneSignal.onesignalLog( - OneSignal.LOG_LEVEL.DEBUG, - "OSFocusHandler running onAppStartFocusLogic" - ) - OneSignal.onAppStartFocusLogic() - } else { - resetStopState() - } - } - - fun startOnStopFocusWork() { - stopRunnable = Runnable { - stopped = true - OneSignal.onesignalLog( - OneSignal.LOG_LEVEL.DEBUG, - "OSFocusHandler setting stop state: true" - ) - }.also { - OSTimeoutHandler.getTimeoutHandler().startTimeout(stopDelay, it) - } - } - - fun startOnLostFocusWorker(tag: String, delay: Long, context: Context) { - val constraints = buildConstraints() - val workRequest = OneTimeWorkRequest.Builder(OnLostFocusWorker::class.java) - .setConstraints(constraints) - .setInitialDelay(delay, TimeUnit.MILLISECONDS) - .addTag(tag) - .build() - - OSWorkManagerHelper.getInstance(context) - .enqueueUniqueWork( - tag, - ExistingWorkPolicy.KEEP, - workRequest - ) - } - - fun cancelOnLostFocusWorker(tag: String, context: Context) { - OSWorkManagerHelper.getInstance(context).cancelAllWorkByTag(tag) - } - - private fun resetStopState() { - stopped = false - stopRunnable?.let { - OSTimeoutHandler.getTimeoutHandler().destroyTimeout(it) - } - } - - private fun resetBackgroundState() { - resetStopState() - backgrounded = false - } - - private fun buildConstraints(): Constraints { - return Constraints.Builder() - .setRequiredNetworkType(NetworkType.CONNECTED) - .build() - } - - class OnLostFocusWorker(context: Context, workerParams: WorkerParameters) : - Worker(context, workerParams) { - override fun doWork(): Result { - onLostFocusDoWork() - return Result.success() - } - } - - companion object { - private const val stopDelay = 1500L - private var stopped = false - private var backgrounded = false - private var completed = false - - fun onLostFocusDoWork() { - val activityLifecycleHandler = ActivityLifecycleListener.getActivityLifecycleHandler() - if (activityLifecycleHandler == null || activityLifecycleHandler.curActivity == null) { - OneSignal.setInForeground(false) - } - OneSignal.onesignalLog( - OneSignal.LOG_LEVEL.DEBUG, - "OSFocusHandler running onAppLostFocus" - ) - backgrounded = true - OneSignal.onAppLostFocus() - completed = true - } - } -} diff --git a/OneSignalSDK/onesignal/src/main/java/com/onesignal/OSInAppMessageController.java b/OneSignalSDK/onesignal/src/main/java/com/onesignal/OSInAppMessageController.java deleted file mode 100644 index 1ac1049952..0000000000 --- a/OneSignalSDK/onesignal/src/main/java/com/onesignal/OSInAppMessageController.java +++ /dev/null @@ -1,1047 +0,0 @@ -package com.onesignal; - -import android.app.AlertDialog; -import android.content.DialogInterface; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import com.onesignal.OSDynamicTriggerController.OSDynamicTriggerControllerObserver; -import com.onesignal.language.LanguageContext; - -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.Date; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; - -import static com.onesignal.OSInAppMessageRepository.IAM_DATA_RESPONSE_RETRY_KEY; - -class OSInAppMessageController extends OSBackgroundManager implements OSDynamicTriggerControllerObserver, OSSystemConditionController.OSSystemConditionObserver { - - private static final Object LOCK = new Object(); - private final static String OS_IAM_DB_ACCESS = "OS_IAM_DB_ACCESS"; - public static final String IN_APP_MESSAGES_JSON_KEY = "in_app_messages"; - private static final String LIQUID_TAG_SCRIPT = "\n\n" + - ""; - private static ArrayList PREFERRED_VARIANT_ORDER = new ArrayList() {{ - add("android"); - add("app"); - add("all"); - }}; - - private final OSLogger logger; - private final OSTaskController taskController; - private final LanguageContext languageContext; - - private OSSystemConditionController systemConditionController; - private OSInAppMessageRepository inAppMessageRepository; - private OSInAppMessageLifecycleHandler inAppMessageLifecycleHandler; - - OSTriggerController triggerController; - - // IAMs loaded remotely from on_session - // If on_session won't be called this will be loaded from cache - @NonNull - private ArrayList messages; - // IAMs that have been dismissed by the user - // This mean they have already displayed to the user - @NonNull - final private Set dismissedMessages; - // IAMs that have been displayed to the user - // This means their impression has been successfully posted to our backend and should not be counted again - @NonNull - final private Set impressionedMessages; - // This means their impression has been successfully posted to our backend and should not be counted again - @NonNull - final private Set viewedPageIds; - // IAM clicks that have been successfully posted to our backend and should not be counted again - @NonNull - final private Set clickedClickIds; - // Ordered IAMs queued to display, includes the message currently displaying, if any. - @NonNull - final private ArrayList messageDisplayQueue; - // IAMs displayed with last displayed time and quantity of displays data - // This is retrieved from a DB Table that take care of each object to be unique - @Nullable - private List redisplayedInAppMessages = null; - - private OSInAppMessagePrompt currentPrompt = null; - private boolean inAppMessagingEnabled = true; - private boolean inAppMessageShowing = false; - - @Nullable - private String userTagsString = ""; - - @Nullable - private OSInAppMessageContent pendingMessageContent = null; - - private boolean waitForTags = false; - - @Nullable - Date lastTimeInAppDismissed = null; - - protected OSInAppMessageController(OneSignalDbHelper dbHelper, OSTaskController controller, OSLogger logger, - OSSharedPreferences sharedPreferences, LanguageContext languageContext) { - taskController = controller; - messages = new ArrayList<>(); - dismissedMessages = OSUtils.newConcurrentSet(); - messageDisplayQueue = new ArrayList<>(); - impressionedMessages = OSUtils.newConcurrentSet(); - viewedPageIds = OSUtils.newConcurrentSet(); - clickedClickIds = OSUtils.newConcurrentSet(); - triggerController = new OSTriggerController(this); - systemConditionController = new OSSystemConditionController(this); - this.languageContext = languageContext; - this.logger = logger; - - inAppMessageRepository = getInAppMessageRepository(dbHelper, logger, sharedPreferences); - Set tempDismissedSet = inAppMessageRepository.getDismissedMessagesId(); - if (tempDismissedSet != null) - dismissedMessages.addAll(tempDismissedSet); - - Set tempImpressionsSet = inAppMessageRepository.getImpressionesMessagesId(); - if (tempImpressionsSet != null) - impressionedMessages.addAll(tempImpressionsSet); - - Set tempPageImpressionsSet = inAppMessageRepository.getViewPageImpressionedIds(); - if (tempPageImpressionsSet != null) - viewedPageIds.addAll(tempPageImpressionsSet); - - Set tempClickedMessageIdsSet = inAppMessageRepository.getClickedMessagesId(); - if (tempClickedMessageIdsSet != null) - clickedClickIds.addAll(tempClickedMessageIdsSet); - - Date tempLastTimeInAppDismissed = inAppMessageRepository.getLastTimeInAppDismissed(); - if (tempLastTimeInAppDismissed != null) { - lastTimeInAppDismissed = tempLastTimeInAppDismissed; - } - - initRedisplayData(); - } - - OSInAppMessageRepository getInAppMessageRepository(OneSignalDbHelper dbHelper, OSLogger logger, OSSharedPreferences sharedPreferences) { - if (inAppMessageRepository == null) - inAppMessageRepository = new OSInAppMessageRepository(dbHelper, logger, sharedPreferences); - - return inAppMessageRepository; - } - - protected void initRedisplayData() { - Runnable getCachedIAMRunnable = new BackgroundRunnable() { - @Override - public void run() { - super.run(); - - synchronized (LOCK) { - redisplayedInAppMessages = inAppMessageRepository.getCachedInAppMessages(); - logger.debug("Retrieved IAMs from DB redisplayedInAppMessages: " + redisplayedInAppMessages.toString()); - } - } - }; - - taskController.addTaskToQueue(getCachedIAMRunnable); - taskController.startPendingTasks(); - } - - boolean shouldRunTaskThroughQueue() { - synchronized (LOCK) { - return redisplayedInAppMessages == null && taskController.shouldRunTaskThroughQueue(); - } - } - - void executeRedisplayIAMDataDependantTask(Runnable task) { - synchronized (LOCK) { - if (shouldRunTaskThroughQueue()) { - logger.debug("Delaying task due to redisplay data not retrieved yet"); - taskController.addTaskToQueue(task); - } else { - task.run(); - } - } - } - - void resetSessionLaunchTime() { - OSDynamicTriggerController.resetSessionLaunchTime(); - } - - // Normally we wait until on_session call to download the latest IAMs - // however an on session won't happen - void initWithCachedInAppMessages() { - // Do not reload from cache if already loaded. - if (!messages.isEmpty()) { - logger.debug("initWithCachedInAppMessages with already in memory messages: " + messages); - return; - } - - String cachedInAppMessageString = inAppMessageRepository.getSavedIAMs(); - logger.debug("initWithCachedInAppMessages: " + cachedInAppMessageString); - - if (cachedInAppMessageString == null || cachedInAppMessageString.isEmpty()) - return; - - synchronized (LOCK) { - try { - // Second check to avoid getting the lock while message list is being set - if (!messages.isEmpty()) - return; - - processInAppMessageJson(new JSONArray(cachedInAppMessageString)); - } catch (JSONException e) { - e.printStackTrace(); - } - } - } - - /** - * Called after the device is registered from UserStateSynchronizer - * which is the REST call to create the player record on_session - */ - void receivedInAppMessageJson(@NonNull final JSONArray json) throws JSONException { - // Cache copy for quick cold starts - inAppMessageRepository.saveIAMs(json.toString()); - - executeRedisplayIAMDataDependantTask(new Runnable() { - @Override - public void run() { - resetRedisplayMessagesBySession(); - try { - processInAppMessageJson(json); - } catch (JSONException e) { - logger.error("ERROR processing InAppMessageJson JSON Response.", e); - } - } - }); - } - - private void resetRedisplayMessagesBySession() { - for (OSInAppMessageInternal redisplayInAppMessage : redisplayedInAppMessages) { - redisplayInAppMessage.setDisplayedInSession(false); - } - } - - private void processInAppMessageJson(@NonNull JSONArray json) throws JSONException { - synchronized (LOCK) { - ArrayList newMessages = new ArrayList<>(); - for (int i = 0; i < json.length(); i++) { - JSONObject messageJson = json.getJSONObject(i); - OSInAppMessageInternal message = new OSInAppMessageInternal(messageJson); - // Avoid null checks later if IAM already comes with null id - if (message.messageId != null) { - newMessages.add(message); - } - } - - messages = newMessages; - } - - evaluateInAppMessages(); - } - - private void evaluateInAppMessages() { - logger.debug("Starting evaluateInAppMessages"); - - if (shouldRunTaskThroughQueue()) { - taskController.addTaskToQueue(new Runnable() { - @Override - public void run() { - logger.debug("Delaying evaluateInAppMessages due to redisplay data not retrieved yet"); - evaluateInAppMessages(); - } - }); - return; - } - - for (OSInAppMessageInternal message : messages) { - // Make trigger evaluation first, dynamic trigger might change "trigger changed" flag value for redisplay messages - if (triggerController.evaluateMessageTriggers(message)) { - setDataForRedisplay(message); - - if (!dismissedMessages.contains(message.messageId) && !message.isFinished()) { - queueMessageForDisplay(message); - } - } - } - } - - private @Nullable String variantIdForMessage(@NonNull OSInAppMessageInternal message) { - String language = languageContext.getLanguage(); - - for (String variant : PREFERRED_VARIANT_ORDER) { - if (!message.variants.containsKey(variant)) - continue; - - HashMap variantMap = message.variants.get(variant); - if (variantMap.containsKey(language)) - return variantMap.get(language); - return variantMap.get("default"); - } - - return null; - } - - void onMessageWasShown(@NonNull final OSInAppMessageInternal message) { - - onMessageDidDisplay(message); - - if (message.isPreview) - return; - - // Check that the messageId is in impressionedMessages so we return early without a second post being made - if (impressionedMessages.contains(message.messageId)) - return; - - // Add the messageId to impressionedMessages so no second request is made - impressionedMessages.add(message.messageId); - - final String variantId = variantIdForMessage(message); - if (variantId == null) - return; - - inAppMessageRepository.sendIAMImpression(OneSignal.appId, OneSignal.getUserId(), variantId, - new OSUtils().getDeviceType(), message.messageId, impressionedMessages, - new OSInAppMessageRepository.OSInAppMessageRequestResponse() { - @Override - public void onSuccess(String response) { - // Everything handled by repository - } - - @Override - public void onFailure(String response) { - // Post failed, impressioned Messages should be removed and this way another post can be attempted - impressionedMessages.remove(message.messageId); - } - }); - } - - void onPageChanged(@NonNull final OSInAppMessageInternal message, @NonNull final JSONObject eventJson) { - final OSInAppMessagePage newPage = new OSInAppMessagePage(eventJson); - if (message.isPreview) { - return; - } - fireRESTCallForPageChange(message, newPage); - } - - void onMessageActionOccurredOnMessage(@NonNull final OSInAppMessageInternal message, @NonNull final JSONObject actionJson) throws JSONException { - final OSInAppMessageAction action = new OSInAppMessageAction(actionJson); - action.setFirstClick(message.takeActionAsUnique()); - - firePublicClickHandler(message.messageId, action); - beginProcessingPrompts(message, action.getPrompts()); - fireClickAction(action); - fireRESTCallForClick(message, action); - fireTagCallForClick(action); - fireOutcomesForClick(message.messageId, action.getOutcomes()); - } - - void onMessageActionOccurredOnPreview(@NonNull final OSInAppMessageInternal message, @NonNull final JSONObject actionJson) throws JSONException { - final OSInAppMessageAction action = new OSInAppMessageAction(actionJson); - action.setFirstClick(message.takeActionAsUnique()); - - firePublicClickHandler(message.messageId, action); - beginProcessingPrompts(message, action.getPrompts()); - fireClickAction(action); - logInAppMessagePreviewActions(action); - } - - /** - * IAM Lifecycle methods - * The following methods call the public OSInAppMessageLifecycleHandler callbacks - */ - void setInAppMessageLifecycleHandler(@Nullable OSInAppMessageLifecycleHandler handler) { - inAppMessageLifecycleHandler = handler; - } - - void onMessageWillDisplay(@NonNull final OSInAppMessageInternal message) { - if (inAppMessageLifecycleHandler == null) { - logger.verbose("OSInAppMessageController onMessageWillDisplay: inAppMessageLifecycleHandler is null"); - return; - } - inAppMessageLifecycleHandler.onWillDisplayInAppMessage(message); - } - - void onMessageDidDisplay(@NonNull final OSInAppMessageInternal message) { - if (inAppMessageLifecycleHandler == null) { - logger.verbose("OSInAppMessageController onMessageDidDisplay: inAppMessageLifecycleHandler is null"); - return; - } - inAppMessageLifecycleHandler.onDidDisplayInAppMessage(message); - } - - void onMessageWillDismiss(@NonNull final OSInAppMessageInternal message) { - if (inAppMessageLifecycleHandler == null) { - logger.verbose("OSInAppMessageController onMessageWillDismiss: inAppMessageLifecycleHandler is null"); - return; - } - inAppMessageLifecycleHandler.onWillDismissInAppMessage(message); - } - - void onMessageDidDismiss(@NonNull final OSInAppMessageInternal message) { - if (inAppMessageLifecycleHandler == null) { - logger.verbose("OSInAppMessageController onMessageDidDismiss: inAppMessageLifecycleHandler is null"); - return; - } - inAppMessageLifecycleHandler.onDidDismissInAppMessage(message); - } - - /* End IAM Lifecycle methods */ - - private void logInAppMessagePreviewActions(final OSInAppMessageAction action) { - if (action.getTags() != null) - logger.debug("Tags detected inside of the action click payload, ignoring because action came from IAM preview:: " + action.getTags().toString()); - - if (action.getOutcomes().size() > 0) - logger.debug("Outcomes detected inside of the action click payload, ignoring because action came from IAM preview: " + action.getOutcomes().toString()); - - // TODO: Add more action payload preview logs here in future - } - - private void beginProcessingPrompts(OSInAppMessageInternal message, final List prompts) { - if (prompts.size() > 0) { - logger.debug("IAM showing prompts from IAM: " + message.toString()); - // TODO until we don't fix the activity going forward or back dismissing the IAM, we need to auto dismiss - WebViewManager.dismissCurrentInAppMessage(); - showMultiplePrompts(message, prompts); - } - } - - private void showMultiplePrompts(final OSInAppMessageInternal inAppMessage, final List prompts) { - for (OSInAppMessagePrompt prompt : prompts) { - // Don't show prompt twice - if (!prompt.hasPrompted()) { - currentPrompt = prompt; - break; - } - } - - if (currentPrompt != null) { - logger.debug("IAM prompt to handle: " + currentPrompt.toString()); - currentPrompt.setPrompted(true); - currentPrompt.handlePrompt(new OneSignal.OSPromptActionCompletionCallback() { - @Override - public void onCompleted(OneSignal.PromptActionResult result) { - currentPrompt = null; - logger.debug("IAM prompt to handle finished with result: " + result); - - // On preview mode we show informative alert dialogs - if (inAppMessage.isPreview && result == OneSignal.PromptActionResult.LOCATION_PERMISSIONS_MISSING_MANIFEST) - showAlertDialogMessage(inAppMessage, prompts); - else - showMultiplePrompts(inAppMessage, prompts); - } - }); - } else { - logger.debug("No IAM prompt to handle, dismiss message: " + inAppMessage.messageId); - messageWasDismissed(inAppMessage); - } - } - - private void showAlertDialogMessage(final OSInAppMessageInternal inAppMessage, final List prompts) { - final String messageTitle = OneSignal.appContext.getString(R.string.location_permission_missing_title); - final String message = OneSignal.appContext.getString(R.string.location_permission_missing_message); - new AlertDialog.Builder(OneSignal.getCurrentActivity()) - .setTitle(messageTitle) - .setMessage(message) - .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int which) { - showMultiplePrompts(inAppMessage, prompts); - } - }) - .show(); - } - - private void fireOutcomesForClick(String messageId, @NonNull final List outcomes) { - OneSignal.getSessionManager().onDirectInfluenceFromIAMClick(messageId); - OneSignal.sendClickActionOutcomes(outcomes); - } - - private void fireTagCallForClick(@NonNull final OSInAppMessageAction action) { - if (action.getTags() != null) { - OSInAppMessageTag tags = action.getTags(); - - if (tags.getTagsToAdd() != null) - OneSignal.sendTags(tags.getTagsToAdd()); - if (tags.getTagsToRemove() != null) - OneSignal.deleteTags(tags.getTagsToRemove(), null); - } - } - - private void firePublicClickHandler(@NonNull final String messageId, @NonNull final OSInAppMessageAction action) { - if (OneSignal.inAppMessageClickHandler == null) - return; - - CallbackThreadManager.Companion.runOnPreferred(new Runnable() { - @Override - public void run() { - // Send public outcome from handler - // Send public outcome not from handler - // Check that only on the handler - // Any outcome sent on this callback should count as DIRECT from this IAM - OneSignal.getSessionManager().onDirectInfluenceFromIAMClick(messageId); - OneSignal.inAppMessageClickHandler.inAppMessageClicked(action); - } - }); - } - - private void fireClickAction(@NonNull final OSInAppMessageAction action) { - if (action.getClickUrl() != null && !action.getClickUrl().isEmpty()) { - if (action.getUrlTarget() == OSInAppMessageAction.OSInAppMessageActionUrlType.BROWSER) - OSUtils.openURLInBrowser(action.getClickUrl()); - else if (action.getUrlTarget() == OSInAppMessageAction.OSInAppMessageActionUrlType.IN_APP_WEBVIEW) - OneSignalChromeTab.open(action.getClickUrl(), true); - } - } - - private void fireRESTCallForPageChange(@NonNull final OSInAppMessageInternal message, @NonNull final OSInAppMessagePage page) { - final String variantId = variantIdForMessage(message); - if (variantId == null) - return; - - final String pageId = page.getPageId(); - - final String messagePrefixedPageId = message.messageId + pageId; - - // Never send multiple page impressions for the same message UUID unless that page change is from an IAM with redisplay - if (viewedPageIds.contains(messagePrefixedPageId)) { - logger.verbose("Already sent page impression for id: " + pageId); - return; - } - - viewedPageIds.add(messagePrefixedPageId); - - inAppMessageRepository.sendIAMPageImpression(OneSignal.appId, OneSignal.getUserId(), variantId, new OSUtils().getDeviceType(), - message.messageId, pageId, viewedPageIds, new OSInAppMessageRepository.OSInAppMessageRequestResponse() { - @Override - public void onSuccess(String response) { - // Everything handled by repository - } - - @Override - public void onFailure(String response) { - // Post failed, viewed page should be removed and this way another post can be attempted - viewedPageIds.remove(messagePrefixedPageId); - } - }); - } - - private void fireRESTCallForClick(@NonNull final OSInAppMessageInternal message, @NonNull final OSInAppMessageAction action) { - final String variantId = variantIdForMessage(message); - if (variantId == null) - return; - - final String clickId = action.getClickId(); - // If IAM has redisplay the clickId may be available - boolean clickAvailableByRedisplay = message.getRedisplayStats().isRedisplayEnabled() && message.isClickAvailable(clickId); - - // Never count multiple clicks for the same click UUID unless that click is from an IAM with redisplay - if (!clickAvailableByRedisplay && clickedClickIds.contains(clickId)) - return; - - clickedClickIds.add(clickId); - // Track clickId per IAM - message.addClickId(clickId); - - inAppMessageRepository.sendIAMClick(OneSignal.appId, OneSignal.getUserId(), variantId, new OSUtils().getDeviceType(), - message.messageId, clickId, action.isFirstClick(), clickedClickIds, new OSInAppMessageRepository.OSInAppMessageRequestResponse() { - @Override - public void onSuccess(String response) { - // Everything handled by repository - } - - @Override - public void onFailure(String response) { - clickedClickIds.remove(clickId); - message.removeClickId(clickId); - } - }); - } - - /** - * Part of redisplay logic - *

- * In order to redisplay an IAM, the following conditions must be satisfied: - * 1. IAM has redisplay property - * 2. Time delay between redisplay satisfied - * 3. Has more redisplays - * 4. An IAM trigger was satisfied - *

- * For redisplay, the message need to be removed from the arrays that track the display/impression - * For click counting, every message has it click id array - */ - private void setDataForRedisplay(OSInAppMessageInternal message) { - boolean messageDismissed = dismissedMessages.contains(message.messageId); - int index = redisplayedInAppMessages.indexOf(message); - - if (messageDismissed && index != -1) { - OSInAppMessageInternal savedIAM = redisplayedInAppMessages.get(index); - message.getRedisplayStats().setDisplayStats(savedIAM.getRedisplayStats()); - message.setDisplayedInSession(savedIAM.isDisplayedInSession()); - - boolean triggerHasChanged = hasMessageTriggerChanged(message); - logger.debug("setDataForRedisplay: " + message.toString() + " triggerHasChanged: " + triggerHasChanged); - - // Check if conditions are correct for redisplay - if (triggerHasChanged && - message.getRedisplayStats().isDelayTimeSatisfied() && - message.getRedisplayStats().shouldDisplayAgain()) { - logger.debug("setDataForRedisplay message available for redisplay: " + message.messageId); - - dismissedMessages.remove(message.messageId); - impressionedMessages.remove(message.messageId); - // Pages from different IAMs should not impact each other so we can clear the entire - // list when an IAM is dismissed or we are re-displaying the same one - viewedPageIds.clear(); - inAppMessageRepository.saveViewPageImpressionedIds(viewedPageIds); - message.clearClickIds(); - } - } - } - - private boolean hasMessageTriggerChanged(OSInAppMessageInternal message) { - // Message that only have dynamic trigger should display only once per session - boolean messageHasOnlyDynamicTrigger = triggerController.messageHasOnlyDynamicTriggers(message); - if (messageHasOnlyDynamicTrigger) - return !message.isDisplayedInSession(); - - // Message that don't have triggers should display only once per session - boolean shouldMessageDisplayInSession = !message.isDisplayedInSession() && message.triggers.isEmpty(); - - return message.isTriggerChanged() || shouldMessageDisplayInSession; - } - - /** - * Message has passed triggers and de-duplication logic. - * Display message now or add it to the queue to be displayed. - */ - private void queueMessageForDisplay(@NonNull OSInAppMessageInternal message) { - synchronized (messageDisplayQueue) { - // Make sure no message is ever added to the queue more than once - if (!messageDisplayQueue.contains(message)) { - messageDisplayQueue.add(message); - logger.debug("In app message with id: " + message.messageId + ", added to the queue"); - } - - attemptToShowInAppMessage(); - } - } - - private void attemptToShowInAppMessage() { - synchronized (messageDisplayQueue) { - // We need to wait for system conditions to be the correct ones - if (!systemConditionController.systemConditionsAvailable()) { - logger.warning("In app message not showing due to system condition not correct"); - return; - } - - logger.debug("displayFirstIAMOnQueue: " + messageDisplayQueue); - // If there are IAMs in the queue and nothing showing, show first in the queue - if (messageDisplayQueue.size() > 0 && !isInAppMessageShowing()) { - logger.debug("No IAM showing currently, showing first item in the queue!"); - displayMessage(messageDisplayQueue.get(0)); - return; - } - - logger.debug("In app message is currently showing or there are no IAMs left in the queue! isInAppMessageShowing: " + isInAppMessageShowing()); - } - } - - boolean isInAppMessageShowing() { - return inAppMessageShowing; - } - - @Nullable - OSInAppMessageInternal getCurrentDisplayedInAppMessage() { - // When in app messaging is paused, the messageDisplayQueue might have IAMs, so return null - return inAppMessageShowing ? messageDisplayQueue.get(0) : null; - } - - /** - * Called after an In-App message is closed and it's dismiss animation has completed - */ - void messageWasDismissed(@NonNull OSInAppMessageInternal message) { - messageWasDismissed(message, false); - } - - void messageWasDismissed(@NonNull OSInAppMessageInternal message, boolean failed) { - - if (!message.isPreview) { - dismissedMessages.add(message.messageId); - // If failed we will retry on next session - if (!failed) { - inAppMessageRepository.saveDismissedMessagesId(dismissedMessages); - - // Don't keep track of last displayed time for a preview - lastTimeInAppDismissed = new Date(); - // Only increase IAM display quantity if IAM was truly displayed - persistInAppMessage(message); - } - logger.debug("OSInAppMessageController messageWasDismissed dismissedMessages: " + dismissedMessages.toString()); - } - - if (!shouldWaitForPromptsBeforeDismiss()) - onMessageDidDismiss(message); - - dismissCurrentMessage(message); - } - - private boolean shouldWaitForPromptsBeforeDismiss() { - return currentPrompt != null; - } - - /** - * Removes first item from the queue and attempts to show the next IAM in the queue - * - * @param message The message dismissed, preview messages are null - */ - private void dismissCurrentMessage(@Nullable OSInAppMessageInternal message) { - // Remove DIRECT influence due to ClickHandler of ClickAction outcomes - OneSignal.getSessionManager().onDirectInfluenceFromIAMClickFinished(); - - if (shouldWaitForPromptsBeforeDismiss()) { - logger.debug("Stop evaluateMessageDisplayQueue because prompt is currently displayed"); - return; - } - - inAppMessageShowing = false; - synchronized (messageDisplayQueue) { - if (message != null && !message.isPreview && messageDisplayQueue.size() > 0) { - if (!messageDisplayQueue.contains(message)) { - logger.debug("Message already removed from the queue!"); - return; - } else { - String removedMessageId = messageDisplayQueue.remove(0).messageId; - logger.debug("In app message with id: " + removedMessageId + ", dismissed (removed) from the queue!"); - } - } - - // Display the next message in the queue, or attempt to add more IAMs to the queue - if (messageDisplayQueue.size() > 0) { - logger.debug("In app message on queue available: " + messageDisplayQueue.get(0).messageId); - displayMessage(messageDisplayQueue.get(0)); - } else { - logger.debug("In app message dismissed evaluating messages"); - evaluateInAppMessages(); - } - } - } - - private void persistInAppMessage(final OSInAppMessageInternal message) { - long displayTimeSeconds = OneSignal.getTime().getCurrentTimeMillis() / 1000; - message.getRedisplayStats().setLastDisplayTime(displayTimeSeconds); - message.getRedisplayStats().incrementDisplayQuantity(); - message.setTriggerChanged(false); - message.setDisplayedInSession(true); - - Runnable saveIAMOnDBRunnable = new BackgroundRunnable() { - @Override - public void run() { - super.run(); - - inAppMessageRepository.saveInAppMessage(message); - inAppMessageRepository.saveLastTimeInAppDismissed(lastTimeInAppDismissed); - } - }; - runRunnableOnThread(saveIAMOnDBRunnable, OS_IAM_DB_ACCESS); - - // Update the data to enable future re displays - // Avoid calling the repository data again - int index = redisplayedInAppMessages.indexOf(message); - if (index != -1) { - redisplayedInAppMessages.set(index, message); - } else { - redisplayedInAppMessages.add(message); - } - - logger.debug("persistInAppMessageForRedisplay: " + message.toString() + " with msg array data: " + redisplayedInAppMessages.toString()); - } - - private void getTagsForLiquidTemplating(@NonNull final OSInAppMessageInternal message, final boolean isPreview) { - waitForTags = false; - if (isPreview || message.getHasLiquid()) { - waitForTags = true; - OneSignal.getTags(new OneSignal.OSGetTagsHandler() { - @Override - public void tagsAvailable(JSONObject tags) { - waitForTags = false; - if (tags != null) { - userTagsString = tags.toString(); - } - if (pendingMessageContent != null) { - if (!isPreview) { - OneSignal.getSessionManager().onInAppMessageReceived(message.messageId); - } - pendingMessageContent.setContentHtml(taggedHTMLString(pendingMessageContent.getContentHtml())); - WebViewManager.showMessageContent(message, pendingMessageContent); - pendingMessageContent = null; - } - } - }); - } - } - - private OSInAppMessageContent parseMessageContentData(JSONObject data, OSInAppMessageInternal message) { - OSInAppMessageContent content = new OSInAppMessageContent(data); - message.setDisplayDuration(content.getDisplayDuration()); - return content; - } - - private void displayMessage(@NonNull final OSInAppMessageInternal message) { - if (!inAppMessagingEnabled) { - logger.verbose("In app messaging is currently paused, in app messages will not be shown!"); - return; - } - - inAppMessageShowing = true; - - getTagsForLiquidTemplating(message, false); - - inAppMessageRepository.getIAMData(OneSignal.appId, message.messageId, variantIdForMessage(message), - new OSInAppMessageRepository.OSInAppMessageRequestResponse() { - @Override - public void onSuccess(String response) { - try { - JSONObject jsonResponse = new JSONObject(response); - OSInAppMessageContent content = parseMessageContentData(jsonResponse, message); - if (content.getContentHtml() == null) { - logger.debug("displayMessage:OnSuccess: No HTML retrieved from loadMessageContent"); - return; - } - if (waitForTags) { - pendingMessageContent = content; - return; - } - OneSignal.getSessionManager().onInAppMessageReceived(message.messageId); - onMessageWillDisplay(message); - content.setContentHtml(taggedHTMLString(content.getContentHtml())); - WebViewManager.showMessageContent(message, content); - } catch (JSONException e) { - e.printStackTrace(); - } - } - - @Override - public void onFailure(String response) { - inAppMessageShowing = false; - try { - JSONObject jsonResponse = new JSONObject(response); - boolean retry = jsonResponse.getBoolean(IAM_DATA_RESPONSE_RETRY_KEY); - if (retry) { - // Retry displaying the same IAM - // Using the queueMessageForDisplay method follows safety checks to prevent issues - // like having 2 IAMs showing at once or duplicate IAMs in the queue - queueMessageForDisplay(message); - } else { - messageWasDismissed(message, true); - } - } catch (JSONException e) { - e.printStackTrace(); - } - } - }); - } - - @NonNull - String taggedHTMLString(@NonNull String untaggedString) { - String tagsDict = userTagsString; - String tagScript = LIQUID_TAG_SCRIPT; - return untaggedString + String.format(tagScript, tagsDict); - } - - void displayPreviewMessage(@NonNull String previewUUID) { - inAppMessageShowing = true; - - final OSInAppMessageInternal message = new OSInAppMessageInternal(true); - getTagsForLiquidTemplating(message, true); - - inAppMessageRepository.getIAMPreviewData(OneSignal.appId, previewUUID, new OSInAppMessageRepository.OSInAppMessageRequestResponse() { - @Override - public void onSuccess(String response) { - try { - JSONObject jsonResponse = new JSONObject(response); - OSInAppMessageContent content = parseMessageContentData(jsonResponse, message); - if (content.getContentHtml() == null) { - logger.debug("displayPreviewMessage:OnSuccess: No HTML retrieved from loadMessageContent"); - return; - } - if (waitForTags) { - pendingMessageContent = content; - return; - } - onMessageWillDisplay(message); - content.setContentHtml(taggedHTMLString(content.getContentHtml())); - WebViewManager.showMessageContent(message, content); - } catch (JSONException e) { - e.printStackTrace(); - } - } - - @Override - public void onFailure(String response) { - dismissCurrentMessage(null); - } - }); - } - - /** - * Remove IAMs that the last display time was six month ago - * 1. Query for all old message ids and old clicked click ids - * 2. Delete old IAMs from SQL - * 3. Use queried data to clean SharedPreferences - */ - void cleanCachedInAppMessages() { - Runnable cleanCachedIAMRunnable = new BackgroundRunnable() { - @Override - public void run() { - super.run(); - - inAppMessageRepository.cleanCachedInAppMessages(); - } - }; - - runRunnableOnThread(cleanCachedIAMRunnable, OS_IAM_DB_ACCESS); - } - - /** - * Part of redisplay logic - *

- * Will update redisplay messages depending on dynamic triggers before setDataForRedisplay is called. - * @see OSInAppMessageController#setDataForRedisplay(OSInAppMessageInternal) - * - * We can't depend only on messageTriggerConditionChanged, due to trigger evaluation to true before scheduling - * @see OSInAppMessageController#messageTriggerConditionChanged() - */ - @Override - public void messageDynamicTriggerCompleted(String triggerId) { - logger.debug("messageDynamicTriggerCompleted called with triggerId: " + triggerId); - Set triggerIds = new HashSet<>(); - triggerIds.add(triggerId); - makeRedisplayMessagesAvailableWithTriggers(triggerIds); - } - - /** - * Dynamic trigger logic - *

- * This will re evaluate messages due to dynamic triggers evaluating to true potentially - * - * @see OSInAppMessageController#setDataForRedisplay(OSInAppMessageInternal) - */ - @Override - public void messageTriggerConditionChanged() { - logger.debug("messageTriggerConditionChanged called"); - - // This method is called when a time-based trigger timer fires, meaning the message can - // probably be shown now. So the current message conditions should be re-evaluated - evaluateInAppMessages(); - } - - /** - * If this method is called a system condition has changed to success - * - Keyboard is down - * - No DialogFragment visible - * - Activity is on focus, this mean no prompt permissions visible - */ - @Override - public void systemConditionChanged() { - attemptToShowInAppMessage(); - } - - /** - * Part of redisplay logic - *

- * Make all messages with redisplay available if: - * - Already displayed - * - At least one Trigger has changed - */ - private void makeRedisplayMessagesAvailableWithTriggers(Collection newTriggersKeys) { - for (OSInAppMessageInternal message : messages) { - if (!message.isTriggerChanged() && redisplayedInAppMessages.contains(message) && - triggerController.isTriggerOnMessage(message, newTriggersKeys)) { - logger.debug("Trigger changed for message: " + message.toString()); - message.setTriggerChanged(true); - } - } - } - - private void checkRedisplayMessagesAndEvaluate(Collection newTriggersKeys) { - makeRedisplayMessagesAvailableWithTriggers(newTriggersKeys); - evaluateInAppMessages(); - } - - /** - * Trigger logic - *

- * These methods mostly pass data to the Trigger Controller, but also cause the SDK to - * re-evaluate messages to see if we should display/redisplay a message now that the trigger - * conditions have changed. - */ - void addTriggers(@NonNull final Map newTriggers) { - logger.debug("Triggers added: " + newTriggers.toString()); - triggerController.addTriggers(newTriggers); - - if (shouldRunTaskThroughQueue()) - taskController.addTaskToQueue(new Runnable() { - @Override - public void run() { - logger.debug("Delaying addTriggers due to redisplay data not retrieved yet"); - checkRedisplayMessagesAndEvaluate(newTriggers.keySet()); - } - }); - else - checkRedisplayMessagesAndEvaluate(newTriggers.keySet()); - } - - void removeTriggersForKeys(final Collection keys) { - logger.debug("Triggers key to remove: " + keys.toString()); - triggerController.removeTriggersForKeys(keys); - - if (shouldRunTaskThroughQueue()) - taskController.addTaskToQueue(new Runnable() { - @Override - public void run() { - logger.debug("Delaying removeTriggersForKeys due to redisplay data not retrieved yet"); - checkRedisplayMessagesAndEvaluate(keys); - } - }); - else - checkRedisplayMessagesAndEvaluate(keys); - } - - Map getTriggers() { - return new HashMap<>(triggerController.getTriggers()); - } - - boolean inAppMessagingEnabled() { - return inAppMessagingEnabled; - } - - void setInAppMessagingEnabled(boolean enabled) { - inAppMessagingEnabled = enabled; - if (enabled) - evaluateInAppMessages(); - } - - @Nullable - Object getTriggerValue(String key) { - return triggerController.getTriggerValue(key); - } - - @NonNull - public ArrayList getInAppMessageDisplayQueue() { - return messageDisplayQueue; - } - - // Method for testing purposes - @NonNull - public List getRedisplayedInAppMessages() { - return redisplayedInAppMessages; - } -} diff --git a/OneSignalSDK/onesignal/src/main/java/com/onesignal/OSNotificationRestoreWorkManager.java b/OneSignalSDK/onesignal/src/main/java/com/onesignal/OSNotificationRestoreWorkManager.java deleted file mode 100644 index 3539ca9c01..0000000000 --- a/OneSignalSDK/onesignal/src/main/java/com/onesignal/OSNotificationRestoreWorkManager.java +++ /dev/null @@ -1,170 +0,0 @@ -package com.onesignal; - -import android.content.Context; -import android.database.Cursor; -import android.os.Build; -import android.service.notification.StatusBarNotification; -import android.text.TextUtils; - -import androidx.annotation.NonNull; -import androidx.work.ExistingWorkPolicy; -import androidx.work.OneTimeWorkRequest; -import androidx.work.Worker; -import androidx.work.WorkerParameters; - -import java.util.ArrayList; -import java.util.concurrent.TimeUnit; - -class OSNotificationRestoreWorkManager { - - static final String[] COLUMNS_FOR_RESTORE = { - OneSignalDbContract.NotificationTable.COLUMN_NAME_NOTIFICATION_ID, - OneSignalDbContract.NotificationTable.COLUMN_NAME_ANDROID_NOTIFICATION_ID, - OneSignalDbContract.NotificationTable.COLUMN_NAME_FULL_DATA, - OneSignalDbContract.NotificationTable.COLUMN_NAME_CREATED_TIME - }; - - // Delay to prevent logcat messages and possibly skipping some notifications - // This prevents the following error; - // E/NotificationService: Package enqueue rate is 10.56985. Shedding events. package=#### - private static final int DELAY_BETWEEN_NOTIFICATION_RESTORES_MS = 200; - private static final String NOTIFICATION_RESTORE_WORKER_IDENTIFIER = NotificationRestoreWorker.class.getCanonicalName(); - - static final int DEFAULT_TTL_IF_NOT_IN_PAYLOAD = 259_200; - - // Notifications will never be force removed when the app's process is running, - // so we only need to restore at most once per cold start of the app. - public static boolean restored; - - public static void beginEnqueueingWork(Context context, boolean shouldDelay) { - // When boot or upgrade, add a 15 second delay to alleviate app doing to much work all at once - int restoreDelayInSeconds = shouldDelay ? 15 : 0; - - OneTimeWorkRequest workRequest = new OneTimeWorkRequest.Builder(NotificationRestoreWorker.class) - .setInitialDelay(restoreDelayInSeconds, TimeUnit.SECONDS) - .build(); - - OSWorkManagerHelper.getInstance(context) - .enqueueUniqueWork(NOTIFICATION_RESTORE_WORKER_IDENTIFIER, ExistingWorkPolicy.KEEP, workRequest); - } - - public static class NotificationRestoreWorker extends Worker { - - public NotificationRestoreWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) { - super(context, workerParams); - } - - @NonNull - @Override - public Result doWork() { - Context context = getApplicationContext(); - - if (OneSignal.appContext == null) - OneSignal.initWithContext(context); - - if (!OSUtils.areNotificationsEnabled(context)) - return Result.failure(); - - if (restored) - return Result.failure(); - restored = true; - - OneSignal.Log(OneSignal.LOG_LEVEL.INFO, "Restoring notifications"); - - OneSignalDbHelper dbHelper = OneSignalDbHelper.getInstance(context); - - StringBuilder dbQuerySelection = OneSignalDbHelper.recentUninteractedWithNotificationsWhere(); - skipVisibleNotifications(context, dbQuerySelection); - - queryAndRestoreNotificationsAndBadgeCount(context, dbHelper, dbQuerySelection); - - return Result.success(); - } - - } - - private static void queryAndRestoreNotificationsAndBadgeCount( - Context context, - OneSignalDbHelper dbHelper, - StringBuilder dbQuerySelection) { - - OneSignal.Log(OneSignal.LOG_LEVEL.INFO, - "Querying DB for notifications to restore: " + dbQuerySelection.toString()); - - Cursor cursor = null; - try { - cursor = dbHelper.query( - OneSignalDbContract.NotificationTable.TABLE_NAME, - COLUMNS_FOR_RESTORE, - dbQuerySelection.toString(), - null, - null, // group by - null, // filter by row groups - OneSignalDbContract.NotificationTable._ID + " DESC", // sort order, new to old - NotificationLimitManager.MAX_NUMBER_OF_NOTIFICATIONS_STR // limit - ); - showNotificationsFromCursor(context, cursor, DELAY_BETWEEN_NOTIFICATION_RESTORES_MS); - BadgeCountUpdater.update(dbHelper, context); - } catch (Throwable t) { - OneSignal.Log(OneSignal.LOG_LEVEL.ERROR, "Error restoring notification records! ", t); - } finally { - if (cursor != null && !cursor.isClosed()) - cursor.close(); - } - } - - /** - * Retrieve the list of notifications that are currently in the shade - * this is used to prevent notifications from being restored twice in M and newer. - * This is important mostly for Android O as they can't be redisplayed in a silent way unless - * they are displayed under a different channel which isn't ideal. - * For pre-O devices this still have the benefit of being more efficient - */ - private static void skipVisibleNotifications(Context context, StringBuilder dbQuerySelection) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) - return; - - StatusBarNotification[] activeNotifs = OneSignalNotificationManager.getActiveNotifications(context); - if (activeNotifs.length == 0) - return; - - ArrayList activeNotifIds = new ArrayList<>(); - for (StatusBarNotification activeNotif : activeNotifs) - activeNotifIds.add(activeNotif.getId()); - - dbQuerySelection - .append(" AND " + OneSignalDbContract.NotificationTable.COLUMN_NAME_ANDROID_NOTIFICATION_ID + " NOT IN (") - .append(TextUtils.join(",", activeNotifIds)) - .append(")"); - } - - /** - * Restores a set of notifications back to the notification shade based on an SQL cursor - * @param cursor - Source cursor to generate notifications from - * @param delay - Delay to slow down process to ensure we don't spike CPU and I/O on the device - */ - static void showNotificationsFromCursor(Context context, Cursor cursor, int delay) { - if (!cursor.moveToFirst()) - return; - - do { - String osNotificationId = cursor.getString(cursor.getColumnIndex(OneSignalDbContract.NotificationTable.COLUMN_NAME_NOTIFICATION_ID)); - int existingId = cursor.getInt(cursor.getColumnIndex(OneSignalDbContract.NotificationTable.COLUMN_NAME_ANDROID_NOTIFICATION_ID)); - String fullData = cursor.getString(cursor.getColumnIndex(OneSignalDbContract.NotificationTable.COLUMN_NAME_FULL_DATA)); - long dateTime = cursor.getLong(cursor.getColumnIndex(OneSignalDbContract.NotificationTable.COLUMN_NAME_CREATED_TIME)); - - OSNotificationWorkManager.beginEnqueueingWork( - context, - osNotificationId, - existingId, - fullData, - dateTime, - true, - false - ); - - if (delay > 0) - OSUtils.sleep(delay); - } while (cursor.moveToNext()); - } -} diff --git a/OneSignalSDK/onesignal/src/main/java/com/onesignal/OSNotificationWorkManager.java b/OneSignalSDK/onesignal/src/main/java/com/onesignal/OSNotificationWorkManager.java deleted file mode 100644 index 199db9faad..0000000000 --- a/OneSignalSDK/onesignal/src/main/java/com/onesignal/OSNotificationWorkManager.java +++ /dev/null @@ -1,123 +0,0 @@ -package com.onesignal; - -import android.content.Context; - -import androidx.annotation.NonNull; -import androidx.work.Data; -import androidx.work.ExistingWorkPolicy; -import androidx.work.OneTimeWorkRequest; -import androidx.work.Worker; -import androidx.work.WorkerParameters; - -import org.json.JSONException; -import org.json.JSONObject; - -import java.util.Set; - -import static com.onesignal.OSUtils.isStringNotEmpty; - -class OSNotificationWorkManager { - - private static final String ANDROID_NOTIF_ID_WORKER_DATA_PARAM = "android_notif_id"; - private static final String JSON_PAYLOAD_WORKER_DATA_PARAM = "json_payload"; - private static final String TIMESTAMP_WORKER_DATA_PARAM = "timestamp"; - private static final String IS_RESTORING_WORKER_DATA_PARAM = "is_restoring"; - - private static Set notificationIds = OSUtils.newConcurrentSet(); - - static boolean addNotificationIdProcessed(String osNotificationId) { - // Duplicate control - // Keep in memory on going processed notifications, to avoid fast duplicates that already finished work process but are not completed yet - // enqueueUniqueWork might not be enough, if the work already finished then the duplicate notification work might be queued again - if (isStringNotEmpty(osNotificationId)) { - if (notificationIds.contains(osNotificationId)) { - OneSignal.Log(OneSignal.LOG_LEVEL.DEBUG, "OSNotificationWorkManager notification with notificationId: " + osNotificationId + " already queued"); - return false; - } else { - notificationIds.add(osNotificationId); - } - } - - return true; - } - - static void removeNotificationIdProcessed(String osNotificationId) { - if (isStringNotEmpty(osNotificationId)) { - notificationIds.remove(osNotificationId); - } - } - - static void beginEnqueueingWork(Context context, String osNotificationId, int androidNotificationId, String jsonPayload, long timestamp, - boolean isRestoring, boolean isHighPriority) { - // TODO: Need to figure out how to implement the isHighPriority param - Data inputData = new Data.Builder() - .putInt(ANDROID_NOTIF_ID_WORKER_DATA_PARAM, androidNotificationId) - .putString(JSON_PAYLOAD_WORKER_DATA_PARAM, jsonPayload) - .putLong(TIMESTAMP_WORKER_DATA_PARAM, timestamp) - .putBoolean(IS_RESTORING_WORKER_DATA_PARAM, isRestoring) - .build(); - - OneTimeWorkRequest workRequest = new OneTimeWorkRequest.Builder(NotificationWorker.class) - .setInputData(inputData) - .build(); - - OneSignal.Log(OneSignal.LOG_LEVEL.DEBUG, "OSNotificationWorkManager enqueueing notification work with notificationId: " + osNotificationId + " and jsonPayload: " + jsonPayload); - - OSWorkManagerHelper.getInstance(context). - enqueueUniqueWork(osNotificationId, ExistingWorkPolicy.KEEP, workRequest); - } - - public static class NotificationWorker extends Worker { - - public NotificationWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) { - super(context, workerParams); - } - - @NonNull - @Override - public Result doWork() { - Data inputData = getInputData(); - try { - OneSignal.onesignalLog(OneSignal.LOG_LEVEL.DEBUG, "NotificationWorker running doWork with data: " + inputData); - - int androidNotificationId = inputData.getInt(ANDROID_NOTIF_ID_WORKER_DATA_PARAM, 0); - JSONObject jsonPayload = new JSONObject(inputData.getString(JSON_PAYLOAD_WORKER_DATA_PARAM)); - long timestamp = inputData.getLong(TIMESTAMP_WORKER_DATA_PARAM, System.currentTimeMillis() / 1000L); - boolean isRestoring = inputData.getBoolean(IS_RESTORING_WORKER_DATA_PARAM, false); - - processNotificationData( - getApplicationContext(), - androidNotificationId, - jsonPayload, - isRestoring, - timestamp); - } catch (JSONException e) { - OneSignal.onesignalLog(OneSignal.LOG_LEVEL.ERROR, "Error occurred doing work for job with id: " + getId().toString()); - e.printStackTrace(); - return Result.failure(); - } - return Result.success(); - } - - private void processNotificationData(Context context, int androidNotificationId, JSONObject jsonPayload, - boolean isRestoring, Long timestamp) { - OSNotification notification = new OSNotification(null, jsonPayload, androidNotificationId); - OSNotificationController controller = new OSNotificationController(context, notification, jsonPayload, isRestoring, true, timestamp); - OSNotificationReceivedEvent notificationReceived = new OSNotificationReceivedEvent(controller, notification); - - if (OneSignal.remoteNotificationReceivedHandler != null) - try { - OneSignal.remoteNotificationReceivedHandler.remoteNotificationReceived(context, notificationReceived); - } catch (Throwable t) { - OneSignal.Log(OneSignal.LOG_LEVEL.ERROR, "remoteNotificationReceived throw an exception. Displaying normal OneSignal notification.", t); - notificationReceived.complete(notification); - - throw t; - } - else { - OneSignal.Log(OneSignal.LOG_LEVEL.WARN, "remoteNotificationReceivedHandler not setup, displaying normal OneSignal notification"); - notificationReceived.complete(notification); - } - } - } -} diff --git a/OneSignalSDK/onesignal/src/main/java/com/onesignal/OSObservable.java b/OneSignalSDK/onesignal/src/main/java/com/onesignal/OSObservable.java deleted file mode 100644 index 9376c97ae0..0000000000 --- a/OneSignalSDK/onesignal/src/main/java/com/onesignal/OSObservable.java +++ /dev/null @@ -1,116 +0,0 @@ -/** - * Modified MIT License - * - * Copyright 2017 OneSignal - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * 1. The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * 2. All copies of substantial portions of the Software may only be used in connection - * with services provided by OneSignal. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ - -package com.onesignal; - -import java.lang.ref.WeakReference; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.List; - -class OSObservable { - private String methodName; - private List observers; - private boolean fireOnMainThread; - - OSObservable(String methodName, boolean fireOnMainThread) { - this.methodName = methodName; - this.fireOnMainThread = fireOnMainThread; - observers = new ArrayList<>(); - } - - void addObserver(ObserverType observer) { - observers.add(new WeakReference<>(observer)); - } - - void addObserverStrong(ObserverType observer){ - observers.add(observer); - } - - void removeObserver(ObserverType observer) { - for(int i = 0; i < observers.size(); i++) { - Object anObserver = ((WeakReference)observers.get(i)).get(); - if (anObserver == null) continue; - if (anObserver.equals(observer)) { - observers.remove(i); - break; - } - } - } - - boolean notifyChange(final StateType state) { - boolean notified = false; - - for(Object observer : observers) { - final Object strongRefObserver; - if (observer instanceof WeakReference) - strongRefObserver = ((WeakReference)observer).get(); - else - strongRefObserver = observer; - - if (strongRefObserver != null) { - Class clazz = strongRefObserver.getClass(); - try { - final Method method = clazz.getDeclaredMethod(methodName, state.getClass()); - method.setAccessible(true); - if (fireOnMainThread) { - CallbackThreadManager.Companion.runOnPreferred( - new Runnable() { - @Override - public void run() { - try { - method.invoke(strongRefObserver, state); - } catch (IllegalAccessException e) { - e.printStackTrace(); - } catch (InvocationTargetException e) { - e.printStackTrace(); - } - } - } - ); - } - else { - try { - method.invoke(strongRefObserver, state); - } catch (IllegalAccessException e) { - e.printStackTrace(); - } catch (InvocationTargetException e) { - e.printStackTrace(); - } - } - - notified = true; - } catch (NoSuchMethodException e) { - e.printStackTrace(); - } - } - } - - return notified; - } -} diff --git a/OneSignalSDK/onesignal/src/main/java/com/onesignal/OSReceiveReceiptController.java b/OneSignalSDK/onesignal/src/main/java/com/onesignal/OSReceiveReceiptController.java deleted file mode 100644 index e6e0e9cae1..0000000000 --- a/OneSignalSDK/onesignal/src/main/java/com/onesignal/OSReceiveReceiptController.java +++ /dev/null @@ -1,140 +0,0 @@ -/** - * Modified MIT License - *

- * Copyright 2019 OneSignal - *

- * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - *

- * 1. The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - *

- * 2. All copies of substantial portions of the Software may only be used in connection - * with services provided by OneSignal. - *

- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ - -package com.onesignal; - -import android.content.Context; - -import androidx.annotation.NonNull; -import androidx.work.Constraints; -import androidx.work.Data; -import androidx.work.ExistingWorkPolicy; -import androidx.work.NetworkType; -import androidx.work.OneTimeWorkRequest; -import androidx.work.Worker; -import androidx.work.WorkerParameters; - -import java.util.concurrent.TimeUnit; - -class OSReceiveReceiptController { - - private static final String OS_NOTIFICATION_ID = "os_notification_id"; - private int minDelay = 0; - private int maxDelay = 25; - - private final OSRemoteParamController remoteParamController; - private static OSReceiveReceiptController sInstance; - - private OSReceiveReceiptController() { - this.remoteParamController = OneSignal.getRemoteParamController(); - } - - synchronized public static OSReceiveReceiptController getInstance() { - if (sInstance == null) - sInstance = new OSReceiveReceiptController(); - return sInstance; - } - - void beginEnqueueingWork(Context context, String osNotificationId) { - if (!remoteParamController.isReceiveReceiptEnabled()) { - OneSignal.Log(OneSignal.LOG_LEVEL.DEBUG, "sendReceiveReceipt disabled"); - return; - } - - int delay = OSUtils.getRandomDelay(minDelay, maxDelay); - - Data inputData = new Data.Builder() - .putString(OS_NOTIFICATION_ID, osNotificationId) - .build(); - - Constraints constraints = buildConstraints(); - - OneTimeWorkRequest workRequest = new OneTimeWorkRequest.Builder(ReceiveReceiptWorker.class) - .setConstraints(constraints) - .setInitialDelay(delay, TimeUnit.SECONDS) - .setInputData(inputData) - .build(); - - OneSignal.Log(OneSignal.LOG_LEVEL.DEBUG, "OSReceiveReceiptController enqueueing send receive receipt work with notificationId: " + osNotificationId + " and delay: " + delay + " seconds"); - - OSWorkManagerHelper.getInstance(context) - .enqueueUniqueWork(osNotificationId + "_receive_receipt", ExistingWorkPolicy.KEEP, workRequest); - } - - Constraints buildConstraints() { - return new Constraints.Builder() - .setRequiredNetworkType(NetworkType.CONNECTED) - .build(); - } - - public static class ReceiveReceiptWorker extends Worker { - - public ReceiveReceiptWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) { - super(context, workerParams); - } - - @NonNull - @Override - public Result doWork() { - Data inputData = getInputData(); - String notificationId = inputData.getString(OS_NOTIFICATION_ID); - - sendReceiveReceipt(notificationId); - - return Result.success(); - } - - void sendReceiveReceipt(@NonNull final String notificationId) { - final String appId = OneSignal.appId == null || OneSignal.appId.isEmpty() ? OneSignal.getSavedAppId() : OneSignal.appId; - final String playerId = OneSignal.getUserId(); - Integer deviceType = null; - - OSReceiveReceiptRepository repository = new OSReceiveReceiptRepository(); - - try { - deviceType = new OSUtils().getDeviceType(); - } catch (NullPointerException e) { - e.printStackTrace(); - } - - final Integer finalDeviceType = deviceType; - OneSignal.Log(OneSignal.LOG_LEVEL.DEBUG, "ReceiveReceiptWorker: Device Type is: " + finalDeviceType); - - repository.sendReceiveReceipt(appId, playerId, finalDeviceType, notificationId, new OneSignalRestClient.ResponseHandler() { - @Override - void onSuccess(String response) { - OneSignal.Log(OneSignal.LOG_LEVEL.DEBUG, "Receive receipt sent for notificationID: " + notificationId); - } - - @Override - void onFailure(int statusCode, String response, Throwable throwable) { - OneSignal.Log(OneSignal.LOG_LEVEL.ERROR, "Receive receipt failed with statusCode: " + statusCode + " response: " + response); - } - }); - } - } -} diff --git a/OneSignalSDK/onesignal/src/main/java/com/onesignal/OSUtils.java b/OneSignalSDK/onesignal/src/main/java/com/onesignal/OSUtils.java deleted file mode 100644 index 33e5b94105..0000000000 --- a/OneSignalSDK/onesignal/src/main/java/com/onesignal/OSUtils.java +++ /dev/null @@ -1,684 +0,0 @@ -/** - * Modified MIT License - * - * Copyright 2017 OneSignal - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * 1. The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * 2. All copies of substantial portions of the Software may only be used in connection - * with services provided by OneSignal. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ - -package com.onesignal; - -import android.annotation.TargetApi; -import android.app.Activity; -import android.content.ContentResolver; -import android.content.Context; -import android.content.Intent; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager; -import android.content.res.Resources; -import android.net.ConnectivityManager; -import android.net.NetworkInfo; -import android.net.Uri; -import android.os.Build; -import android.os.Bundle; -import android.os.DeadSystemException; -import android.os.Handler; -import android.os.Looper; - -import androidx.annotation.Keep; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.core.app.NotificationManagerCompat; -import android.telephony.TelephonyManager; -import android.text.TextUtils; - -import androidx.legacy.content.WakefulBroadcastReceiver; - -import com.google.android.gms.common.GoogleApiAvailability; -import com.huawei.hms.api.HuaweiApiAvailability; - -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.HashSet; -import java.util.Iterator; -import java.util.Random; -import java.util.Set; -import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; -import java.util.regex.Pattern; - -import static com.onesignal.OneSignal.Log; - -class OSUtils { - - public static final int UNINITIALIZABLE_STATUS = -999; - - public static int MAX_NETWORK_REQUEST_ATTEMPT_COUNT = 3; - static final int[] NO_RETRY_NETWROK_REQUEST_STATUS_CODES = {401, 402, 403, 404, 410}; - - public enum SchemaType { - DATA("data"), - HTTPS("https"), - HTTP("http"), - ; - - private final String text; - - SchemaType(final String text) { - this.text = text; - } - - public static SchemaType fromString(String text) { - for (SchemaType type : SchemaType.values()) { - if (type.text.equalsIgnoreCase(text)) { - return type; - } - } - return null; - } - } - - public static boolean shouldRetryNetworkRequest(int statusCode) { - for (int code : NO_RETRY_NETWROK_REQUEST_STATUS_CODES) - if (statusCode == code) - return false; - - return true; - } - - int initializationChecker(Context context, String oneSignalAppId) { - int subscribableStatus = 1; - int deviceType = getDeviceType(); - - try { - //noinspection ResultOfMethodCallIgnored - UUID.fromString(oneSignalAppId); - } catch (Throwable t) { - Log(OneSignal.LOG_LEVEL.FATAL, "OneSignal AppId format is invalid.\nExample: 'b2f7f966-d8cc-11e4-bed1-df8f05be55ba'\n", t); - return UNINITIALIZABLE_STATUS; - } - - if ("b2f7f966-d8cc-11e4-bed1-df8f05be55ba".equals(oneSignalAppId) || - "5eb5a37e-b458-11e3-ac11-000c2940e62c".equals(oneSignalAppId)) - Log(OneSignal.LOG_LEVEL.ERROR, "OneSignal Example AppID detected, please update to your app's id found on OneSignal.com"); - - if (deviceType == UserState.DEVICE_TYPE_ANDROID) { - Integer pushErrorType = checkForGooglePushLibrary(); - if (pushErrorType != null) - subscribableStatus = pushErrorType; - } - - Integer supportErrorType = checkAndroidSupportLibrary(context); - if (supportErrorType != null) - subscribableStatus = supportErrorType; - - return subscribableStatus; - } - - // Interim method that works around Proguard's overly aggressive assumenosideeffects which - // ignores keep rules. - // This is specifically designed to address Proguard removing catches for NoClassDefFoundError - // when the config has "-assumenosideeffects" with - // java.lang.Class.getName() & java.lang.Object.getClass(). - // This @Keep annotation is key so this method does not get removed / inlined. - // Addresses issue https://github.com/OneSignal/OneSignal-Android-SDK/issues/1423 - @Keep - private static boolean opaqueHasClass(Class _class) { - return true; - } - - static boolean hasFCMLibrary() { - try { - return opaqueHasClass(com.google.firebase.messaging.FirebaseMessaging.class); - } catch (NoClassDefFoundError e) { - return false; - } - } - - static boolean hasGMSLocationLibrary() { - try { - return opaqueHasClass(com.google.android.gms.location.LocationListener.class); - } catch (NoClassDefFoundError e) { - return false; - } - } - - private static boolean hasHMSAvailabilityLibrary() { - try { - return opaqueHasClass(com.huawei.hms.api.HuaweiApiAvailability.class); - } catch (NoClassDefFoundError e) { - return false; - } - } - - private static boolean hasHMSPushKitLibrary() { - try { - return opaqueHasClass(com.huawei.hms.aaid.HmsInstanceId.class); - } catch (NoClassDefFoundError e) { - return false; - } - } - - private static boolean hasHMSAGConnectLibrary() { - try { - return opaqueHasClass(com.huawei.agconnect.config.AGConnectServicesConfig.class); - } catch (NoClassDefFoundError e) { - return false; - } - } - - static boolean hasHMSLocationLibrary() { - try { - return opaqueHasClass(com.huawei.hms.location.LocationCallback.class); - } catch (NoClassDefFoundError e) { - return false; - } - } - - static boolean hasAllHMSLibrariesForPushKit() { - // NOTE: hasHMSAvailabilityLibrary technically is not required, - // just used as recommend way to detect if "HMS Core" app exists and is enabled - return hasHMSAGConnectLibrary() && hasHMSPushKitLibrary(); - } - - Integer checkForGooglePushLibrary() { - boolean hasFCMLibrary = hasFCMLibrary(); - - if (!hasFCMLibrary) { - Log(OneSignal.LOG_LEVEL.FATAL, "The Firebase FCM library is missing! Please make sure to include it in your project."); - return UserState.PUSH_STATUS_MISSING_FIREBASE_FCM_LIBRARY; - } - - return null; - } - - private static boolean hasWakefulBroadcastReceiver() { - try { - // noinspection ConstantConditions - return WakefulBroadcastReceiver.class != null; - } catch (Throwable e) { - return false; - } - } - - private static boolean hasNotificationManagerCompat() { - try { - // noinspection ConstantConditions - return androidx.core.app.NotificationManagerCompat.class != null; - } catch (Throwable e) { - return false; - } - } - - private static boolean hasJobIntentService() { - try { - // noinspection ConstantConditions - return androidx.core.app.JobIntentService.class != null; - } catch (Throwable e) { - return false; - } - } - - private Integer checkAndroidSupportLibrary(Context context) { - boolean hasWakefulBroadcastReceiver = hasWakefulBroadcastReceiver(); - boolean hasNotificationManagerCompat = hasNotificationManagerCompat(); - - if (!hasWakefulBroadcastReceiver && !hasNotificationManagerCompat) { - Log(OneSignal.LOG_LEVEL.FATAL, "Could not find the Android Support Library. Please make sure it has been correctly added to your project."); - return UserState.PUSH_STATUS_MISSING_ANDROID_SUPPORT_LIBRARY; - } - - if (!hasWakefulBroadcastReceiver || !hasNotificationManagerCompat) { - Log(OneSignal.LOG_LEVEL.FATAL, "The included Android Support Library is to old or incomplete. Please update to the 26.0.0 revision or newer."); - return UserState.PUSH_STATUS_OUTDATED_ANDROID_SUPPORT_LIBRARY; - } - - // If running on Android O and targeting O we need version 26.0.0 for - // the new compat NotificationCompat.Builder constructor. - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O - && getTargetSdkVersion(context) >= Build.VERSION_CODES.O) { - // Class was added in 26.0.0-beta2 - if (!hasJobIntentService()) { - Log(OneSignal.LOG_LEVEL.FATAL, "The included Android Support Library is to old or incomplete. Please update to the 26.0.0 revision or newer."); - return UserState.PUSH_STATUS_OUTDATED_ANDROID_SUPPORT_LIBRARY; - } - } - - return null; - } - - private static boolean packageInstalledAndEnabled(@NonNull String packageName) { - GetPackageInfoResult result = - PackageInfoHelper.Companion.getInfo( - OneSignal.appContext, - packageName, - PackageManager.GET_META_DATA - ); - if (!result.getSuccessful()) { - return false; - } - - PackageInfo info = result.getPackageInfo(); - if (info == null) { - return false; - } - - return info.applicationInfo.enabled; - } - - // TODO: Maybe able to switch to GoogleApiAvailability.isGooglePlayServicesAvailable to simplify - // However before doing so we need to test with an old version of the "Google Play services" - // on the device to make sure it would still be counted as "SUCCESS". - // Or if we get back "SERVICE_VERSION_UPDATE_REQUIRED" then we may want to count that as successful too. - static boolean isGMSInstalledAndEnabled() { - return packageInstalledAndEnabled(GoogleApiAvailability.GOOGLE_PLAY_SERVICES_PACKAGE); - } - - private static final int HMS_AVAILABLE_SUCCESSFUL = 0; - @TargetApi(24) - private static boolean isHMSCoreInstalledAndEnabled() { - HuaweiApiAvailability availability = HuaweiApiAvailability.getInstance(); - try { - return availability.isHuaweiMobileServicesAvailable(OneSignal.appContext) == HMS_AVAILABLE_SUCCESSFUL; - } catch (Exception e) { - // Suppressing DeadSystemException as the app is already dying for - // another reason and allowing this exception to bubble up would - // create a red herring for app developers. We still re-throw - // others, as we don't want to silently hide other issues. - if (!(e instanceof DeadSystemException)) { - throw e; - } - return false; - } - } - - private static final String HMS_CORE_SERVICES_PACKAGE = "com.huawei.hwid"; // = HuaweiApiAvailability.SERVICES_PACKAGE - // HuaweiApiAvailability is the recommend way to detect if "HMS Core" is available but this fallback - // works even if the app developer doesn't include any HMS libraries in their app. - private static boolean isHMSCoreInstalledAndEnabledFallback() { - return packageInstalledAndEnabled(HMS_CORE_SERVICES_PACKAGE); - } - - private boolean supportsADM() { - try { - // Class only available on the FireOS and only when the following is in the AndroidManifest.xml. - // - Class.forName("com.amazon.device.messaging.ADM"); - return true; - } catch (ClassNotFoundException e) { - return false; - } - } - - private boolean supportsHMS() { - // 1. App should have the HMSAvailability for best detection and must have PushKit libraries - if (!hasHMSAvailabilityLibrary() || !hasAllHMSLibrariesForPushKit()) - return false; - - // 2. Device must have HMS Core installed and enabled - return isHMSCoreInstalledAndEnabled(); - } - - private boolean supportsGooglePush() { - // 1. If app does not have the FCM library it won't support Google push - if (!hasFCMLibrary()) - return false; - - // 2. "Google Play services" must be installed and enabled - return isGMSInstalledAndEnabled(); - } - - /** - * Device type is determined by the push channel(s) the device supports. - * Since a player_id can only support one we attempt to select the one that is native to the device - * 1. ADM - This can NOT be side loaded on the device, if it has it then it is native - * 2. FCM - If this is available then most likely native. - * - Prefer over HMS as FCM has more features on older Huawei devices. - * 3. HMS - Huawei devices only. - * - New 2020 Huawei devices don't have FCM support, HMS only - * - Technically works for non-Huawei devices if you side load the Huawei AppGallery. - * i. "Notification Message" pushes are very bare bones. (title + body) - * ii. "Data Message" works as expected. - */ - int getDeviceType() { - if (supportsADM()) - return UserState.DEVICE_TYPE_FIREOS; - - if (supportsGooglePush()) - return UserState.DEVICE_TYPE_ANDROID; - - // Some Huawei devices have both FCM & HMS support, but prefer FCM (Google push) over HMS - if (supportsHMS()) - return UserState.DEVICE_TYPE_HUAWEI; - - // Start - Fallback logic - // Libraries in the app (Google:FCM, HMS:PushKit) + Device may not have a valid combo - // Example: App with only the FCM library in it and a Huawei device with only HMS Core - if (isGMSInstalledAndEnabled()) - return UserState.DEVICE_TYPE_ANDROID; - - if (isHMSCoreInstalledAndEnabledFallback()) - return UserState.DEVICE_TYPE_HUAWEI; - - // Last fallback - // Fallback to device_type 1 (Android) if there are no supported push channels on the device - return UserState.DEVICE_TYPE_ANDROID; - } - - static boolean isAndroidDeviceType() { - return new OSUtils().getDeviceType() == UserState.DEVICE_TYPE_ANDROID; - } - - static boolean isFireOSDeviceType() { - return new OSUtils().getDeviceType() == UserState.DEVICE_TYPE_FIREOS; - } - - static boolean isHuaweiDeviceType() { - return new OSUtils().getDeviceType() == UserState.DEVICE_TYPE_HUAWEI; - } - - Integer getNetType () { - ConnectivityManager cm = (ConnectivityManager) OneSignal.appContext.getSystemService(Context.CONNECTIVITY_SERVICE); - NetworkInfo netInfo = cm.getActiveNetworkInfo(); - - if (netInfo != null) { - int networkType = netInfo.getType(); - if (networkType == ConnectivityManager.TYPE_WIFI || networkType == ConnectivityManager.TYPE_ETHERNET) - return 0; - return 1; - } - - return null; - } - - String getCarrierName() { - try { - TelephonyManager manager = (TelephonyManager) OneSignal.appContext.getSystemService(Context.TELEPHONY_SERVICE); - // May throw even though it's not in noted in the Android docs. - // Issue #427 - String carrierName = manager.getNetworkOperatorName(); - return "".equals(carrierName) ? null : carrierName; - } catch(Throwable t) { - t.printStackTrace(); - return null; - } - } - - static Bundle getManifestMetaBundle(Context context) { - ApplicationInfo ai = ApplicationInfoHelper.Companion.getInfo(context); - if (ai == null) { - return null; - } - return ai.metaData; - } - - static boolean getManifestMetaBoolean(Context context, String metaName) { - Bundle bundle = getManifestMetaBundle(context); - if (bundle != null) { - return bundle.getBoolean(metaName); - } - - return false; - } - - static String getManifestMeta(Context context, String metaName) { - Bundle bundle = getManifestMetaBundle(context); - if (bundle != null) { - return bundle.getString(metaName); - } - - return null; - } - - static String getResourceString(Context context, String key, String defaultStr) { - Resources resources = context.getResources(); - int bodyResId = resources.getIdentifier(key, "string", context.getPackageName()); - if (bodyResId != 0) - return resources.getString(bodyResId); - return defaultStr; - } - - static boolean isValidEmail(String email) { - if (email == null) - return false; - - String emRegex = "^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@((\\[[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\])|(([a-zA-Z\\-0-9]+\\.)+[a-zA-Z]{2,}))$"; - Pattern pattern = Pattern.compile(emRegex); - return pattern.matcher(email).matches(); - } - - static boolean isStringNotEmpty(String body) { - return !TextUtils.isEmpty(body); - } - - // Get the app's permission which will be false if the user disabled notifications for the app - // from Settings > Apps or by long pressing the notifications and selecting block. - // - Detection works on Android 4.4+, requires Android Support v4 Library 24.0.0+ - static boolean areNotificationsEnabled(Context context) { - try { - return NotificationManagerCompat.from(OneSignal.appContext).areNotificationsEnabled(); - } catch (Throwable t) {} - - return true; - } - - static boolean isRunningOnMainThread() { - return Thread.currentThread().equals(Looper.getMainLooper().getThread()); - } - - static void runOnMainUIThread(Runnable runnable) { - if (Looper.getMainLooper().getThread() == Thread.currentThread()) - runnable.run(); - else { - Handler handler = new Handler(Looper.getMainLooper()); - handler.post(runnable); - } - } - - static void runOnMainThreadDelayed(Runnable runnable, int delay) { - Handler handler = new Handler(Looper.getMainLooper()); - handler.postDelayed(runnable, delay); - } - - static int getTargetSdkVersion(Context context) { - ApplicationInfo applicationInfo = ApplicationInfoHelper.Companion.getInfo(context); - if (applicationInfo == null) { - return Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1; - } - return applicationInfo.targetSdkVersion; - } - - static boolean isValidResourceName(String name) { - return (name != null && !name.matches("^[0-9]")); - } - - static Uri getSoundUri(Context context, String sound) { - Resources resources = context.getResources(); - String packageName = context.getPackageName(); - int soundId; - - if (isValidResourceName(sound)) { - soundId = resources.getIdentifier(sound, "raw", packageName); - if (soundId != 0) - return Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE + "://" + packageName + "/" + soundId); - } - - soundId = resources.getIdentifier("onesignal_default_sound", "raw", packageName); - if (soundId != 0) - return Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE + "://" + packageName + "/" + soundId); - - return null; - } - - static long[] parseVibrationPattern(JSONObject fcmBundle) { - try { - Object patternObj = fcmBundle.opt("vib_pt"); - JSONArray jsonVibArray; - if (patternObj instanceof String) - jsonVibArray = new JSONArray((String)patternObj); - else - jsonVibArray = (JSONArray)patternObj; - - long[] longArray = new long[jsonVibArray.length()]; - for (int i = 0; i < jsonVibArray.length(); i++) - longArray[i] = jsonVibArray.optLong(i); - - return longArray; - } catch (JSONException e) {} - - return null; - } - - static void sleep(int ms) { - try { - Thread.sleep(ms); - } catch (InterruptedException e) { - e.printStackTrace(); - } - } - - static void openURLInBrowser(@NonNull String url) { - openURLInBrowser(Uri.parse(url.trim())); - } - - private static void openURLInBrowser(@NonNull Uri uri) { - Intent intent = openURLInBrowserIntent(uri); - OneSignal.appContext.startActivity(intent); - } - - @NonNull - static Intent openURLInBrowserIntent(@NonNull Uri uri) { - SchemaType type = uri.getScheme() != null ? SchemaType.fromString(uri.getScheme()) : null; - if (type == null) { - type = SchemaType.HTTP; - if (!uri.toString().contains("://")) { - uri = Uri.parse("http://" + uri.toString()); - } - } - Intent intent; - switch (type) { - case DATA: - intent = Intent.makeMainSelectorActivity(Intent.ACTION_MAIN, Intent.CATEGORY_APP_BROWSER); - intent.setData(uri); - break; - case HTTPS: - case HTTP: - default: - intent = new Intent(Intent.ACTION_VIEW, uri); - break; - } - intent.addFlags( - Intent.FLAG_ACTIVITY_NEW_TASK - ); - return intent; - } - - // Creates a new Set that supports reads and writes from more than one thread at a time - static Set newConcurrentSet() { - return Collections.newSetFromMap(new ConcurrentHashMap()); - } - - // Creates a new Set from a Set String by converting and iterating a JSONArray - static Set newStringSetFromJSONArray(JSONArray jsonArray) throws JSONException { - Set stringSet = new HashSet<>(); - - for (int i = 0; i < jsonArray.length(); i++) { - stringSet.add(jsonArray.getString(i)); - } - - return stringSet; - } - - static boolean hasConfigChangeFlag(Activity activity, int configChangeFlag) { - boolean hasFlag = false; - try { - int configChanges = activity.getPackageManager().getActivityInfo(activity.getComponentName(), 0).configChanges; - int flagInt = configChanges & configChangeFlag; - hasFlag = flagInt != 0; - } catch (PackageManager.NameNotFoundException e) { - e.printStackTrace(); - } - return hasFlag; - } - - static @NonNull Collection extractStringsFromCollection(@Nullable Collection collection) { - Collection result = new ArrayList<>(); - if (collection == null) - return result; - - for (Object value : collection) { - if (value instanceof String) - result.add((String) value); - } - return result; - } - - static @Nullable Bundle jsonStringToBundle(@NonNull String data) { - try { - JSONObject jsonObject = new JSONObject(data); - Bundle bundle = new Bundle(); - Iterator iterator = jsonObject.keys(); - while (iterator.hasNext()) { - String key = (String)iterator.next(); - String value = jsonObject.getString(key); - bundle.putString(key, value); - } - return bundle; - } catch (JSONException e) { - e.printStackTrace(); - return null; - } - } - - static boolean shouldLogMissingAppIdError(@Nullable String appId) { - if (appId != null) - return false; - - // Wrapper SDKs can't normally call on Application.onCreate so just count this as informational. - Log(OneSignal.LOG_LEVEL.INFO, "OneSignal was not initialized, " + - "ensure to always initialize OneSignal from the onCreate of your Application class."); - return true; - } - - static int getRandomDelay(int minDelay, int maxDelay) { - return new Random().nextInt(maxDelay + 1 - minDelay) + minDelay; - } - - @NonNull - static Throwable getRootCauseThrowable(@NonNull Throwable subjectThrowable) { - Throwable throwable = subjectThrowable; - while (throwable.getCause() != null && throwable.getCause() != throwable) { - throwable = throwable.getCause(); - } - return throwable; - } - - static String getRootCauseMessage(@NonNull Throwable throwable) { - return getRootCauseThrowable(throwable).getMessage(); - } -} diff --git a/OneSignalSDK/onesignal/src/main/java/com/onesignal/OSWorkManagerHelper.kt b/OneSignalSDK/onesignal/src/main/java/com/onesignal/OSWorkManagerHelper.kt deleted file mode 100644 index 05c63ddbe4..0000000000 --- a/OneSignalSDK/onesignal/src/main/java/com/onesignal/OSWorkManagerHelper.kt +++ /dev/null @@ -1,66 +0,0 @@ -/** - * Modified MIT License - *

- * Copyright 2023 OneSignal - *

- * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - *

- * 1. The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - *

- * 2. All copies of substantial portions of the Software may only be used in connection - * with services provided by OneSignal. - *

- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ - -package com.onesignal - -import android.annotation.SuppressLint -import android.content.Context -import androidx.work.Configuration -import androidx.work.WorkManager -import androidx.work.impl.WorkManagerImpl - -object OSWorkManagerHelper { - /** - * Helper method to provide a way to check if WorkManager is initialized in this process. - * - * This is effectively the `WorkManager.isInitialized()` public method introduced in androidx.work:work-*:2.8.0-alpha02. - * Please see https://android-review.googlesource.com/c/platform/frameworks/support/+/1941186. - * - * @return `true` if WorkManager has been initialized in this process. - */ - @SuppressWarnings("deprecation") - @SuppressLint("RestrictedApi") - private fun isInitialized(): Boolean { - return WorkManagerImpl.getInstance() != null - } - - /** - * If there is an instance of WorkManager available, use it. Else, in rare cases, initialize it ourselves. - * - * Calling `WorkManager.getInstance(context)` directly can cause an exception if it is null. - * - * @return an instance of WorkManager - */ - @JvmStatic - @Synchronized - fun getInstance(context: Context): WorkManager { - if (!isInitialized()) { - WorkManager.initialize(context, Configuration.Builder().build()) - } - return WorkManager.getInstance(context) - } -} diff --git a/OneSignalSDK/onesignal/src/main/java/com/onesignal/OneSignal.java b/OneSignalSDK/onesignal/src/main/java/com/onesignal/OneSignal.java deleted file mode 100644 index 051f3ebf68..0000000000 --- a/OneSignalSDK/onesignal/src/main/java/com/onesignal/OneSignal.java +++ /dev/null @@ -1,3496 +0,0 @@ -/** - * Modified MIT License - * - * Copyright 2019 OneSignal - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * 1. The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * 2. All copies of substantial portions of the Software may only be used in connection - * with services provided by OneSignal. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ - -package com.onesignal; - -import android.app.Activity; -import android.app.AlertDialog; -import android.app.Application; -import android.app.NotificationManager; -import android.content.Context; -import android.content.Intent; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageManager; -import android.os.Build; -import android.os.Bundle; -import android.text.TextUtils; -import android.util.Log; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.WorkerThread; -import androidx.core.app.NotificationCompat; - -import com.onesignal.influence.data.OSTrackerFactory; -import com.onesignal.influence.domain.OSInfluence; -import com.onesignal.language.LanguageContext; -import com.onesignal.language.LanguageProviderAppDefined; -import com.onesignal.OneSignalStateSynchronizer.OSDeviceInfoError; -import com.onesignal.OneSignalStateSynchronizer.OSDeviceInfoCompletionHandler; -import com.onesignal.outcomes.data.OSOutcomeEventsFactory; - -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -import java.io.PrintWriter; -import java.io.StringWriter; -import java.lang.ref.WeakReference; -import java.time.ZoneId; -import java.util.ArrayList; -import java.util.Calendar; -import java.util.Collection; -import java.util.Date; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.TimeZone; - -import static com.onesignal.GenerateNotification.BUNDLE_KEY_ACTION_ID; -import static com.onesignal.GenerateNotification.BUNDLE_KEY_ANDROID_NOTIFICATION_ID; -import static com.onesignal.NotificationBundleProcessor.newJsonArray; - -/** - * The main OneSignal class - this is where you will interface with the OneSignal SDK - *

- * Reminder: Add your {@code onesignal_app_id} to your build.gradle config in android > defaultConfig - *
- * @see OneSignal Gradle Setup - */ -public class OneSignal { - - // If the app is this amount time or longer in the background we will count the session as done - static final long MIN_ON_SESSION_TIME_MILLIS = 30 * 1_000L; - - public enum LOG_LEVEL { - NONE, FATAL, ERROR, WARN, INFO, DEBUG, VERBOSE - } - - /** - * An app entry type enum for knowing how the user foregrounded or backgrounded the app. - *

- * The enum also helps decide the type of session the user is in an is tracked in {@link OneSignal#sessionManager} - * from the {@link OSSessionManager}. - *

- * {@link AppEntryAction#NOTIFICATION_CLICK} will always lead a overridden {@link com.onesignal.influence.domain.OSInfluenceType#DIRECT}. - * {@link AppEntryAction#APP_OPEN} on a new session notifications within the attribution window - * parameter, this will lead to a {@link com.onesignal.influence.domain.OSInfluenceType#DIRECT}. - *

- * @see OneSignal#onAppFocus - * @see OneSignal#onAppLostFocus - * @see OneSignal#handleNotificationOpen - */ - public enum AppEntryAction { - - /** - * Entered the app through opening a notification - */ - NOTIFICATION_CLICK, - - /** - * Entered the app through clicking the icon - */ - APP_OPEN, - - /** - * App came from the background - */ - APP_CLOSE; - - public boolean isNotificationClick() { - return this.equals(NOTIFICATION_CLICK); - } - - public boolean isAppOpen() { - return this.equals(APP_OPEN); - } - - public boolean isAppClose() { - return this.equals(APP_CLOSE); - } - } - - /** - * Implement this interface on a class with a default public constructor and provide class with namespace - * as a value to a new `meta-data` tag with the key name of "com.onesignal.NotificationServiceExtension" in - * your AndroidManifest.xml. - * ex. - *

- * Allows for modification of a notification by calling {@link OSNotification#mutableCopy} - * instance and passing it into {@link OSMutableNotification#setExtender(NotificationCompat.Extender)} - * To display the notification, call {@link OSNotificationReceivedEvent#complete(OSNotification)} with a notification instance. - * To omit displaying a notification call {@link OSNotificationReceivedEvent#complete(OSNotification)} with null. - */ - public interface OSRemoteNotificationReceivedHandler { - - void remoteNotificationReceived(Context context, OSNotificationReceivedEvent notificationReceivedEvent); - } - - /** - * Meant to be implemented with {@link OneSignal#setNotificationWillShowInForegroundHandler(OSNotificationWillShowInForegroundHandler)} - *

- * Call {@link OSNotificationReceivedEvent#complete(OSNotification)} with null - * for not displaying notification or {@link OSMutableNotification} to modify notification before displaying. - * If {@link OSNotificationReceivedEvent#complete(OSNotification)} is not called within 25 seconds, original notification will be displayed. - *

- * TODO: Update docs with new NotificationReceivedHandler - * @see NotificationReceivedHandler | OneSignal Docs - */ - public interface OSNotificationWillShowInForegroundHandler { - - void notificationWillShowInForeground(OSNotificationReceivedEvent notificationReceivedEvent); - } - - /** - * An interface used to process a OneSignal notification the user just tapped on. - *
- * Set this during OneSignal init in - * {@link OneSignal#setNotificationOpenedHandler(OSNotificationOpenedHandler)} - *

- * @see NotificationOpenedHandler | OneSignal Docs - */ - public interface OSNotificationOpenedHandler { - /** - * Fires when a user taps on a notification. - * @param result a {@link OSNotificationOpenedResult} with the user's response and properties of this notification - */ - void notificationOpened(OSNotificationOpenedResult result); - } - - /** - * An interface used to process a OneSignal In-App Message the user just tapped on. - *
- * Set this during OneSignal init in - * {@link OneSignal#setInAppMessageClickHandler(OSInAppMessageClickHandler)} - */ - public interface OSInAppMessageClickHandler { - /** - * Fires when a user taps on a clickable element in the notification such as a button or image - * @param result a {@link OSInAppMessageAction} - **/ - void inAppMessageClicked(OSInAppMessageAction result); - } - - /** - * Interface which you can implement and pass to {@link OneSignal#getTags(OSGetTagsHandler)} to - * get all the tags set on a user - *

- * Note: the {@link #tagsAvailable(JSONObject)} callback does not run on the Main(UI) - * Thread, so be aware when modifying UI in this method. - */ - public interface OSGetTagsHandler { - /** - * Note: this callback does not run on the Main(UI) - * Thread, so be aware when modifying UI in this method. - * @param tags a JSONObject containing the OneSignal tags for the user in a key/value map - */ - void tagsAvailable(JSONObject tags); - } - - public interface ChangeTagsUpdateHandler { - void onSuccess(JSONObject tags); - void onFailure(SendTagsError error); - } - - public static class SendTagsError { - private String message; - private int code; - - SendTagsError(int errorCode, String errorMessage) { - this.message = errorMessage; - this.code = errorCode; - } - - public int getCode() { return code; } - public String getMessage() { return message; } - } - - public static class OSLanguageError { - private int errorCode; - private String message; - - OSLanguageError(int errorCode, String message) { - this.errorCode = errorCode; - this.message = message; - } - - public int getCode() { return errorCode; } - - public String getMessage() { return message; } - } - - public interface OSSetLanguageCompletionHandler { - void onSuccess(String results); - void onFailure(OSLanguageError error); - } - - public enum ExternalIdErrorType { - REQUIRES_EXTERNAL_ID_AUTH, INVALID_OPERATION, NETWORK - } - - public static class ExternalIdError { - private ExternalIdErrorType type; - private String message; - - ExternalIdError(ExternalIdErrorType type, String message) { - this.type = type; - this.message = message; - } - - public ExternalIdErrorType getType() { - return type; - } - - public String getMessage() { - return message; - } - } - - public interface OSExternalUserIdUpdateCompletionHandler { - void onSuccess(JSONObject results); - void onFailure(ExternalIdError error); - } - - interface OSInternalExternalUserIdUpdateCompletionHandler { - void onComplete(String channel, boolean success); - } - - public enum SMSErrorType { - VALIDATION, REQUIRES_SMS_AUTH, INVALID_OPERATION, NETWORK - } - - public static class OSSMSUpdateError { - private SMSErrorType type; - private String message; - - OSSMSUpdateError(SMSErrorType type, String message) { - this.type = type; - this.message = message; - } - - public SMSErrorType getType() { - return type; - } - - public String getMessage() { - return message; - } - } - - public interface OSSMSUpdateHandler { - void onSuccess(JSONObject result); - void onFailure(OSSMSUpdateError error); - } - - private static OSSMSUpdateHandler smsUpdateHandler; - private static OSSMSUpdateHandler smsLogoutHandler; - - public enum EmailErrorType { - VALIDATION, REQUIRES_EMAIL_AUTH, INVALID_OPERATION, NETWORK - } - - public static class EmailUpdateError { - private EmailErrorType type; - private String message; - - EmailUpdateError(EmailErrorType type, String message) { - this.type = type; - this.message = message; - } - - public EmailErrorType getType() { - return type; - } - - public String getMessage() { - return message; - } - } - - public interface EmailUpdateHandler { - void onSuccess(); - void onFailure(EmailUpdateError error); - } - - private static EmailUpdateHandler emailUpdateHandler; - private static EmailUpdateHandler emailLogoutHandler; - - /** - * Fires delegate when the notification was created or fails to be created. - */ - public interface PostNotificationResponseHandler { - void onSuccess(JSONObject response); - void onFailure(JSONObject response); - } - - /** - * Fires when the User accepts or declines the notification permission prompt. - */ - public interface PromptForPushNotificationPermissionResponseHandler { - void response(boolean accepted); - } - - interface EntryStateListener { - // Fire with the last appEntryState that just ended. - void onEntryStateChange(AppEntryAction appEntryState); - } - - private static List entryStateListeners = new ArrayList<>(); - static void callEntryStateListeners(AppEntryAction appEntryState) { - List entryStateListeners = new ArrayList<>(OneSignal.entryStateListeners); - for (EntryStateListener sessionListener : entryStateListeners) { - sessionListener.onEntryStateChange(appEntryState); - } - } - - static void addEntryStateListener(EntryStateListener sessionListener, AppEntryAction appEntryState) { - // We only care for open and close changes - if (!appEntryState.equals(AppEntryAction.NOTIFICATION_CLICK)) - entryStateListeners.add(sessionListener); - } - - static void removeEntryStateListener(EntryStateListener sessionListener) { - entryStateListeners.remove(sessionListener); - } - - static Context appContext; - static WeakReference appActivity; - static String appId; - static String googleProjectNumber; - - @Nullable - static Activity getCurrentActivity() { - ActivityLifecycleHandler activityLifecycleHandler = ActivityLifecycleListener.getActivityLifecycleHandler(); - return activityLifecycleHandler != null ? activityLifecycleHandler.getCurActivity() : null; - } - - private static LOG_LEVEL visualLogLevel = LOG_LEVEL.NONE; - private static LOG_LEVEL logCatLevel = LOG_LEVEL.WARN; - - private static String userId = null; - private static String emailId = null; - private static String smsId = null; - private static int subscribableStatus = Integer.MAX_VALUE; - - // changed from private to package-private for unit test access - static LanguageContext languageContext = null; - - static OSRemoteNotificationReceivedHandler remoteNotificationReceivedHandler; - static OSNotificationWillShowInForegroundHandler notificationWillShowInForegroundHandler; - static OSNotificationOpenedHandler notificationOpenedHandler; - static OSInAppMessageClickHandler inAppMessageClickHandler; - - // Is the init() of OneSignal SDK finished yet - private static boolean initDone; - static boolean isInitDone() { - return initDone; - } - - // Is the app in the inForeground or not - private static boolean inForeground; - static boolean isInForeground() { - return inForeground; - } - static void setInForeground(boolean inForeground) { - OneSignal.inForeground = inForeground; - } - - // Tells the action taken to enter the app - @NonNull private static AppEntryAction appEntryState = AppEntryAction.APP_CLOSE; - static @NonNull AppEntryAction getAppEntryState() { - return appEntryState; - } - - private static TrackGooglePurchase trackGooglePurchase; - private static TrackAmazonPurchase trackAmazonPurchase; - private static TrackFirebaseAnalytics trackFirebaseAnalytics; - - private static final String VERSION = "040807"; - public static String getSdkVersionRaw() { - return VERSION; - } - - private static OSLogger logger = new OSLogWrapper(); - static OSLogger getLogger() { - return logger; - } - - private static FocusTimeController focusTimeController; - private static OSSessionManager.SessionListener sessionListener = new OSSessionManager.SessionListener() { - @Override - public void onSessionEnding(@NonNull List lastInfluences) { - if (outcomeEventsController == null) - OneSignal.Log(LOG_LEVEL.WARN, "OneSignal onSessionEnding called before init!"); - if (outcomeEventsController != null) - outcomeEventsController.cleanOutcomes(); - getFocusTimeController().onSessionEnded(lastInfluences); - } - }; - - private static OSInAppMessageControllerFactory inAppMessageControllerFactory = new OSInAppMessageControllerFactory(); - static OSInAppMessageController getInAppMessageController() { - return inAppMessageControllerFactory.getController(getDBHelperInstance(), taskController, getLogger(), getSharedPreferences(), languageContext); - } - private static OSTime time = new OSTimeImpl(); - private static OSRemoteParamController remoteParamController = new OSRemoteParamController(); - private static OSTaskController taskController = new OSTaskController(logger); - private static OSTaskRemoteController taskRemoteController = new OSTaskRemoteController(remoteParamController, logger); - private static OneSignalAPIClient apiClient = new OneSignalRestClientWrapper(); - private static OSSharedPreferences preferences = new OSSharedPreferencesWrapper(); - static OSSharedPreferences getSharedPreferences() { - return preferences; - } - private static OSTrackerFactory trackerFactory = new OSTrackerFactory(preferences, logger, time); - private static OSSessionManager sessionManager = new OSSessionManager(sessionListener, trackerFactory, logger); - @Nullable private static OSOutcomeEventsController outcomeEventsController; - @Nullable private static OSOutcomeEventsFactory outcomeEventsFactory; - @Nullable private static OSNotificationDataController notificationDataController; - private static final Object outcomeEventsControllerSyncLock = new Object() {}; - - static OSOutcomeEventsController getOutcomeEventsController() { - if (outcomeEventsController == null) { - synchronized(outcomeEventsControllerSyncLock) { - if (outcomeEventsController == null) { - if (outcomeEventsFactory == null) { - OneSignalDbHelper dbHelper = getDBHelperInstance(); - outcomeEventsFactory = new OSOutcomeEventsFactory(logger, apiClient, dbHelper, preferences); - } - outcomeEventsController = new OSOutcomeEventsController(sessionManager, outcomeEventsFactory); - } - } - } - return outcomeEventsController; - } - - @SuppressWarnings("WeakerAccess") - public static String sdkType = "native"; - private static String lastRegistrationId; - - @NonNull private static OSUtils osUtils = new OSUtils(); - - private static boolean registerForPushFired, locationFired, getTagsCall, waitingToPostStateSync, androidParamsRequestStarted; - - private static LocationController.LocationPoint lastLocationPoint; - - private static Collection unprocessedOpenedNotifs = new ArrayList<>(); - private static HashSet postedOpenedNotifIds = new HashSet<>(); - private static final ArrayList pendingGetTagsHandlers = new ArrayList<>(); - - private static DelayedConsentInitializationParameters delayedInitParams; - static DelayedConsentInitializationParameters getDelayedInitParams() { - return delayedInitParams; - } - - // Start PermissionState - private static OSPermissionState currentPermissionState; - private static OSPermissionState getCurrentPermissionState(Context context) { - if (context == null) - return null; - - if (currentPermissionState == null) { - currentPermissionState = new OSPermissionState(false); - currentPermissionState.getObservable().addObserverStrong(new OSPermissionChangedInternalObserver()); - } - - return currentPermissionState; - } - - static OSPermissionState lastPermissionState; - private static OSPermissionState getLastPermissionState(Context context) { - if (context == null) - return null; - - if (lastPermissionState == null) - lastPermissionState = new OSPermissionState(true); - - return lastPermissionState; - } - - private static OSObservable permissionStateChangesObserver; - static OSObservable getPermissionStateChangesObserver() { - if (permissionStateChangesObserver == null) - permissionStateChangesObserver = new OSObservable<>("onOSPermissionChanged", true); - return permissionStateChangesObserver; - } - // End PermissionState - - // Start SubscriptionState - private static OSSubscriptionState currentSubscriptionState; - private static OSSubscriptionState getCurrentSubscriptionState(Context context) { - if (context == null) - return null; - - if (currentSubscriptionState == null) { - currentSubscriptionState = new OSSubscriptionState(false, getCurrentPermissionState(context).areNotificationsEnabled()); - getCurrentPermissionState(context).getObservable().addObserver(currentSubscriptionState); - currentSubscriptionState.getObservable().addObserverStrong(new OSSubscriptionChangedInternalObserver()); - } - - return currentSubscriptionState; - } - - static OSSubscriptionState lastSubscriptionState; - private static OSSubscriptionState getLastSubscriptionState(Context context) { - if (context == null) - return null; - - if (lastSubscriptionState == null) - lastSubscriptionState = new OSSubscriptionState(true, false); - - return lastSubscriptionState; - } - - private static OSObservable subscriptionStateChangesObserver; - static OSObservable getSubscriptionStateChangesObserver() { - if (subscriptionStateChangesObserver == null) - subscriptionStateChangesObserver = new OSObservable<>("onOSSubscriptionChanged", true); - return subscriptionStateChangesObserver; - } - // End SubscriptionState - - - // Start EmailSubscriptionState - private static OSEmailSubscriptionState currentEmailSubscriptionState; - private static OSEmailSubscriptionState getCurrentEmailSubscriptionState(Context context) { - if (context == null) - return null; - - if (currentEmailSubscriptionState == null) { - currentEmailSubscriptionState = new OSEmailSubscriptionState(false); - currentEmailSubscriptionState.getObservable().addObserverStrong(new OSEmailSubscriptionChangedInternalObserver()); - } - - return currentEmailSubscriptionState; - } - static OSEmailSubscriptionState getEmailSubscriptionState() { - return getCurrentEmailSubscriptionState(appContext); - } - - static OSEmailSubscriptionState lastEmailSubscriptionState; - private static OSEmailSubscriptionState getLastEmailSubscriptionState(Context context) { - if (context == null) - return null; - - if (lastEmailSubscriptionState == null) - lastEmailSubscriptionState = new OSEmailSubscriptionState(true); - - return lastEmailSubscriptionState; - } - - private static OSObservable emailSubscriptionStateChangesObserver; - static OSObservable getEmailSubscriptionStateChangesObserver() { - if (emailSubscriptionStateChangesObserver == null) - emailSubscriptionStateChangesObserver = new OSObservable<>("onOSEmailSubscriptionChanged", true); - return emailSubscriptionStateChangesObserver; - } - // End EmailSubscriptionState - - // Start SMSSubscriptionState - private static OSSMSSubscriptionState currentSMSSubscriptionState; - private static OSSMSSubscriptionState getCurrentSMSSubscriptionState(Context context) { - if (context == null) - return null; - - if (currentSMSSubscriptionState == null) { - currentSMSSubscriptionState = new OSSMSSubscriptionState(false); - currentSMSSubscriptionState.getObservable().addObserverStrong(new OSSMSSubscriptionChangedInternalObserver()); - } - - return currentSMSSubscriptionState; - } - static OSSMSSubscriptionState getSMSSubscriptionState() { - return getCurrentSMSSubscriptionState(appContext); - } - - static OSSMSSubscriptionState lastSMSSubscriptionState; - private static OSSMSSubscriptionState getLastSMSSubscriptionState(Context context) { - if (context == null) - return null; - - if (lastSMSSubscriptionState == null) - lastSMSSubscriptionState = new OSSMSSubscriptionState(true); - - return lastSMSSubscriptionState; - } - - private static OSObservable smsSubscriptionStateChangesObserver; - static OSObservable getSMSSubscriptionStateChangesObserver() { - if (smsSubscriptionStateChangesObserver == null) - smsSubscriptionStateChangesObserver = new OSObservable<>("onSMSSubscriptionChanged", true); - return smsSubscriptionStateChangesObserver; - } - // End SMSSubscriptionState - - /** - * Get the current user data, notification and permissions state. - */ - @Nullable - public static OSDeviceState getDeviceState() { - if (appContext == null) { - logger.error("OneSignal.initWithContext has not been called. Could not get OSDeviceState"); - return null; - } - - OSSubscriptionState subscriptionStatus = getCurrentSubscriptionState(appContext); - OSPermissionState permissionStatus = getCurrentPermissionState(appContext); - OSEmailSubscriptionState emailSubscriptionStatus = getCurrentEmailSubscriptionState(appContext); - OSSMSSubscriptionState smsSubscriptionStatus = getCurrentSMSSubscriptionState(appContext); - return new OSDeviceState(subscriptionStatus, permissionStatus, emailSubscriptionStatus, smsSubscriptionStatus); - } - - private static class IAPUpdateJob { - JSONArray toReport; - boolean newAsExisting; - OneSignalRestClient.ResponseHandler restResponseHandler; - - IAPUpdateJob(JSONArray toReport) { - this.toReport = toReport; - } - } - private static IAPUpdateJob iapUpdateJob; - - /** - * If notifications are disabled for your app, unsubscribe the user from OneSignal. - * This will happen when your users go to Settings > Apps and turn off notifications or - * they long press your notifications and select "block notifications". This is {@code false} by default. - * @param set if {@code false} - don't unsubscribe users
- * if {@code true} - unsubscribe users when notifications are disabled
- * the default is {@code false} - * @return the builder you called this method on - */ - public static void unsubscribeWhenNotificationsAreDisabled(final boolean set) { - if (taskRemoteController.shouldQueueTaskForInit(OSTaskRemoteController.UNSUBSCRIBE_WHEN_NOTIFICATION_ARE_DISABLED)) { - logger.error("Waiting for remote params. " + - "Moving " + OSTaskRemoteController.UNSUBSCRIBE_WHEN_NOTIFICATION_ARE_DISABLED + " operation to a pending task queue."); - taskRemoteController.addTaskToQueue(new Runnable() { - @Override - public void run() { - logger.debug("Running " + OSTaskRemoteController.UNSUBSCRIBE_WHEN_NOTIFICATION_ARE_DISABLED + " operation from pending task queue."); - unsubscribeWhenNotificationsAreDisabled(set); - } - }); - return; - } - - // Already set by remote params - if (getRemoteParamController().hasUnsubscribeNotificationKey()) { - logger.warning("unsubscribeWhenNotificationsAreDisabled already called by remote params!, ignoring user set"); - return; - } - - getRemoteParamController().saveUnsubscribeWhenNotificationsAreDisabled(set); - } - - /** - * 1/2 steps in OneSignal init, relying on initWithContext (usage order does not matter) - * Sets the app id OneSignal should use in the application - * This is should be set from all OneSignal entry points - * @param newAppId - String app id associated with the OneSignal dashboard app - */ - public static void setAppId(@NonNull String newAppId) { - if (newAppId == null || newAppId.isEmpty()) { - logger.warning("setAppId called with id: " + newAppId + ", ignoring!"); - return; - } else if (!newAppId.equals(appId)) { - // Pre-check on app id to make sure init of SDK is performed properly - // Usually when the app id is changed during runtime so that SDK is reinitialized properly - initDone = false; - logger.verbose("setAppId called with id: " + newAppId + " changing id from: " + appId); - } - - appId = newAppId; - - if (appContext == null) { - logger.warning("appId set, but please call initWithContext(appContext) with Application context to complete OneSignal init!"); - return; - } - - if (appActivity != null && appActivity.get() != null) - init(appActivity.get()); - else - init(appContext); - } - - /** - * 1/2 steps in OneSignal init, relying on setAppId (usage order does not matter) - * Sets the global shared ApplicationContext for OneSignal - * This is should be set from all OneSignal entry points - * - BroadcastReceivers, Services, and Activities - * @param context - Context used by the Application of the app - */ - public static void initWithContext(@NonNull Context context) { - if (context == null) { - logger.warning("initWithContext called with null context, ignoring!"); - return; - } - - if (context instanceof Activity) - appActivity = new WeakReference<>((Activity) context); - - boolean wasAppContextNull = (appContext == null); - appContext = context.getApplicationContext(); - - setupContextListeners(wasAppContextNull); - setupPrivacyConsent(appContext); - - if (appId == null) { - // Get the cached app id, if it exists - String oldAppId = getSavedAppId(); - if (oldAppId == null) { - logger.warning("appContext set, but please call setAppId(appId) with a valid appId to complete OneSignal init!"); - } else { - logger.verbose("appContext set and cached app id found, calling setAppId with: " + oldAppId); - setAppId(oldAppId); - } - return; - } else { - logger.verbose("initWithContext called with: " + context); - } - init(context); - } - - static void setRemoteNotificationReceivedHandler(OSRemoteNotificationReceivedHandler callback) { - if (remoteNotificationReceivedHandler == null) - remoteNotificationReceivedHandler = callback; - } - - public static void setNotificationWillShowInForegroundHandler(@Nullable OSNotificationWillShowInForegroundHandler callback) { - notificationWillShowInForegroundHandler = callback; - } - - public static void setInAppMessageLifecycleHandler(@Nullable final OSInAppMessageLifecycleHandler handler) { - if (appContext == null) { - logger.error("Waiting initWithContext. " + - "Moving " + OSTaskRemoteController.SET_IN_APP_MESSAGE_LIFECYCLE_HANDLER + " operation to a pending task queue."); - taskRemoteController.addTaskToQueue(new Runnable() { - @Override - public void run() { - logger.debug("Running " + OSTaskRemoteController.SET_IN_APP_MESSAGE_LIFECYCLE_HANDLER + " operation from pending queue."); - setInAppMessageLifecycleHandler(handler); - } - }); - return; - } - getInAppMessageController().setInAppMessageLifecycleHandler(handler); - } - - public static void setNotificationOpenedHandler(@Nullable OSNotificationOpenedHandler callback) { - notificationOpenedHandler = callback; - - if (initDone && notificationOpenedHandler != null) - fireCallbackForOpenedNotifications(); - } - - public static void setInAppMessageClickHandler(@Nullable OSInAppMessageClickHandler callback) { - inAppMessageClickHandler = callback; - } - - /** - * Called after setAppId and initWithContext, depending on which one is called last (order does not matter) - */ - synchronized private static void init(Context context) { - logger.verbose("Starting OneSignal initialization!"); - OSNotificationController.setupNotificationServiceExtension(appContext); - - if (requiresUserPrivacyConsent() || !remoteParamController.isRemoteParamsCallDone()) { - if (!remoteParamController.isRemoteParamsCallDone()) - logger.verbose("OneSignal SDK initialization delayed, " + - "waiting for remote params."); - else - logger.verbose("OneSignal SDK initialization delayed, " + - "waiting for privacy consent to be set."); - - delayedInitParams = new DelayedConsentInitializationParameters(appContext, appId); - String lastAppId = appId; - // Set app id null since OneSignal was not init fully - appId = null; - // Wrapper SDK's call init twice and pass null as the appId on the first call - // the app ID is required to download parameters, so do not download params until the appID is provided - if (lastAppId != null && context != null) - makeAndroidParamsRequest(lastAppId, getUserId(), false); - return; - } - - // Keep last subscribed Status if already set - subscribableStatus = subscribableStatus != Integer.MAX_VALUE ? subscribableStatus : osUtils.initializationChecker(appContext, appId); - if (isSubscriptionStatusUninitializable()) - return; - - if (initDone) { - if (notificationOpenedHandler != null) - fireCallbackForOpenedNotifications(); - logger.debug("OneSignal SDK initialization already completed."); - return; - } - - handleActivityLifecycleHandler(context); - // Clean saved init activity - appActivity = null; - - OneSignalStateSynchronizer.initUserState(); - - // Check and handle app id change of the current session - handleAppIdChange(); - - // Verify the session is an Amazon purchase and track it - handleAmazonPurchase(); - - OSPermissionChangedInternalObserver.handleInternalChanges(getCurrentPermissionState(appContext)); - - // When the session reaches timeout threshold, start new session - // This is where the LocationGMS prompt is triggered and shown to the user - doSessionInit(); - - if (notificationOpenedHandler != null) - fireCallbackForOpenedNotifications(); - - if (TrackGooglePurchase.CanTrack(appContext)) - trackGooglePurchase = new TrackGooglePurchase(appContext); - - if (TrackFirebaseAnalytics.CanTrack()) - trackFirebaseAnalytics = new TrackFirebaseAnalytics(appContext); - - initDone = true; - OneSignal.Log(LOG_LEVEL.VERBOSE, "OneSignal SDK initialization done."); - - getOutcomeEventsController().sendSavedOutcomes(); - - // Clean up any pending tasks that were queued up before initialization - taskRemoteController.startPendingTasks(); - } - - static void onRemoteParamSet() { - boolean initDelayed = reassignDelayedInitParams(); - if (!initDelayed && inForeground) // Remote Params called from onAppFocus - onAppFocusLogic(); - } - - private static void setupContextListeners(boolean wasAppContextNull) { - // Register the lifecycle listener of the app for state changes in activities with proper context - ActivityLifecycleListener.registerActivityLifecycleCallbacks((Application) appContext); - - // Do work here that should only happen once or at the start of a new lifecycle - if (wasAppContextNull) { - // Set Language Context to null - languageContext = new LanguageContext(preferences); - - // Prefs require a context to save - // If the previous state of appContext was null, kick off write in-case it was waiting - OneSignalPrefs.startDelayedWrite(); - - OneSignalDbHelper dbHelper = getDBHelperInstance(); - notificationDataController = new OSNotificationDataController(dbHelper, logger); - - // Cleans out old cached data to prevent over using the storage on devices - notificationDataController.cleanOldCachedData(); - - getInAppMessageController().cleanCachedInAppMessages(); - - if (outcomeEventsFactory == null) - outcomeEventsFactory = new OSOutcomeEventsFactory(logger, apiClient, dbHelper, preferences); - - sessionManager.initSessionFromCache(); - getOutcomeEventsController().cleanCachedUniqueOutcomes(); - } - } - - private static void setupPrivacyConsent(Context context) { - ApplicationInfo ai = ApplicationInfoHelper.Companion.getInfo(context); - if (ai == null) { - return; - } - Bundle bundle = ai.metaData; - - // Read the current privacy consent setting from AndroidManifest.xml - String requireSetting = bundle.getString("com.onesignal.PrivacyConsent"); - if (requireSetting != null) - setRequiresUserPrivacyConsent("ENABLE".equalsIgnoreCase(requireSetting)); - } - - private static void handleAppIdChange() { - // Re-register user if the app id changed (might happen when a dev is testing) - String oldAppId = getSavedAppId(); - if (oldAppId != null) { - if (!oldAppId.equals(appId)) { - Log(LOG_LEVEL.DEBUG, "App id has changed:\nFrom: " + oldAppId + "\n To: " + appId + "\nClearing the user id, app state, and remoteParams as they are no longer valid"); - saveAppId(appId); - OneSignalStateSynchronizer.resetCurrentState(); - remoteParamController.clearRemoteParams(); - } - } else { - // First time setting an app id - Log(LOG_LEVEL.DEBUG, "App id set for first time: " + appId); - BadgeCountUpdater.updateCount(0, appContext); - saveAppId(appId); - } - } - - public static boolean userProvidedPrivacyConsent() { - return remoteParamController.getSavedUserConsentStatus(); - } - - private static boolean isSubscriptionStatusUninitializable() { - return subscribableStatus == OSUtils.UNINITIALIZABLE_STATUS; - } - - private static void handleActivityLifecycleHandler(Context context) { - ActivityLifecycleHandler activityLifecycleHandler = ActivityLifecycleListener.getActivityLifecycleHandler(); - boolean isContextActivity = context instanceof Activity; - boolean isCurrentActivityNull = OneSignal.getCurrentActivity() == null; - setInForeground(!isCurrentActivityNull || isContextActivity); - logger.debug("OneSignal handleActivityLifecycleHandler inForeground: " + inForeground); - - if (inForeground) { - if (isCurrentActivityNull && isContextActivity && activityLifecycleHandler != null) { - activityLifecycleHandler.setCurActivity((Activity) context); - activityLifecycleHandler.setNextResumeIsFirstActivity(true); - } - OSNotificationRestoreWorkManager.beginEnqueueingWork(context, false); - getFocusTimeController().appForegrounded(); - } else if (activityLifecycleHandler != null) { - activityLifecycleHandler.setNextResumeIsFirstActivity(true); - } - } - - private static void handleAmazonPurchase() { - try { - Class.forName("com.amazon.device.iap.PurchasingListener"); - trackAmazonPurchase = new TrackAmazonPurchase(appContext); - } catch (ClassNotFoundException e) {} - } - - // If the app is not in the inForeground yet do not make an on_session call yet. - // If we don't have a OneSignal player_id yet make the call to create it regardless of focus - private static void doSessionInit() { - // Check session time to determine whether to start a new session or not - if (shouldStartNewSession()) { - logger.debug("Starting new session with appEntryState: " + getAppEntryState()); - - OneSignalStateSynchronizer.setNewSession(); - getOutcomeEventsController().cleanOutcomes(); - sessionManager.restartSessionIfNeeded(getAppEntryState()); - getInAppMessageController().resetSessionLaunchTime(); - setLastSessionTime(time.getCurrentTimeMillis()); - } else if (isInForeground()) { - logger.debug("Continue on same session with appEntryState: " + getAppEntryState()); - sessionManager.attemptSessionUpgrade(getAppEntryState()); - } - - getInAppMessageController().initWithCachedInAppMessages(); - - // We still want register the user to OneSignal if the SDK was initialized - // in the background for the first time. - if (!inForeground && hasUserId()) - logger.debug("doSessionInit on background with already registered user"); - - startRegistrationOrOnSession(); - } - - private static void startRegistrationOrOnSession() { - if (waitingToPostStateSync) - return; - waitingToPostStateSync = true; - - if (inForeground && OneSignalStateSynchronizer.getSyncAsNewSession()) - locationFired = false; - - startLocationUpdate(); - - registerForPushFired = false; - - // This will also enable background player updates - if (getRemoteParams() != null) - registerForPushToken(); - else - makeAndroidParamsRequest(appId, getUserId(), true); - } - - private static void startLocationUpdate() { - LocationController.LocationHandler locationHandler = new LocationController.LocationHandler() { - @Override - public LocationController.PermissionType getType() { - return LocationController.PermissionType.STARTUP; - } - @Override - public void onComplete(LocationController.LocationPoint point) { - lastLocationPoint = point; - locationFired = true; - registerUser(); - } - }; - - LocationController.getLocation(appContext, false, false, locationHandler); - } - - private static PushRegistrator mPushRegistrator; - - private static PushRegistrator getPushRegistrator() { - if (mPushRegistrator != null) - return mPushRegistrator; - - if (OSUtils.isFireOSDeviceType()) - mPushRegistrator = new PushRegistratorADM(); - else if (OSUtils.isAndroidDeviceType()) { - if (OSUtils.hasFCMLibrary()) - mPushRegistrator = getPushRegistratorFCM(); - } else - mPushRegistrator = new PushRegistratorHMS(); - - return mPushRegistrator; - } - - @NonNull - static private PushRegistratorFCM getPushRegistratorFCM() { - OneSignalRemoteParams.FCMParams fcmRemoteParams = remoteParamController.getRemoteParams().fcmParams; - PushRegistratorFCM.Params fcmParams = null; - if (fcmRemoteParams != null) { - fcmParams = new PushRegistratorFCM.Params(fcmRemoteParams.projectId, fcmRemoteParams.appId, fcmRemoteParams.apiKey); - } - return new PushRegistratorFCM(appContext, fcmParams); - } - - private static void registerForPushToken() { - getPushRegistrator().registerForPush(appContext, googleProjectNumber, new PushRegistrator.RegisteredHandler() { - @Override - public void complete(String id, int status) { - logger.debug("registerForPushToken completed with id: " + id + " status: " + status); - if (status < UserState.PUSH_STATUS_SUBSCRIBED) { - // Only allow errored subscribableStatuses if we have never gotten a token. - // This ensures the device will not later be marked unsubscribed due to a - // any inconsistencies returned by Google Play services. - // Also do not override a config error status if we got a runtime error - if (OneSignalStateSynchronizer.getRegistrationId() == null && - (subscribableStatus == UserState.PUSH_STATUS_SUBSCRIBED || - pushStatusRuntimeError(subscribableStatus))) - subscribableStatus = status; - } - else if (pushStatusRuntimeError(subscribableStatus)) - subscribableStatus = status; - - lastRegistrationId = id; - registerForPushFired = true; - getCurrentSubscriptionState(appContext).setPushToken(id); - registerUser(); - } - }); - } - - private static boolean pushStatusRuntimeError(int subscriptionStatus) { - return subscriptionStatus < -6; - } - - private static void makeAndroidParamsRequest(String appId, String userId, final boolean queuePushRegistration) { - if (getRemoteParams() != null || androidParamsRequestStarted) - return; - - androidParamsRequestStarted = true; - OneSignalRemoteParams.makeAndroidParamsRequest(appId, userId, new OneSignalRemoteParams.Callback() { - @Override - public void complete(OneSignalRemoteParams.Params params) { - androidParamsRequestStarted = false; - if (params.googleProjectNumber != null) - googleProjectNumber = params.googleProjectNumber; - - remoteParamController.saveRemoteParams(params, trackerFactory, preferences, logger); - onRemoteParamSet(); - - NotificationChannelManager.processChannelList( - OneSignal.appContext, - params.notificationChannels - ); - - if (queuePushRegistration) - registerForPushToken(); - } - }); - } - - private static void fireCallbackForOpenedNotifications() { - for (JSONArray dataArray : unprocessedOpenedNotifs) - runNotificationOpenedCallback(dataArray); - - unprocessedOpenedNotifs.clear(); - } - - /** - * TODO: Decide on a single logging method to use instead of using several all over the place - * Please do not use this method for logging, it is meant solely to be - * used by our wrapper SDK's. - */ - public static void onesignalLog(LOG_LEVEL level, String message) { - OneSignal.Log(level, message); - } - - public static void provideUserConsent(boolean consent) { - boolean previousConsentStatus = userProvidedPrivacyConsent(); - - remoteParamController.saveUserConsentStatus(consent); - - if (!previousConsentStatus && consent && delayedInitParams != null) { - OneSignal.Log(LOG_LEVEL.VERBOSE, "Privacy consent provided, reassigning all delayed init params and attempting init again..."); - reassignDelayedInitParams(); - } - } - - private static boolean reassignDelayedInitParams() { - if (initDone) - return false; - - String delayedAppId; - Context delayedContext; - if (delayedInitParams == null) { - // Get the cached app id, if it exists - delayedAppId = getSavedAppId(); - delayedContext = appContext; - logger.error("Trying to continue OneSignal with null delayed params"); - } else { - delayedAppId = delayedInitParams.getAppId(); - delayedContext = delayedInitParams.getContext(); - } - - logger.debug("reassignDelayedInitParams with appContext: " + appContext); - - delayedInitParams = null; - setAppId(delayedAppId); - - // Check to avoid extra initWithContext logging and logic - if (!initDone) { - if (delayedContext == null) { - logger.error("Trying to continue OneSignal with null delayed params context"); - return false; - } - initWithContext(delayedContext); - } - return true; - } - - static OneSignalRemoteParams.Params getRemoteParams() { - return remoteParamController.getRemoteParams(); - } - - /** - * Indicates if the SDK is still waiting for the user to provide consent - */ - public static boolean requiresUserPrivacyConsent() { - return appContext == null || isUserPrivacyConsentRequired() && !userProvidedPrivacyConsent(); - } - - /** - * This method will be replaced by remote params set - */ - public static void setRequiresUserPrivacyConsent(final boolean required) { - // Already set by remote params - if (getRemoteParamController().hasPrivacyConsentKey()) { - logger.warning("setRequiresUserPrivacyConsent already called by remote params!, ignoring user set"); - return; - } - - if (requiresUserPrivacyConsent() && !required) { - OneSignal.Log(LOG_LEVEL.ERROR, "Cannot change requiresUserPrivacyConsent() from TRUE to FALSE"); - return; - } - - getRemoteParamController().savePrivacyConsentRequired(required); - } - - static boolean shouldLogUserPrivacyConsentErrorMessageForMethodName(String methodName) { - if (requiresUserPrivacyConsent()) { - if (methodName != null) - OneSignal.Log(LOG_LEVEL.WARN, "Method " + methodName + " was called before the user provided privacy consent. Your application is set to require the user's privacy consent before the OneSignal SDK can be initialized. Please ensure the user has provided consent before calling this method. You can check the latest OneSignal consent status by calling OneSignal.userProvidedPrivacyConsent()"); - return true; - } - - return false; - } - - public static void setLogLevel(LOG_LEVEL inLogCatLevel, LOG_LEVEL inVisualLogLevel) { - logCatLevel = inLogCatLevel; visualLogLevel = inVisualLogLevel; - } - - /** - * Enable logging to help debug if you run into an issue setting up OneSignal. - * The following options are available with increasingly more information: - *
- * - {@code NONE} - *
- * - {@code FATAL} - *
- * - {@code ERROR} - *
- * - {@code WARN} - *
- * - {@code INFO} - *
- * - {@code DEBUG} - *
- * - {@code VERBOSE} - * @param inLogCatLevel Sets the logging level to print to the Android LogCat log - * @param inVisualLogLevel Sets the logging level to show as alert dialogs - */ - public static void setLogLevel(int inLogCatLevel, int inVisualLogLevel) { - setLogLevel(getLogLevel(inLogCatLevel), getLogLevel(inVisualLogLevel)); - } - - private static OneSignal.LOG_LEVEL getLogLevel(int level) { - switch(level) { - case 0: - return OneSignal.LOG_LEVEL.NONE; - case 1: - return OneSignal.LOG_LEVEL.FATAL; - case 2: - return OneSignal.LOG_LEVEL.ERROR; - case 3: - return OneSignal.LOG_LEVEL.WARN; - case 4: - return OneSignal.LOG_LEVEL.INFO; - case 5: - return OneSignal.LOG_LEVEL.DEBUG; - case 6: - return OneSignal.LOG_LEVEL.VERBOSE; - } - - if (level < 0) - return OneSignal.LOG_LEVEL.NONE; - return OneSignal.LOG_LEVEL.VERBOSE; - } - - static boolean atLogLevel(LOG_LEVEL level) { - return level.compareTo(visualLogLevel) < 1 || level.compareTo(logCatLevel) < 1; - } - - static void Log(@NonNull LOG_LEVEL level, @NonNull String message) { - Log(level, message, null); - } - - static void Log(@NonNull final LOG_LEVEL level, @NonNull String message, @Nullable Throwable throwable) { - - final String TAG = "OneSignal"; - - if (level.compareTo(logCatLevel) < 1) { - if (level == LOG_LEVEL.VERBOSE) - Log.v(TAG, message, throwable); - else if (level == LOG_LEVEL.DEBUG) - Log.d(TAG, message, throwable); - else if (level == LOG_LEVEL.INFO) - Log.i(TAG, message, throwable); - else if (level == LOG_LEVEL.WARN) - Log.w(TAG, message, throwable); - else if (level == LOG_LEVEL.ERROR || level == LOG_LEVEL.FATAL) - Log.e(TAG, message, throwable); - } - - if (level.compareTo(visualLogLevel) < 1 && OneSignal.getCurrentActivity() != null) { - try { - String fullMessage = message + "\n"; - if (throwable != null) { - fullMessage += throwable.getMessage(); - StringWriter sw = new StringWriter(); - PrintWriter pw = new PrintWriter(sw); - throwable.printStackTrace(pw); - fullMessage += sw.toString(); - } - - final String finalFullMessage = fullMessage; - OSUtils.runOnMainUIThread(new Runnable() { - @Override - public void run() { - if (OneSignal.getCurrentActivity() != null) - new AlertDialog.Builder(OneSignal.getCurrentActivity()) - .setTitle(level.toString()) - .setMessage(finalFullMessage) - .show(); - } - }); - } catch(Throwable t) { - Log.e(TAG, "Error showing logging message.", t); - } - } - } - - static void logHttpError(String errorString, int statusCode, Throwable throwable, String errorResponse) { - String jsonError = ""; - if (errorResponse != null && atLogLevel(LOG_LEVEL.INFO)) - jsonError = "\n" + errorResponse + "\n"; - Log(LOG_LEVEL.WARN, "HTTP code: " + statusCode + " " + errorString + jsonError, throwable); - } - - // Returns true if there is active time that is non synced. - @WorkerThread - static void onAppLostFocus() { - Log(LOG_LEVEL.DEBUG, "Application lost focus initDone: " + initDone); - setInForeground(false); - appEntryState = AppEntryAction.APP_CLOSE; - - setLastSessionTime(OneSignal.getTime().getCurrentTimeMillis()); - LocationController.onFocusChange(); - - if (!initDone) { - // Make sure remote param call has finish in order to know if privacyConsent is required - if (taskRemoteController.shouldQueueTaskForInit(OSTaskRemoteController.APP_LOST_FOCUS)) { - logger.error("Waiting for remote params. " + - "Moving " + OSTaskRemoteController.APP_LOST_FOCUS + " operation to a pending task queue."); - taskRemoteController.addTaskToQueue(new Runnable() { - @Override - public void run() { - logger.debug("Running " + OSTaskRemoteController.APP_LOST_FOCUS + " operation from a pending task queue."); - backgroundSyncLogic(); - } - }); - } - return; - } - - backgroundSyncLogic(); - } - - static void backgroundSyncLogic() { - if (inForeground) - return; - - if (trackAmazonPurchase != null) - trackAmazonPurchase.checkListener(); - - getFocusTimeController().appBackgrounded(); - - scheduleSyncService(); - } - - // Schedules location update or a player update if there are any unsynced changes - private static boolean scheduleSyncService() { - boolean unsyncedChanges = OneSignalStateSynchronizer.persist(); - logger.debug("OneSignal scheduleSyncService unsyncedChanges: " + unsyncedChanges); - if (unsyncedChanges) - OSSyncService.getInstance().scheduleSyncTask(appContext); - - boolean locationScheduled = LocationController.scheduleUpdate(appContext); - logger.debug("OneSignal scheduleSyncService locationScheduled: " + locationScheduled); - return locationScheduled || unsyncedChanges; - } - - static void onAppFocus() { - Log(LOG_LEVEL.DEBUG, "Application on focus"); - setInForeground(true); - - // If the app gains focus and has not been set to NOTIFICATION_CLICK yet we can assume this is a normal app open - if (!appEntryState.equals(AppEntryAction.NOTIFICATION_CLICK)) { - callEntryStateListeners(appEntryState); - // Check again because listeners might have changed the appEntryState - if (!appEntryState.equals(AppEntryAction.NOTIFICATION_CLICK)) - appEntryState = AppEntryAction.APP_OPEN; - } - - LocationController.onFocusChange(); - NotificationPermissionController.INSTANCE.onAppForegrounded(); - - if (OSUtils.shouldLogMissingAppIdError(appId)) - return; - // Make sure remote param call has finish in order to know if privacyConsent is required - if (!remoteParamController.isRemoteParamsCallDone()) { - Log(LOG_LEVEL.DEBUG, "Delay onAppFocus logic due to missing remote params"); - makeAndroidParamsRequest(appId, getUserId(), false); - return; - } - - onAppFocusLogic(); - } - - static void onAppStartFocusLogic() { - refreshNotificationPermissionState(); - } - - static void refreshNotificationPermissionState() { - getCurrentPermissionState(appContext).refreshAsTo(); - } - - private static void onAppFocusLogic() { - // Make sure without privacy consent, onAppFocus returns early - if (shouldLogUserPrivacyConsentErrorMessageForMethodName("onAppFocus")) - return; - - getFocusTimeController().appForegrounded(); - - doSessionInit(); - - if (trackGooglePurchase != null) - trackGooglePurchase.trackIAP(); - - OSNotificationRestoreWorkManager.beginEnqueueingWork(appContext, false); - - refreshNotificationPermissionState(); - - if (trackFirebaseAnalytics != null && getFirebaseAnalyticsEnabled()) - trackFirebaseAnalytics.trackInfluenceOpenEvent(); - - OSSyncService.getInstance().cancelSyncTask(appContext); - } - - static void addNetType(JSONObject jsonObj) { - try { - jsonObj.put("net_type", osUtils.getNetType()); - } catch (Throwable t) {} - } - - private static int getTimeZoneOffset() { - TimeZone timezone = Calendar.getInstance().getTimeZone(); - int offset = timezone.getRawOffset(); - - if (timezone.inDaylightTime(new Date())) - offset = offset + timezone.getDSTSavings(); - - return offset / 1000; - } - - private static String getTimeZoneId() { - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { - return ZoneId.systemDefault().getId(); - } else { - return TimeZone.getDefault().getID(); - } - } - - private static void registerUser() { - logger.debug( - "registerUser:" + - "registerForPushFired:" + registerForPushFired + - ", locationFired: " + locationFired + - ", remoteParams: " + getRemoteParams() + - ", appId: " + appId - ); - - if (!registerForPushFired || !locationFired || getRemoteParams() == null || appId == null) { - logger.debug("registerUser not possible"); - return; - } - - new Thread(new Runnable() { - public void run() { - try { - registerUserTask(); - } catch(JSONException t) { - Log(LOG_LEVEL.FATAL, "FATAL Error registering device!", t); - } - } - }, "OS_REG_USER").start(); - } - - private static void registerUserTask() throws JSONException { - String packageName = appContext.getPackageName(); - - JSONObject deviceInfo = new JSONObject(); - - deviceInfo.put("app_id", getSavedAppId()); - - deviceInfo.put("device_os", Build.VERSION.RELEASE); - deviceInfo.put("timezone", getTimeZoneOffset()); - deviceInfo.put("timezone_id", getTimeZoneId()); - deviceInfo.put("language", languageContext.getLanguage()); - deviceInfo.put("sdk", VERSION); - deviceInfo.put("sdk_type", sdkType); - deviceInfo.put("android_package", packageName); - deviceInfo.put("device_model", Build.MODEL); - Integer appVersion = getAppVersion(); - if (appVersion != null) { - deviceInfo.put("game_version", appVersion); - } - deviceInfo.put("net_type", osUtils.getNetType()); - deviceInfo.put("carrier", osUtils.getCarrierName()); - deviceInfo.put("rooted", RootToolsInternalMethods.isRooted()); - - OneSignalStateSynchronizer.updateDeviceInfo(deviceInfo, null); - - JSONObject pushState = new JSONObject(); - pushState.put("identifier", lastRegistrationId); - pushState.put("subscribableStatus", subscribableStatus); - pushState.put("androidPermission", areNotificationsEnabledForSubscribedState()); - pushState.put("device_type", osUtils.getDeviceType()); - OneSignalStateSynchronizer.updatePushState(pushState); - - if (isLocationShared() && lastLocationPoint != null) - OneSignalStateSynchronizer.updateLocation(lastLocationPoint); - - logger.debug("registerUserTask calling readyToUpdate"); - OneSignalStateSynchronizer.readyToUpdate(true); - - waitingToPostStateSync = false; - } - - private static Integer getAppVersion() { - String packageName = appContext.getPackageName(); - GetPackageInfoResult result = - PackageInfoHelper.Companion.getInfo( - OneSignal.appContext, - packageName, - 0 - ); - if (!result.getSuccessful() || result.getPackageInfo() == null) { - return null; - } - - return result.getPackageInfo().versionCode; - } - - public static void setSMSNumber(@NonNull final String smsNumber, OSSMSUpdateHandler callback) { - setSMSNumber(smsNumber, null, callback); - } - - public static void setSMSNumber(@NonNull final String smsNumber) { - setSMSNumber(smsNumber, null, null); - } - - public static void setSMSNumber(@NonNull final String smsNumber, @Nullable final String smsAuthHash) { - setSMSNumber(smsNumber, smsAuthHash, null); - } - - /** - * Set an sms number for the device to later send sms to this number - * @param smsNumber The sms number that you want subscribe and associate with the device - * @param smsAuthHash Generated auth hash from your server to authorize. (Recommended) - * Create and send this hash from your backend to your app after - * the user logs into your app. - * DO NOT generate this from your app! - * Omit this value if you do not have a backend to authenticate the user. - * @param callback Fire onSuccess or onFailure depending if the update successes or fails - */ - public static void setSMSNumber(@NonNull final String smsNumber, final String smsAuthHash, final OSSMSUpdateHandler callback) { - if (taskRemoteController.shouldQueueTaskForInit(OSTaskRemoteController.SET_SMS_NUMBER)) { - logger.error("Waiting for remote params. " + - "Moving " + OSTaskRemoteController.SET_SMS_NUMBER + " operation to a pending task queue."); - taskRemoteController.addTaskToQueue(new Runnable() { - @Override - public void run() { - logger.debug("Running " + OSTaskRemoteController.SET_SMS_NUMBER + " operation from a pending task queue."); - setSMSNumber(smsNumber, smsAuthHash, callback); - } - }); - return; - } - - // If applicable, check if the user provided privacy consent - if (shouldLogUserPrivacyConsentErrorMessageForMethodName(OSTaskRemoteController.SET_SMS_NUMBER)) - return; - - if (TextUtils.isEmpty(smsNumber)) { - String errorMessage = "SMS number is invalid"; - if (callback != null) - callback.onFailure(new OSSMSUpdateError(SMSErrorType.VALIDATION, errorMessage)); - logger.error(errorMessage); - return; - } - - if (getRemoteParams().useSMSAuth && (smsAuthHash == null || smsAuthHash.length() == 0)) { - String errorMessage = "SMS authentication (auth token) is set to REQUIRED for this application. Please provide an auth token from your backend server or change the setting in the OneSignal dashboard."; - if (callback != null) - callback.onFailure(new OSSMSUpdateError(SMSErrorType.REQUIRES_SMS_AUTH, errorMessage)); - logger.error(errorMessage); - return; - } - - smsUpdateHandler = callback; - - getCurrentSMSSubscriptionState(appContext).setSMSNumber(smsNumber); - OneSignalStateSynchronizer.setSMSNumber(smsNumber, smsAuthHash); - } - - /** - * Call when user logs out of their account. - * This dissociates the device from the email address. - * This does not effect the subscription status of the email address itself. - */ - public static void logoutSMSNumber() { - logoutSMSNumber(null); - } - - public static void logoutSMSNumber(@Nullable final OSSMSUpdateHandler callback) { - if (taskRemoteController.shouldQueueTaskForInit(OSTaskRemoteController.LOGOUT_SMS_NUMBER)) { - logger.error("Waiting for remote params. " + - "Moving " + OSTaskRemoteController.LOGOUT_SMS_NUMBER + " operation to a pending task queue."); - taskRemoteController.addTaskToQueue(new Runnable() { - @Override - public void run() { - logger.debug("Running " + OSTaskRemoteController.LOGOUT_SMS_NUMBER + " operation from pending task queue."); - logoutSMSNumber(callback); - } - }); - return; - } - - // If applicable, check if the user provided privacy consent - if (shouldLogUserPrivacyConsentErrorMessageForMethodName(OSTaskRemoteController.LOGOUT_SMS_NUMBER)) - return; - - if (getSMSId() == null) { - final String message = OSTaskRemoteController.LOGOUT_SMS_NUMBER + " not valid as sms number was not set or already logged out!"; - if (callback != null) - callback.onFailure(new OSSMSUpdateError(SMSErrorType.INVALID_OPERATION, message)); - logger.error(message); - return; - } - - smsLogoutHandler = callback; - OneSignalStateSynchronizer.logoutSMS(); - } - - public static void setEmail(@NonNull final String email, EmailUpdateHandler callback) { - setEmail(email, null, callback); - } - - public static void setEmail(@NonNull final String email) { - setEmail(email, null, null); - } - - public static void setEmail(@NonNull final String email, @Nullable final String emailAuthHash) { - setEmail(email, emailAuthHash, null); - } - - /** - * Set an email for the device to later send emails to this address - * @param email The email that you want subscribe and associate with the device - * @param emailAuthHash Generated auth hash from your server to authorize. (Recommended) - * Create and send this hash from your backend to your app after - * the user logs into your app. - * DO NOT generate this from your app! - * Omit this value if you do not have a backend to authenticate the user. - * @param callback Fire onSuccess or onFailure depending if the update successes or fails - */ - public static void setEmail(@NonNull final String email, @Nullable final String emailAuthHash, @Nullable final EmailUpdateHandler callback) { - if (taskRemoteController.shouldQueueTaskForInit(OSTaskRemoteController.SET_EMAIL)) { - logger.error("Waiting for remote params. " + - "Moving " + OSTaskRemoteController.SET_EMAIL + " operation to a pending task queue."); - taskRemoteController.addTaskToQueue(new Runnable() { - @Override - public void run() { - logger.debug("Running " + OSTaskRemoteController.SET_EMAIL + " operation from a pending task queue."); - setEmail(email, emailAuthHash, callback); - } - }); - return; - } - // If applicable, check if the user provided privacy consent - if (shouldLogUserPrivacyConsentErrorMessageForMethodName(OSTaskRemoteController.SET_EMAIL)) - return; - - if (!OSUtils.isValidEmail(email)) { - String errorMessage = "Email is invalid"; - if (callback != null) - callback.onFailure(new EmailUpdateError(EmailErrorType.VALIDATION, errorMessage)); - logger.error(errorMessage); - return; - } - - if (getRemoteParams().useEmailAuth && (emailAuthHash == null || emailAuthHash.length() == 0)) { - String errorMessage = "Email authentication (auth token) is set to REQUIRED for this application. Please provide an auth token from your backend server or change the setting in the OneSignal dashboard."; - if (callback != null) - callback.onFailure(new EmailUpdateError(EmailErrorType.REQUIRES_EMAIL_AUTH, errorMessage)); - logger.error(errorMessage); - return; - } - - emailUpdateHandler = callback; - - String trimmedEmail = email.trim(); - - String internalEmailAuthHash = emailAuthHash; - if (internalEmailAuthHash != null) - internalEmailAuthHash = internalEmailAuthHash.toLowerCase(); - - getCurrentEmailSubscriptionState(appContext).setEmailAddress(trimmedEmail); - OneSignalStateSynchronizer.setEmail(trimmedEmail.toLowerCase(), internalEmailAuthHash); - } - - /** - * Call when user logs out of their account. - * This dissociates the device from the email address. - * This does not effect the subscription status of the email address itself. - */ - public static void logoutEmail() { - logoutEmail(null); - } - - public static void logoutEmail(@Nullable final EmailUpdateHandler callback) { - if (taskRemoteController.shouldQueueTaskForInit(OSTaskRemoteController.LOGOUT_EMAIL)) { - logger.error("Waiting for remote params. " + - "Moving " + OSTaskRemoteController.LOGOUT_EMAIL + " operation to a pending task queue."); - taskRemoteController.addTaskToQueue(new Runnable() { - @Override - public void run() { - logger.debug("Running " + OSTaskRemoteController.LOGOUT_EMAIL + " operation from pending task queue."); - logoutEmail(callback); - } - }); - return; - } - - // If applicable, check if the user provided privacy consent - if (shouldLogUserPrivacyConsentErrorMessageForMethodName(OSTaskRemoteController.LOGOUT_EMAIL)) - return; - - if (getEmailId() == null) { - final String message = "logoutEmail not valid as email was not set or already logged out!"; - if (callback != null) - callback.onFailure(new EmailUpdateError(EmailErrorType.INVALID_OPERATION, message)); - logger.error(message); - return; - } - - emailLogoutHandler = callback; - OneSignalStateSynchronizer.logoutEmail(); - } - - public static void setLanguage(@NonNull final String language) { - setLanguage(language, null); - } - - public static void setLanguage(@NonNull final String language, @Nullable final OSSetLanguageCompletionHandler completionCallback) { - if (taskRemoteController.shouldQueueTaskForInit(OSTaskRemoteController.SET_LANGUAGE)) { - logger.error("Waiting for remote params. " + - "Moving " + OSTaskRemoteController.SET_LANGUAGE + " operation to a pending task queue."); - taskRemoteController.addTaskToQueue(new Runnable() { - @Override - public void run() { - logger.debug("Running " + OSTaskRemoteController.SET_LANGUAGE + " operation from pending task queue."); - setLanguage(language, completionCallback); - } - }); - return; - } - - OSDeviceInfoCompletionHandler deviceInfoCompletionHandler = null; - - if(completionCallback != null) { - deviceInfoCompletionHandler = new OSDeviceInfoCompletionHandler() { - @Override - public void onSuccess(String results) { - completionCallback.onSuccess(results); - } - - @Override - public void onFailure(OSDeviceInfoError error) { - OSLanguageError languageError = new OSLanguageError(error.errorCode, error.message); - completionCallback.onFailure(languageError); - } - }; - } - - if (shouldLogUserPrivacyConsentErrorMessageForMethodName(OSTaskRemoteController.SET_LANGUAGE)) - return; - - LanguageProviderAppDefined languageProviderAppDefined = new LanguageProviderAppDefined(preferences); - languageProviderAppDefined.setLanguage(language); - languageContext.setStrategy(languageProviderAppDefined); - - try { - JSONObject deviceInfo = new JSONObject(); - deviceInfo.put("language", languageContext.getLanguage()); - OneSignalStateSynchronizer.updateDeviceInfo(deviceInfo, deviceInfoCompletionHandler); - } catch (JSONException exception) { - exception.printStackTrace(); - } - } - - public static void setExternalUserId(@NonNull final String externalId) { - setExternalUserId(externalId, null, null); - } - - public static void setExternalUserId(@NonNull final String externalId, @Nullable final OSExternalUserIdUpdateCompletionHandler completionCallback) { - setExternalUserId(externalId, null, completionCallback); - } - - public static void setExternalUserId(@NonNull final String externalId, @Nullable final String externalIdAuthHash) { - setExternalUserId(externalId, externalIdAuthHash, null); - } - - public static void setExternalUserId(@NonNull final String externalId, @Nullable final String externalIdAuthHash, @Nullable final OSExternalUserIdUpdateCompletionHandler completionCallback) { - if (taskRemoteController.shouldQueueTaskForInit(OSTaskRemoteController.SET_EXTERNAL_USER_ID)) { - logger.error("Waiting for remote params. " + - "Moving " + OSTaskRemoteController.SET_EXTERNAL_USER_ID + " operation to a pending task queue."); - taskRemoteController.addTaskToQueue(new Runnable() { - @Override - public void run() { - logger.debug("Running " + OSTaskRemoteController.SET_EXTERNAL_USER_ID + " operation from pending task queue."); - setExternalUserId(externalId, externalIdAuthHash, completionCallback); - } - }); - return; - } - - if (shouldLogUserPrivacyConsentErrorMessageForMethodName("setExternalUserId()")) - return; - - if (externalId == null) { - logger.warning("External id can't be null, set an empty string to remove an external id"); - return; - } - - // Empty external Id is used for remove external id, in this case auth hash will be pop from saved UserState - if (!externalId.isEmpty() && getRemoteParams() != null && getRemoteParams().useUserIdAuth && (externalIdAuthHash == null || externalIdAuthHash.length() == 0)) { - String errorMessage = "External Id authentication (auth token) is set to REQUIRED for this application. Please provide an auth token from your backend server or change the setting in the OneSignal dashboard."; - if (completionCallback != null) - completionCallback.onFailure(new ExternalIdError(ExternalIdErrorType.REQUIRES_EXTERNAL_ID_AUTH, errorMessage)); - logger.error(errorMessage); - return; - } - - String lowerCaseIdAuthHash = externalIdAuthHash; - if (lowerCaseIdAuthHash != null) - lowerCaseIdAuthHash = externalIdAuthHash.toLowerCase(); - - try { - OneSignalStateSynchronizer.setExternalUserId(externalId, lowerCaseIdAuthHash, completionCallback); - } catch (JSONException exception) { - String operation = externalId.equals("") ? "remove" : "set"; - logger.error("Attempted to " + operation + " external ID but encountered a JSON exception"); - exception.printStackTrace(); - } - } - - public static void removeExternalUserId() { - if (shouldLogUserPrivacyConsentErrorMessageForMethodName("removeExternalUserId()")) - return; - - removeExternalUserId(null); - } - - public static void removeExternalUserId(final OSExternalUserIdUpdateCompletionHandler completionHandler) { - if (shouldLogUserPrivacyConsentErrorMessageForMethodName("removeExternalUserId()")) - return; - - // to remove the external user ID, the API requires an empty string - setExternalUserId("", completionHandler); - } - - /** - * Tag a user based on an app event of your choosing so later you can create - * OneSignal Segments - * to target these users. - * - * @see OneSignal#sendTags to set more than one tag on a user at a time. - * - * @param key Key of your chossing to create or update - * @param value Value to set on the key. Note: Passing in a blank {@code String} deletes - * the key. - * @see OneSignal#deleteTag - * @see OneSignal#deleteTags - */ - public static void sendTag(final String key, final String value) { - if (taskRemoteController.shouldQueueTaskForInit(OSTaskRemoteController.SEND_TAG)) { - logger.error("Waiting for remote params. " + - "Moving " + OSTaskRemoteController.SEND_TAG + " operation to a pending task queue."); - taskRemoteController.addTaskToQueue(new Runnable() { - @Override - public void run() { - logger.debug("Running " + OSTaskRemoteController.SEND_TAG + " operation from pending task queue."); - sendTag(key, value); - } - }); - return; - } - - // If applicable, check if the user provided privacy consent - if (shouldLogUserPrivacyConsentErrorMessageForMethodName(OSTaskRemoteController.SEND_TAG)) - return; - - try { - sendTags(new JSONObject().put(key, value)); - } catch (JSONException t) { - t.printStackTrace(); - } - } - - public static void sendTags(String jsonString) { - try { - sendTags(new JSONObject(jsonString)); - } catch (JSONException t) { - Log(LOG_LEVEL.ERROR, "Generating JSONObject for sendTags failed!", t); - } - } - - /** - * Tag a user based on an app event of your choosing so later you can create - * OneSignal Segments - * to target these users. - * @param keyValues Key value pairs of your choosing to create or update. Note: - * Passing in a blank String as a value deletes a key. - * @see OneSignal#deleteTag - * @see OneSignal#deleteTags - */ - public static void sendTags(final JSONObject keyValues) { - sendTags(keyValues, null); - } - - /** - * Tag a user based on an app event of your choosing so later you can create - * OneSignal Segments - * to target these users. - * - * NOTE: The ChangeTagsUpdateHandler will not be called under all circumstances. It can also take - * more than 5 seconds in some cases to be called, so please do not block any user action - * based on this callback. - * @param keyValues Key value pairs of your choosing to create or update. Note: - * Passing in a blank String as a value deletes a key. - * @see OneSignal#deleteTag - * @see OneSignal#deleteTags - * - */ - public static void sendTags(final JSONObject keyValues, final ChangeTagsUpdateHandler changeTagsUpdateHandler) { - if (taskRemoteController.shouldQueueTaskForInit(OSTaskRemoteController.SEND_TAGS)) { - logger.error("Waiting for remote params. " + - "Moving " + OSTaskRemoteController.SEND_TAGS + " operation to a pending task queue."); - taskRemoteController.addTaskToQueue(new Runnable() { - @Override - public void run() { - logger.debug("Running " + OSTaskRemoteController.SEND_TAGS + " operation from pending task queue."); - sendTags(keyValues, changeTagsUpdateHandler); - } - }); - return; - } - - // If applicable, check if the user provided privacy consent - if (shouldLogUserPrivacyConsentErrorMessageForMethodName(OSTaskRemoteController.SEND_TAGS)) - return; - - Runnable sendTagsRunnable = new Runnable() { - @Override - public void run() { - if (keyValues == null) { - logger.error("Attempted to send null tags"); - if (changeTagsUpdateHandler != null) - changeTagsUpdateHandler.onFailure(new SendTagsError(-1, "Attempted to send null tags")); - return; - } - - JSONObject existingKeys = OneSignalStateSynchronizer.getTags(false).result; - JSONObject toSend = new JSONObject(); - - Iterator keys = keyValues.keys(); - String key; - Object value; - - while (keys.hasNext()) { - key = keys.next(); - try { - value = keyValues.opt(key); - if (value instanceof JSONArray || value instanceof JSONObject) - Log(LOG_LEVEL.ERROR, "Omitting key '" + key + "'! sendTags DO NOT supported nested values!"); - else if (keyValues.isNull(key) || "".equals(value)) { - if (existingKeys != null && existingKeys.has(key)) - toSend.put(key, ""); - } - else - toSend.put(key, value.toString()); - } - catch (Throwable t) {} - } - - if (!toSend.toString().equals("{}")) { - logger.debug("Available tags to send: " + toSend.toString()); - OneSignalStateSynchronizer.sendTags(toSend, changeTagsUpdateHandler); - } else { - logger.debug("Send tags ended successfully"); - if (changeTagsUpdateHandler != null) - changeTagsUpdateHandler.onSuccess(existingKeys); - } - } - }; - - // If pendingTaskExecutor is running, there might be sendTags tasks running, use it to run sendTagsRunnable to keep order call - if (taskRemoteController.shouldRunTaskThroughQueue()) { - logger.debug("Sending " + OSTaskRemoteController.SEND_TAGS + " operation to pending task queue."); - taskRemoteController.addTaskToQueue(sendTagsRunnable); - return; - } - - sendTagsRunnable.run(); - } - - public static void postNotification(String json, final PostNotificationResponseHandler handler) { - try { - postNotification(new JSONObject(json), handler); - } catch (JSONException e) { - Log(LOG_LEVEL.ERROR, "Invalid postNotification JSON format: " + json); - } - } - - /** - * Allows you to send notifications from user to user or schedule ones in the future to be delivered - * to the current device. - *

- * Note: You can only use {@code include_player_ids} as a targeting parameter from your app. - * Other target options such as {@code tags} and {@code included_segments} require your OneSignal - * App REST API key which can only be used from your server. - * - * @param json Contains notification options, see OneSignal | Create Notification - * POST call for all options. - * @param handler a {@link PostNotificationResponseHandler} object to receive the request result - */ - public static void postNotification(JSONObject json, final PostNotificationResponseHandler handler) { - - // If applicable, check if the user provided privacy consent - if (shouldLogUserPrivacyConsentErrorMessageForMethodName("postNotification()")) - return; - - try { - if (!json.has("app_id")) - json.put("app_id", getSavedAppId()); - - // app_id will not be set if init was never called. - if (!json.has("app_id")) { - if (handler != null) - handler.onFailure(new JSONObject().put("error", "Missing app_id")); - return; - } - - OneSignalRestClient.post("notifications/", json, new OneSignalRestClient.ResponseHandler() { - @Override - public void onSuccess(String response) { - logger.debug("HTTP create notification success: " + (response != null ? response : "null")); - if (handler != null) { - try { - JSONObject jsonObject = new JSONObject(response); - if (jsonObject.has("errors")) - handler.onFailure(jsonObject); - else - handler.onSuccess(new JSONObject(response)); - } catch (Throwable t) { - t.printStackTrace(); - } - } - } - - @Override - void onFailure(int statusCode, String response, Throwable throwable) { - logHttpError("create notification failed", statusCode, throwable, response); - if (handler != null) { - try { - if (statusCode == 0) - response = "{\"error\": \"HTTP no response error\"}"; - - handler.onFailure(new JSONObject(response)); - } catch (Throwable t) { - try { - handler.onFailure(new JSONObject("{\"error\": \"Unknown response!\"}")); - } catch (JSONException e) { - e.printStackTrace(); - } - } - } - } - }); - } catch (JSONException e) { - logger.error("HTTP create notification json exception!", e); - if (handler != null) { - try { - handler.onFailure(new JSONObject("{'error': 'HTTP create notification json exception!'}")); - } catch (JSONException e1) { - e1.printStackTrace(); - } - } - } - } - - /** - * Retrieve a list of tags that have been set on the user frm the OneSignal server. - * @param getTagsHandler an instance of {@link OSGetTagsHandler}. - *
- * Calls {@link OSGetTagsHandler#tagsAvailable(JSONObject) tagsAvailable} once the tags are available - */ - public static void getTags(final OSGetTagsHandler getTagsHandler) { - if (taskRemoteController.shouldQueueTaskForInit(OSTaskRemoteController.GET_TAGS)) { - logger.error("Waiting for remote params. " + - "Moving " + OSTaskRemoteController.GET_TAGS + " operation to a pending queue."); - taskRemoteController.addTaskToQueue(new Runnable() { - @Override - public void run() { - logger.debug("Running " + OSTaskRemoteController.GET_TAGS + " operation from pending queue."); - getTags(getTagsHandler); - } - }); - return; - } - - // If applicable, check if the user provided privacy consent - if (shouldLogUserPrivacyConsentErrorMessageForMethodName(OSTaskRemoteController.GET_TAGS)) - return; - - if (getTagsHandler == null) { - logger.error("getTags called with null GetTagsHandler!"); - return; - } - - new Thread(new Runnable() { - @Override - public void run() { - synchronized (pendingGetTagsHandlers) { - pendingGetTagsHandlers.add(getTagsHandler); - - // If there is an existing in-flight request, we should return - // since there's no point in making a duplicate runnable - if (pendingGetTagsHandlers.size() > 1) return; - } - - runGetTags(); - } - }, "OS_GETTAGS").start(); - } - - private static void runGetTags() { - if (getUserId() == null) { - logger.warning("getTags called under a null user!"); - return; - } - - internalFireGetTagsCallbacks(); - } - - private static void internalFireGetTagsCallbacks() { - synchronized (pendingGetTagsHandlers) { - if (pendingGetTagsHandlers.size() == 0) return; - } - - new Thread(new Runnable() { - @Override - public void run() { - final UserStateSynchronizer.GetTagsResult tags = OneSignalStateSynchronizer.getTags(!getTagsCall); - if (tags.serverSuccess) getTagsCall = true; - - synchronized (pendingGetTagsHandlers) { - for (OSGetTagsHandler handler : pendingGetTagsHandlers) { - handler.tagsAvailable(tags.result == null || tags.toString().equals("{}") ? null : tags.result); - } - - pendingGetTagsHandlers.clear(); - } - } - }, "OS_GETTAGS_CALLBACK").start(); - } - - /** - * Deletes a single tag that was previously set on a user with - * @see OneSignal#sendTag or {@link #sendTags(JSONObject)}. - * @see OneSignal#deleteTags if you need to delete - * more than one. - * @param key Key to remove. - */ - public static void deleteTag(String key) { - deleteTag(key, null); - } - - public static void deleteTag(String key, ChangeTagsUpdateHandler handler) { - //if applicable, check if the user provided privacy consent - if (shouldLogUserPrivacyConsentErrorMessageForMethodName("deleteTag()")) - return; - - Collection tempList = new ArrayList<>(1); - tempList.add(key); - deleteTags(tempList, handler); - } - - /** - * Deletes one or more tags that were previously set on a user with - * @see OneSignal#sendTag or {@link #sendTags(JSONObject)}. - * @param keys Keys to remove. - */ - public static void deleteTags(Collection keys) { - deleteTags(keys, null); - } - - public static void deleteTags(Collection keys, ChangeTagsUpdateHandler handler) { - //if applicable, check if the user provided privacy consent - if (shouldLogUserPrivacyConsentErrorMessageForMethodName("deleteTags()")) - return; - - try { - JSONObject jsonTags = new JSONObject(); - for (String key : keys) - jsonTags.put(key, ""); - - sendTags(jsonTags, handler); - } catch (Throwable t) { - Log(LOG_LEVEL.ERROR, "Failed to generate JSON for deleteTags.", t); - } - } - - public static void deleteTags(String jsonArrayString) { - deleteTags(jsonArrayString, null); - } - - public static void deleteTags(String jsonArrayString, ChangeTagsUpdateHandler handler) { - try { - deleteTags(new JSONArray(jsonArrayString), handler); - } catch (Throwable t) { - Log(LOG_LEVEL.ERROR, "Failed to generate JSON for deleteTags.", t); - } - } - - public static void deleteTags(JSONArray jsonArray, ChangeTagsUpdateHandler handler) { - //if applicable, check if the user provided privacy consent - if (shouldLogUserPrivacyConsentErrorMessageForMethodName("deleteTags()")) - return; - - try { - JSONObject jsonTags = new JSONObject(); - - for (int i = 0; i < jsonArray.length(); i++) - jsonTags.put(jsonArray.getString(i), ""); - - sendTags(jsonTags, handler); - } catch (Throwable t) { - Log(LOG_LEVEL.ERROR, "Failed to generate JSON for deleteTags.", t); - } - } - - static void sendPurchases(JSONArray purchases, boolean newAsExisting, OneSignalRestClient.ResponseHandler responseHandler) { - - //if applicable, check if the user provided privacy consent - if (shouldLogUserPrivacyConsentErrorMessageForMethodName("sendPurchases()")) - return; - - if (getUserId() == null) { - iapUpdateJob = new IAPUpdateJob(purchases); - iapUpdateJob.newAsExisting = newAsExisting; - iapUpdateJob.restResponseHandler = responseHandler; - - return; - } - - try { - JSONObject jsonBody = new JSONObject(); - jsonBody.put("app_id", getSavedAppId()); - if (newAsExisting) - jsonBody.put("existing", true); - jsonBody.put("purchases", purchases); - - OneSignalStateSynchronizer.sendPurchases(jsonBody, responseHandler); - } catch (Throwable t) { - Log(LOG_LEVEL.ERROR, "Failed to generate JSON for sendPurchases.", t); - } - } - - private static void runNotificationOpenedCallback(final JSONArray dataArray) { - if (notificationOpenedHandler == null) { - unprocessedOpenedNotifs.add(dataArray); - return; - } - - OSNotificationOpenedResult openedResult = generateNotificationOpenedResult(dataArray); - addEntryStateListener(openedResult, appEntryState); - fireNotificationOpenedHandler(openedResult); - } - - // Also called for received but OSNotification is extracted from it. - @NonNull - private static OSNotificationOpenedResult generateNotificationOpenedResult(JSONArray jsonArray) { - int jsonArraySize = jsonArray.length(); - - boolean firstMessage = true; - int androidNotificationId = jsonArray.optJSONObject(0).optInt(BUNDLE_KEY_ANDROID_NOTIFICATION_ID); - - List groupedNotifications = new ArrayList<>(); - String actionSelected = null; - JSONObject payload = null; - - for (int i = 0; i < jsonArraySize; i++) { - try { - payload = jsonArray.getJSONObject(i); - - if (actionSelected == null && payload.has(BUNDLE_KEY_ACTION_ID)) - actionSelected = payload.optString(BUNDLE_KEY_ACTION_ID, null); - - if (firstMessage) - firstMessage = false; - else { - groupedNotifications.add(new OSNotification(payload)); - } - } catch (Throwable t) { - Log(LOG_LEVEL.ERROR, "Error parsing JSON item " + i + "/" + jsonArraySize + " for callback.", t); - } - } - - OSNotificationAction.ActionType actionType = actionSelected != null ? OSNotificationAction.ActionType.ActionTaken : OSNotificationAction.ActionType.Opened; - OSNotificationAction notificationAction = new OSNotificationAction(actionType, actionSelected); - - OSNotification notification = new OSNotification(groupedNotifications, payload, androidNotificationId); - return new OSNotificationOpenedResult(notification, notificationAction); - } - - private static void fireNotificationOpenedHandler(final OSNotificationOpenedResult openedResult) { - // TODO: Is there a reason we need the opened handler to be fired from main thread? - - // TODO: Once the NotificationOpenedHandler gets a Worker, we should make sure we add a catch - // like we have implemented for the OSRemoteNotificationReceivedHandler and NotificationWillShowInForegroundHandlers - CallbackThreadManager.Companion.runOnPreferred(new Runnable() { - @Override - public void run() { - notificationOpenedHandler.notificationOpened(openedResult); - } - }); - } - - /** - * Called when receiving FCM/ADM message after it has been displayed. - * Or right when it is received if it is a silent one - * If a NotificationExtenderService is present in the developers app this will not fire for silent notifications. - */ - static void handleNotificationReceived(OSNotificationGenerationJob notificationJob) { - try { - JSONObject jsonObject = new JSONObject(notificationJob.getJsonPayload().toString()); - jsonObject.put(BUNDLE_KEY_ANDROID_NOTIFICATION_ID, notificationJob.getAndroidId()); - - OSNotificationOpenedResult openResult = generateNotificationOpenedResult(newJsonArray(jsonObject)); - if (trackFirebaseAnalytics != null && getFirebaseAnalyticsEnabled()) - trackFirebaseAnalytics.trackReceivedEvent(openResult); - - } catch (JSONException e) { - e.printStackTrace(); - } - } - - /** - * Checks if the app is in the background - * Checks if notificationWillShowInForegroundHandler is setup - *

- * @see OSNotificationWillShowInForegroundHandler - */ - static boolean shouldFireForegroundHandlers(OSNotificationGenerationJob notificationJob) { - if (!isInForeground()) { - OneSignal.onesignalLog(LOG_LEVEL.INFO, "App is in background, show notification"); - return false; - } - - if (notificationWillShowInForegroundHandler == null) { - OneSignal.onesignalLog(LOG_LEVEL.INFO, "No NotificationWillShowInForegroundHandler setup, show notification"); - return false; - } - - // Notification is restored. Don't fire for restored notifications. - if (notificationJob.isRestoring()) { - OneSignal.onesignalLog(LOG_LEVEL.INFO, "Not firing notificationWillShowInForegroundHandler for restored notifications"); - return false; - } - - return true; - } - - /** - * Responsible for firing the notificationWillShowInForegroundHandler - *

- * @see OSNotificationWillShowInForegroundHandler - */ - static void fireForegroundHandlers(OSNotificationController notificationController) { - OneSignal.onesignalLog(OneSignal.LOG_LEVEL.INFO, "Fire notificationWillShowInForegroundHandler"); - - OSNotificationReceivedEvent notificationReceivedEvent = notificationController.getNotificationReceivedEvent(); - try { - OneSignal.notificationWillShowInForegroundHandler.notificationWillShowInForeground(notificationReceivedEvent); - } catch (Throwable t) { - OneSignal.onesignalLog(LOG_LEVEL.ERROR, "Exception thrown while notification was being processed for display by notificationWillShowInForegroundHandler, showing notification in foreground!"); - notificationReceivedEvent.complete(notificationReceivedEvent.getNotification()); - throw t; - } - } - - /** - * Method called when opening a notification - */ - static void handleNotificationOpen(final Activity context, final JSONArray data, @Nullable final String notificationId) { - // If applicable, check if the user provided privacy consent - if (shouldLogUserPrivacyConsentErrorMessageForMethodName(null)) - return; - - notificationOpenedRESTCall(context, data); - - if (trackFirebaseAnalytics != null && getFirebaseAnalyticsEnabled()) - trackFirebaseAnalytics.trackOpenedEvent(generateNotificationOpenedResult(data)); - - if (shouldInitDirectSessionFromNotificationOpen(context, data)) { - applicationOpenedByNotification(notificationId); - } - - openDestinationActivity(context, data); - - runNotificationOpenedCallback(data); - } - - static void openDestinationActivity( - @NonNull final Activity activity, - @NonNull final JSONArray pushPayloads - ) { - try { - // Always use the top most notification if user tapped on the summary notification - JSONObject firstPayloadItem = pushPayloads.getJSONObject(0); - GenerateNotificationOpenIntent intentGenerator = GenerateNotificationOpenIntentFromPushPayload.INSTANCE.create( - activity, - firstPayloadItem - ); - - Intent intent = intentGenerator.getIntentVisible(); - if (intent != null) { - logger.info("SDK running startActivity with Intent: " + intent); - activity.startActivity(intent); - } - else { - logger.info("SDK not showing an Activity automatically due to it's settings."); - } - } catch (JSONException e) { - e.printStackTrace(); - } - } - - private static boolean shouldInitDirectSessionFromNotificationOpen(Activity context, final JSONArray data) { - if (inForeground) { - return false; - } - - try { - JSONObject interactedNotificationData = data.getJSONObject(0); - return new OSNotificationOpenBehaviorFromPushPayload( - context, - interactedNotificationData - ).getShouldOpenApp(); - } catch (JSONException e) { - e.printStackTrace(); - } - return true; - } - - static void applicationOpenedByNotification(@Nullable final String notificationId) { - // We want to set the app entry state to NOTIFICATION_CLICK when coming from background - appEntryState = AppEntryAction.NOTIFICATION_CLICK; - sessionManager.onDirectInfluenceFromNotificationOpen(appEntryState, notificationId); - } - - private static void notificationOpenedRESTCall(Context inContext, JSONArray dataArray) { - for (int i = 0; i < dataArray.length(); i++) { - try { - JSONObject data = dataArray.getJSONObject(i); - JSONObject customJson = new JSONObject(data.optString("custom", null)); - - String notificationId = customJson.optString("i", null); - // Prevent duplicate calls from summary notifications. - // Also needed if developer overrides setAutoCancel. - if (postedOpenedNotifIds.contains(notificationId)) - continue; - postedOpenedNotifIds.add(notificationId); - - JSONObject jsonBody = new JSONObject(); - jsonBody.put("app_id", getSavedAppId(inContext)); - jsonBody.put("player_id", getSavedUserId(inContext)); - jsonBody.put("opened", true); - jsonBody.put("device_type", osUtils.getDeviceType()); - - OneSignalRestClient.put("notifications/" + notificationId, jsonBody, new OneSignalRestClient.ResponseHandler() { - @Override - void onFailure(int statusCode, String response, Throwable throwable) { - logHttpError("sending Notification Opened Failed", statusCode, throwable, response); - } - }); - } - catch(Throwable t){ // JSONException and UnsupportedEncodingException - Log(LOG_LEVEL.ERROR, "Failed to generate JSON to send notification opened.", t); - } - } - } - - private static void saveAppId(String appId) { - if (appContext == null) - return; - - OneSignalPrefs.saveString( - OneSignalPrefs.PREFS_ONESIGNAL, - OneSignalPrefs.PREFS_GT_APP_ID, - appId); - } - - static String getSavedAppId() { - return getSavedAppId(appContext); - } - - private static String getSavedAppId(Context inContext) { - if (inContext == null) - return null; - - return OneSignalPrefs.getString( - OneSignalPrefs.PREFS_ONESIGNAL, - OneSignalPrefs.PREFS_GT_APP_ID, - null); - } - - private static String getSavedUserId(Context inContext) { - if (inContext == null) - return null; - - return OneSignalPrefs.getString( - OneSignalPrefs.PREFS_ONESIGNAL, - OneSignalPrefs.PREFS_GT_PLAYER_ID, - null); - } - - static boolean hasUserId() { - return getUserId() != null; - } - - static String getUserId() { - if (userId == null && appContext != null) - userId = getSavedUserId(appContext); - - return userId; - } - - static void saveUserId(String id) { - userId = id; - if (appContext == null) - return; - - OneSignalPrefs.saveString( - OneSignalPrefs.PREFS_ONESIGNAL, - OneSignalPrefs.PREFS_GT_PLAYER_ID, - userId); - } - - static boolean hasEmailId() { - return !TextUtils.isEmpty(emailId); - } - - static String getEmailId() { - if (emailId == null && appContext != null) { - emailId = OneSignalPrefs.getString( - OneSignalPrefs.PREFS_ONESIGNAL, - OneSignalPrefs.PREFS_OS_EMAIL_ID, - null); - } - - if (TextUtils.isEmpty(emailId)) - return null; - - return emailId; - } - - static void saveEmailId(String id) { - emailId = id; - if (appContext == null) - return; - - OneSignalPrefs.saveString( - OneSignalPrefs.PREFS_ONESIGNAL, - OneSignalPrefs.PREFS_OS_EMAIL_ID, - "".equals(emailId) ? null : emailId); - } - - static boolean hasSMSlId() { - return !TextUtils.isEmpty(smsId); - } - - static String getSMSId() { - if (smsId == null && appContext != null) { - smsId = OneSignalPrefs.getString( - OneSignalPrefs.PREFS_ONESIGNAL, - OneSignalPrefs.PREFS_OS_SMS_ID, - null); - } - - if (TextUtils.isEmpty(smsId)) - return null; - - return smsId; - } - - static void saveSMSId(String id) { - smsId = id; - if (appContext == null) - return; - - OneSignalPrefs.saveString( - OneSignalPrefs.PREFS_ONESIGNAL, - OneSignalPrefs.PREFS_OS_SMS_ID, - "".equals(smsId) ? null : smsId); - } - - // Called when a player id is returned from OneSignal - // Updates anything else that might have been waiting for this id. - static void updateUserIdDependents(String userId) { - saveUserId(userId); - internalFireGetTagsCallbacks(); - - getCurrentSubscriptionState(appContext).setUserId(userId); - - if (iapUpdateJob != null) { - sendPurchases(iapUpdateJob.toReport, iapUpdateJob.newAsExisting, iapUpdateJob.restResponseHandler); - iapUpdateJob = null; - } - - OneSignalStateSynchronizer.refreshSecondaryChannelState(); - } - - static void updateEmailIdDependents(String emailId) { - saveEmailId(emailId); - getCurrentEmailSubscriptionState(appContext).setEmailUserId(emailId); - try { - JSONObject updateJson = new JSONObject().put("parent_player_id", emailId); - OneSignalStateSynchronizer.updatePushState(updateJson); - } catch (JSONException e) { - e.printStackTrace(); - } - } - - static void updateSMSIdDependents(String smsId) { - saveSMSId(smsId); - getCurrentSMSSubscriptionState(appContext).setSMSUserId(smsId); - } - - // Start Remote params getters - static boolean getFirebaseAnalyticsEnabled() { - return remoteParamController.getFirebaseAnalyticsEnabled(); - } - - static boolean getClearGroupSummaryClick() { - return remoteParamController.getClearGroupSummaryClick(); - } - - static boolean getDisableGMSMissingPrompt() { - return remoteParamController.isGMSMissingPromptDisable(); - } - - public static boolean isLocationShared() { - return remoteParamController.isLocationShared(); - } - - static boolean isUserPrivacyConsentRequired() { - return remoteParamController.isPrivacyConsentRequired(); - } - // End Remote params getters - - static void setLastSessionTime(long time) { - logger.debug("Last session time set to: " + time); - OneSignalPrefs.saveLong( - OneSignalPrefs.PREFS_ONESIGNAL, - OneSignalPrefs.PREFS_OS_LAST_SESSION_TIME, - time); - } - - private static long getLastSessionTime() { - return OneSignalPrefs.getLong( - OneSignalPrefs.PREFS_ONESIGNAL, - OneSignalPrefs.PREFS_OS_LAST_SESSION_TIME, - -31 * 1000L); - } - - /** - * You can call this method with {@code true} to opt users out of receiving all notifications through - * OneSignal. You can pass {@code false} later to opt users back into notifications. - * @param disable whether to subscribe the user to notifications or not - */ - public static void disablePush(final boolean disable) { - if (taskRemoteController.shouldQueueTaskForInit(OSTaskRemoteController.SET_SUBSCRIPTION)) { - logger.error("Waiting for remote params. " + - "Moving " + OSTaskRemoteController.SET_SUBSCRIPTION + " operation to a pending queue."); - taskRemoteController.addTaskToQueue(new Runnable() { - @Override - public void run() { - logger.debug("Running " + OSTaskRemoteController.SET_SUBSCRIPTION + " operation from pending queue."); - disablePush(disable); - } - }); - return; - } - - // If applicable, check if the user provided privacy consent - if (shouldLogUserPrivacyConsentErrorMessageForMethodName(OSTaskRemoteController.SET_SUBSCRIPTION)) - return; - - getCurrentSubscriptionState(appContext).setPushDisabled(disable); - OneSignalStateSynchronizer.setSubscription(!disable); - } - - - /** - * This method will be replaced by remote params set - */ - public static void disableGMSMissingPrompt(final boolean promptDisable) { - // Already set by remote params - if (getRemoteParamController().hasDisableGMSMissingPromptKey()) - return; - - getRemoteParamController().saveGMSMissingPromptDisable(promptDisable); - } - - /** - * This method will be replaced by remote params set - */ - public static void setLocationShared(final boolean enable) { - if (taskRemoteController.shouldQueueTaskForInit(OSTaskRemoteController.SET_LOCATION_SHARED)) { - logger.error("Waiting for remote params. " + - "Moving " + OSTaskRemoteController.SET_LOCATION_SHARED + " operation to a pending task queue."); - taskRemoteController.addTaskToQueue(new Runnable() { - @Override - public void run() { - logger.debug("Running " + OSTaskRemoteController.SET_LOCATION_SHARED + " operation from pending task queue."); - setLocationShared(enable); - } - }); - return; - } - - // Already set by remote params - if (getRemoteParamController().hasLocationKey()) - return; - - startLocationShared(enable); - } - - static void startLocationShared(boolean enable) { - logger.debug("OneSignal startLocationShared: " + enable); - getRemoteParamController().saveLocationShared(enable); - - if (!enable) { - logger.debug("OneSignal is shareLocation set false, clearing last location!"); - OneSignalStateSynchronizer.clearLocation(); - } - } - - /** - * Use this method to manually prompt the user for location permissions. - * This allows for geotagging so you send notifications to users based on location. - *

- * Make sure you have one of the following permission in your {@code AndroidManifest.xml} as well. - *
- * {@code } - *
- * {@code } - * - *

Be aware of best practices regarding asking permissions on Android: - * - * Requesting Permissions | Android Developers - * - * - * @see Permission Requests | OneSignal Docs - */ - public static void promptLocation() { - promptLocation(null, false); - } - - static void promptLocation(@Nullable final OSPromptActionCompletionCallback callback, final boolean fallbackToSettings) { - if (taskRemoteController.shouldQueueTaskForInit(OSTaskRemoteController.PROMPT_LOCATION)) { - logger.error("Waiting for remote params. " + - "Moving " + OSTaskRemoteController.PROMPT_LOCATION + " operation to a pending queue."); - taskRemoteController.addTaskToQueue(new Runnable() { - @Override - public void run() { - logger.debug("Running " + OSTaskRemoteController.PROMPT_LOCATION + " operation from pending queue."); - promptLocation(callback, fallbackToSettings); - } - }); - return; - } - - // If applicable, check if the user provided privacy consent - if (shouldLogUserPrivacyConsentErrorMessageForMethodName(OSTaskRemoteController.PROMPT_LOCATION)) - return; - - LocationController.LocationHandler locationHandler = new LocationController.LocationPromptCompletionHandler() { - @Override - public LocationController.PermissionType getType() { - return LocationController.PermissionType.PROMPT_LOCATION; - } - - @Override - public void onComplete(LocationController.LocationPoint point) { - //if applicable, check if the user provided privacy consent - if (shouldLogUserPrivacyConsentErrorMessageForMethodName(OSTaskRemoteController.PROMPT_LOCATION)) - return; - - if (point != null) - OneSignalStateSynchronizer.updateLocation(point); - } - - @Override - void onAnswered(OneSignal.PromptActionResult result) { - super.onAnswered(result); - if (callback != null) - callback.onCompleted(result); - } - }; - - LocationController.getLocation(appContext, true, fallbackToSettings, locationHandler); - } - - /** - * On Android 13 shows the system notification permission prompt to enable displaying - * notifications. This is required for apps that target Android API level 33 / "Tiramisu" - * to subscribe the device for push notifications. - */ - public static void promptForPushNotifications() { - promptForPushNotifications(false); - } - - /** - * On Android 13 shows the system notification permission prompt to enable displaying - * notifications. This is required for apps that target Android API level 33 / "Tiramisu" - * to subscribe the device for push notifications. - * - * @param fallbackToSettings whether to show a Dialog to direct users to the App's notification - * settings if they have declined before. - */ - public static void promptForPushNotifications(boolean fallbackToSettings) { - promptForPushNotifications(fallbackToSettings, null); - } - - /** - * On Android 13 shows the system notification permission prompt to enable displaying - * notifications. This is required for apps that target Android API level 33 / "Tiramisu" - * to subscribe the device for push notifications. - * - * @param fallbackToSettings whether to show a Dialog to direct users to the App's notification - * settings if they have declined before. - * @param handler fires when the user declines ore accepts the notification permission prompt. - */ - public static void promptForPushNotifications( - boolean fallbackToSettings, - @Nullable PromptForPushNotificationPermissionResponseHandler handler - ) { - NotificationPermissionController.INSTANCE.prompt(fallbackToSettings, handler); - } - - /** - * Removes all OneSignal notifications from the Notification Shade. If you just use - * {@link NotificationManager#cancelAll()}, OneSignal notifications will be restored when - * your app is restarted. - */ - public static void clearOneSignalNotifications() { - if (taskRemoteController.shouldQueueTaskForInit(OSTaskRemoteController.CLEAR_NOTIFICATIONS) || notificationDataController == null) { - logger.error("Waiting for remote params. " + - "Moving " + OSTaskRemoteController.CLEAR_NOTIFICATIONS + " operation to a pending queue."); - taskRemoteController.addTaskToQueue(new Runnable() { - @Override - public void run() { - logger.debug("Running " + OSTaskRemoteController.CLEAR_NOTIFICATIONS + " operation from pending queue."); - clearOneSignalNotifications(); - } - }); - return; - } - - notificationDataController.clearOneSignalNotifications(new WeakReference<>(appContext)); - } - - /** - * Cancels a single OneSignal notification based on its Android notification integer ID. Use - * instead of Android's {@link NotificationManager#cancel(int)}, otherwise the notification will be restored - * when your app is restarted. - * @param id - */ - public static void removeNotification(final int id) { - if (taskRemoteController.shouldQueueTaskForInit(OSTaskRemoteController.REMOVE_NOTIFICATION) || notificationDataController == null) { - logger.error("Waiting for remote params. " + - "Moving " + OSTaskRemoteController.REMOVE_NOTIFICATION + " operation to a pending queue."); - taskRemoteController.addTaskToQueue(new Runnable() { - @Override - public void run() { - logger.debug("Running " + OSTaskRemoteController.REMOVE_NOTIFICATION + " operation from pending queue."); - removeNotification(id); - } - }); - return; - } - - // If applicable, check if the user provided privacy consent - if (shouldLogUserPrivacyConsentErrorMessageForMethodName(OSTaskRemoteController.REMOVE_NOTIFICATION)) - return; - - notificationDataController.removeNotification(id, new WeakReference<>(appContext)); - } - - public static void removeGroupedNotifications(final String group) { - if (taskRemoteController.shouldQueueTaskForInit(OSTaskRemoteController.REMOVE_GROUPED_NOTIFICATIONS) || notificationDataController == null) { - logger.error("Waiting for remote params. " + - "Moving " + OSTaskRemoteController.REMOVE_GROUPED_NOTIFICATIONS + " operation to a pending queue."); - taskRemoteController.addTaskToQueue(new Runnable() { - @Override - public void run() { - logger.debug("Running " + OSTaskRemoteController.REMOVE_GROUPED_NOTIFICATIONS + " operation from pending queue."); - removeGroupedNotifications(group); - } - }); - return; - } - - // If applicable, check if the user provided privacy consent - if (shouldLogUserPrivacyConsentErrorMessageForMethodName(OSTaskRemoteController.REMOVE_GROUPED_NOTIFICATIONS)) - return; - - notificationDataController.removeGroupedNotifications(group, new WeakReference<>(appContext)); - } - - /** - * The {@link OSPermissionObserver#onOSPermissionChanged(OSPermissionStateChanges)} - * method will be fired on the passed-in object when a notification permission setting changes. - * This happens when the user enables or disables notifications for your app from the system - * settings outside of your app. Disable detection is supported on Android 4.4+ - *

- * Keep a reference - Make sure to hold a reference to your observable at the class level, - * otherwise it may not fire - *
- * Leak Safe - OneSignal holds a weak reference to your observer so it's guaranteed not to - * leak your {@code Activity} - * - * @param observer the instance of {@link OSPermissionObserver} that you want to process the permission - * changes within - */ - public static void addPermissionObserver(OSPermissionObserver observer) { - - if (appContext == null) { - logger.error("OneSignal.initWithContext has not been called. Could not add permission observer"); - return; - } - - getPermissionStateChangesObserver().addObserver(observer); - - if (getCurrentPermissionState(appContext).compare(getLastPermissionState(appContext))) - OSPermissionChangedInternalObserver.fireChangesToPublicObserver(getCurrentPermissionState(appContext)); - } - - public static void removePermissionObserver(OSPermissionObserver observer) { - if (appContext == null) { - logger.error("OneSignal.initWithContext has not been called. Could not modify permission observer"); - return; - } - - getPermissionStateChangesObserver().removeObserver(observer); - } - - /** - * The {@link OSSubscriptionObserver#onOSSubscriptionChanged(OSSubscriptionStateChanges)} - * method will be fired on the passed-in object when a notification subscription property changes. - *

- * This includes the following events: - *
- * - Getting a Registration ID (push token) from Google - *
- * - Getting a player/user ID from OneSignal - *
- * - {@link OneSignal#disablePush(boolean)} is called - *
- * - User disables or enables notifications - * @param observer the instance of {@link OSSubscriptionObserver} that acts as the observer - */ - public static void addSubscriptionObserver(OSSubscriptionObserver observer) { - if (appContext == null) { - logger.error("OneSignal.initWithContext has not been called. Could not add subscription observer"); - return; - } - - getSubscriptionStateChangesObserver().addObserver(observer); - - if (getCurrentSubscriptionState(appContext).compare(getLastSubscriptionState(appContext))) - OSSubscriptionChangedInternalObserver.fireChangesToPublicObserver(getCurrentSubscriptionState(appContext)); - } - - public static void removeSubscriptionObserver(OSSubscriptionObserver observer) { - if (appContext == null) { - logger.error("OneSignal.initWithContext has not been called. Could not modify subscription observer"); - return; - } - - getSubscriptionStateChangesObserver().removeObserver(observer); - } - - /** - * The {@link OSEmailSubscriptionObserver#onOSEmailSubscriptionChanged(OSEmailSubscriptionStateChanges)} - * method will be fired on the passed-in object when a email subscription property changes. - *

- * This includes the following events: - *
- * - Email address set - *
- * - Getting a player/user ID from OneSignal - * @param observer the instance of {@link OSSubscriptionObserver} that acts as the observer - */ - public static void addEmailSubscriptionObserver(@NonNull OSEmailSubscriptionObserver observer) { - - if (appContext == null) { - logger.error("OneSignal.initWithContext has not been called. Could not add email subscription observer"); - return; - } - - getEmailSubscriptionStateChangesObserver().addObserver(observer); - - if (getCurrentEmailSubscriptionState(appContext).compare(getLastEmailSubscriptionState(appContext))) - OSEmailSubscriptionChangedInternalObserver.fireChangesToPublicObserver(getCurrentEmailSubscriptionState(appContext)); - } - - public static void removeEmailSubscriptionObserver(@NonNull OSEmailSubscriptionObserver observer) { - if (appContext == null) { - logger.error("OneSignal.initWithContext has not been called. Could not modify email subscription observer"); - return; - } - - getEmailSubscriptionStateChangesObserver().removeObserver(observer); - } - - /** - * The {@link OSSMSSubscriptionObserver#onSMSSubscriptionChanged(OSSMSSubscriptionStateChanges)} - * method will be fired on the passed-in object when a sms subscription property changes. - *

- * This includes the following events: - *
- * - SMS number set - *
- * - Getting a player/user ID from OneSignal - * @param observer the instance of {@link OSSubscriptionObserver} that acts as the observer - */ - public static void addSMSSubscriptionObserver(@NonNull OSSMSSubscriptionObserver observer) { - if (appContext == null) { - logger.error("OneSignal.initWithContext has not been called. Could not add sms subscription observer"); - return; - } - - getSMSSubscriptionStateChangesObserver().addObserver(observer); - - if (getCurrentSMSSubscriptionState(appContext).compare(getLastSMSSubscriptionState(appContext))) - OSSMSSubscriptionChangedInternalObserver.fireChangesToPublicObserver(getCurrentSMSSubscriptionState(appContext)); - } - - public static void removeSMSSubscriptionObserver(@NonNull OSSMSSubscriptionObserver observer) { - if (appContext == null) { - logger.error("OneSignal.initWithContext has not been called. Could not modify sms subscription observer"); - return; - } - - getSMSSubscriptionStateChangesObserver().removeObserver(observer); - } - - /** In-App Message Triggers */ - - /** - * Allows you to set multiple trigger key/value pairs simultaneously with a Map - * Triggers are used for targeting in-app messages. - */ - public static void addTriggers(Map triggers) { - getInAppMessageController().addTriggers(triggers); - } - - /** - * Allows you to set an individual trigger key/value pair for in-app message targeting - */ - public static void addTrigger(String key, Object object) { - HashMap triggerMap = new HashMap<>(); - triggerMap.put(key, object); - - getInAppMessageController().addTriggers(triggerMap); - } - - /** Removes a list/collection of triggers from their keys with a Collection of Strings */ - public static void removeTriggersForKeys(Collection keys) { - getInAppMessageController().removeTriggersForKeys(keys); - } - - /** Removes a single trigger for the given key */ - public static void removeTriggerForKey(String key) { - ArrayList triggerKeys = new ArrayList<>(); - triggerKeys.add(key); - - getInAppMessageController().removeTriggersForKeys(triggerKeys); - } - - /** Returns a single trigger value for the given key (if it exists, otherwise returns null) */ - @Nullable - public static Object getTriggerValueForKey(String key) { - if (appContext == null) { - logger.error("Before calling getTriggerValueForKey, Make sure OneSignal initWithContext and setAppId is called first"); - return null; - } - - return getInAppMessageController().getTriggerValue(key); - } - - /** Returns all trigger key-value for the current user */ - public static Map getTriggers() { - if (appContext == null) { - logger.error("Before calling getTriggers, Make sure OneSignal initWithContext and setAppId is called first"); - return new HashMap<>(); - } - - return getInAppMessageController().getTriggers(); - } - - /*** - * Can temporarily pause in-app messaging on this device. - * Useful if you don't want to interrupt a user while playing a match in a game. - * - * @param pause The boolean that pauses/resumes in-app messages - */ - public static void pauseInAppMessages(final boolean pause) { - if (appContext == null) { - logger.error("Waiting initWithContext. " + - "Moving " + OSTaskRemoteController.PAUSE_IN_APP_MESSAGES + " operation to a pending task queue."); - taskRemoteController.addTaskToQueue(new Runnable() { - @Override - public void run() { - logger.debug("Running " + OSTaskRemoteController.PAUSE_IN_APP_MESSAGES + " operation from pending queue."); - pauseInAppMessages(pause); - } - }); - return; - } - - getInAppMessageController().setInAppMessagingEnabled(!pause); - } - - public static boolean isInAppMessagingPaused() { - if (appContext == null) { - logger.error("Before calling isInAppMessagingPaused, Make sure OneSignal initWithContext and setAppId is called first"); - return false; - } - - return !getInAppMessageController().inAppMessagingEnabled(); - } - - /** - * Method that checks if notification is valid or duplicated. - * - * Method called from Notification processing. This might be called from a flow that doesn't call initWithContext yet. - * Check if notificationDataController is not init before processing notValidOrDuplicated - * - * @param context the context from where the notification was received - * @param jsonPayload the notification payload in a JSON format - * @param callback the callback to return the result since this method does DB access in another thread - * */ - static void notValidOrDuplicated(Context context, JSONObject jsonPayload, @NonNull OSNotificationDataController.InvalidOrDuplicateNotificationCallback callback) { - if (notificationDataController == null) { - OneSignalDbHelper helper = OneSignal.getDBHelperInstance(context); - notificationDataController = new OSNotificationDataController(helper, logger); - } - notificationDataController.notValidOrDuplicated(jsonPayload, callback); - } - - static String getNotificationIdFromFCMJson(@Nullable JSONObject fcmJson) { - if (fcmJson == null) - return null; - try { - JSONObject customJSON = new JSONObject(fcmJson.getString("custom")); - - if (customJSON.has("i")) - return customJSON.optString("i", null); - else - logger.debug("Not a OneSignal formatted FCM message. No 'i' field in custom."); - } catch (JSONException e) { - logger.debug("Not a OneSignal formatted FCM message. No 'custom' field in the JSONObject."); - } - - return null; - } - - static boolean isAppActive() { - return initDone && isInForeground(); - } - - private static boolean shouldStartNewSession() { - if (!isInForeground()) - return false; - - if (!isPastOnSessionTime()) - return false; - - return true; - } - - private static boolean isPastOnSessionTime() { - long currentTimeMillis = OneSignal.getTime().getCurrentTimeMillis(); - long lastSessionTime = getLastSessionTime(); - long difference = currentTimeMillis - lastSessionTime; - logger.debug("isPastOnSessionTime currentTimeMillis: " + currentTimeMillis + " lastSessionTime: " + lastSessionTime + " difference: " + difference); - return difference >= MIN_ON_SESSION_TIME_MILLIS; - } - - // Extra check to make sure we don't unsubscribe devices that rely on silent background notifications. - static boolean areNotificationsEnabledForSubscribedState() { - if (remoteParamController.unsubscribeWhenNotificationsAreDisabled()) - return OSUtils.areNotificationsEnabled(appContext); - return true; - } - - static void handleSuccessfulEmailLogout() { - if (emailLogoutHandler != null) { - emailLogoutHandler.onSuccess(); - emailLogoutHandler = null; - } - } - - static void handleFailedEmailLogout() { - if (emailLogoutHandler != null) { - emailLogoutHandler.onFailure(new EmailUpdateError(EmailErrorType.NETWORK, "Failed due to network failure. Will retry on next sync.")); - emailLogoutHandler = null; - } - } - - static void fireEmailUpdateSuccess() { - if (emailUpdateHandler != null) { - emailUpdateHandler.onSuccess(); - emailUpdateHandler = null; - } - } - - static void fireEmailUpdateFailure() { - if (emailUpdateHandler != null) { - emailUpdateHandler.onFailure(new EmailUpdateError(EmailErrorType.NETWORK, "Failed due to network failure. Will retry on next sync.")); - emailUpdateHandler = null; - } - } - - static void handleSuccessfulSMSlLogout(JSONObject result) { - if (smsLogoutHandler != null) { - smsLogoutHandler.onSuccess(result); - smsLogoutHandler = null; - } - } - - static void handleFailedSMSLogout() { - if (smsLogoutHandler != null) { - smsLogoutHandler.onFailure(new OSSMSUpdateError(SMSErrorType.NETWORK, "Failed due to network failure. Will retry on next sync.")); - smsLogoutHandler = null; - } - } - - static void fireSMSUpdateSuccess(JSONObject result) { - if (smsUpdateHandler != null) { - smsUpdateHandler.onSuccess(result); - smsUpdateHandler = null; - } - } - - static void fireSMSUpdateFailure() { - if (smsUpdateHandler != null) { - smsUpdateHandler.onFailure(new OSSMSUpdateError(SMSErrorType.NETWORK, "Failed due to network failure. Will retry on next sync.")); - smsUpdateHandler = null; - } - } - - @NonNull - static OSTime getTime() { - return time; - } - - /* - * Start Mock Injection module - */ - static void setTime(OSTime time) { - OneSignal.time = time; - } - - static void setTrackerFactory(OSTrackerFactory trackerFactory) { - OneSignal.trackerFactory = trackerFactory; - } - - static void setSessionManager(OSSessionManager sessionManager) { - OneSignal.sessionManager = sessionManager; - } - - static void setSharedPreferences(OSSharedPreferences preferences) { - OneSignal.preferences = preferences; - } - - static OSSessionManager.SessionListener getSessionListener() { - return sessionListener; - } - - static OSRemoteParamController getRemoteParamController() { - return remoteParamController; - } - - static OneSignalDbHelper getDBHelperInstance() { - return OneSignalDbHelper.getInstance(appContext); - } - - static OneSignalDbHelper getDBHelperInstance(Context context) { - return OneSignalDbHelper.getInstance(context); - } - - static OSTaskController getTaskRemoteController() { - return taskRemoteController; - } - - static OSTaskController getTaskController() { - return taskController; - } - - static FocusTimeController getFocusTimeController() { - if (focusTimeController == null) { - focusTimeController = new FocusTimeController(new OSFocusTimeProcessorFactory(), logger); - } - - return focusTimeController; - } - /* - * End Mock Injection module - */ - - /* - * Start OneSignalOutcome module - */ - static OSSessionManager getSessionManager() { - return sessionManager; - } - - static void sendClickActionOutcomes(@NonNull List outcomes) { - // This is called from IAM shouldn't need this check - if (outcomeEventsController == null || appId == null) { - OneSignal.Log(LOG_LEVEL.ERROR, "Make sure OneSignal.init is called first"); - return; - } - - outcomeEventsController.sendClickActionOutcomes(outcomes); - } - - public static void sendOutcome(@NonNull String name) { - sendOutcome(name, null); - } - - public static void sendOutcome(@NonNull final String name, final OutcomeCallback callback) { - if (!isValidOutcomeEntry(name)) { - logger.error("Make sure OneSignal initWithContext and setAppId is called first"); - return; - } - - // Outcomes needs app id, delay until init is not done - if (taskRemoteController.shouldQueueTaskForInit(OSTaskRemoteController.SEND_OUTCOME) || outcomeEventsController == null) { - logger.error("Waiting for remote params. " + - "Moving " + OSTaskRemoteController.SEND_OUTCOME + " operation to a pending queue."); - taskRemoteController.addTaskToQueue(new Runnable() { - @Override - public void run() { - logger.debug("Running " + OSTaskRemoteController.SEND_OUTCOME + " operation from pending queue."); - sendOutcome(name, callback); - } - }); - return; - } - - if (shouldLogUserPrivacyConsentErrorMessageForMethodName("sendOutcome()")) { - return; - } - - outcomeEventsController.sendOutcomeEvent(name, callback); - } - - public static void sendUniqueOutcome(@NonNull String name) { - sendUniqueOutcome(name, null); - } - - public static void sendUniqueOutcome(@NonNull final String name, final OutcomeCallback callback) { - if (!isValidOutcomeEntry(name)) - return; - - // Outcomes needs app id, delay until init is not done - if (taskRemoteController.shouldQueueTaskForInit(OSTaskRemoteController.SEND_UNIQUE_OUTCOME) || outcomeEventsController == null) { - logger.error("Waiting for remote params. " + - "Moving " + OSTaskRemoteController.SEND_UNIQUE_OUTCOME + " operation to a pending queue."); - taskRemoteController.addTaskToQueue(new Runnable() { - @Override - public void run() { - logger.debug("Running " + OSTaskRemoteController.SEND_UNIQUE_OUTCOME + " operation from pending queue."); - sendUniqueOutcome(name, callback); - } - }); - return; - } - - if (shouldLogUserPrivacyConsentErrorMessageForMethodName("sendUniqueOutcome()")) { - return; - } - - outcomeEventsController.sendUniqueOutcomeEvent(name, callback); - } - - public static void sendOutcomeWithValue(@NonNull String name, float value) { - sendOutcomeWithValue(name, value, null); - } - - public static void sendOutcomeWithValue(@NonNull final String name, final float value, final OutcomeCallback callback) { - if (!isValidOutcomeEntry(name) || !isValidOutcomeValue(value)) - return; - - // Outcomes needs app id, delay until init is not done - if (taskRemoteController.shouldQueueTaskForInit(OSTaskRemoteController.SEND_OUTCOME_WITH_VALUE) || outcomeEventsController == null) { - logger.error("Waiting for remote params. " + - "Moving " + OSTaskRemoteController.SEND_OUTCOME_WITH_VALUE + " operation to a pending queue."); - taskRemoteController.addTaskToQueue(new Runnable() { - @Override - public void run() { - logger.debug("Running " + OSTaskRemoteController.SEND_OUTCOME_WITH_VALUE + " operation from pending queue."); - sendOutcomeWithValue(name, value, callback); - } - }); - return; - } - - outcomeEventsController.sendOutcomeEventWithValue(name, value, callback); - } - - private static boolean isValidOutcomeEntry(String name) { - if (name == null || name.isEmpty()) { - OneSignal.Log(LOG_LEVEL.ERROR, "Outcome name must not be empty"); - return false; - } - - return true; - } - - private static boolean isValidOutcomeValue(float value) { - if (value <= 0) { - OneSignal.Log(LOG_LEVEL.ERROR, "Outcome value must be greater than 0"); - return false; - } - - return true; - } - - /** - * OutcomeEvent will be null in cases where the request was not sent: - * 1. OutcomeEventParams cached already for re-attempt in future - * 2. Unique OutcomeEventParams already sent for ATTRIBUTED session and notification(s) - * 3. Unique OutcomeEventParams already sent for UNATTRIBUTED session during session - * 4. Outcomes disabled - */ - public interface OutcomeCallback { - void onSuccess(@Nullable OSOutcomeEvent outcomeEvent); - } - /* - * End OneSignalOutcome module - */ - - interface OSPromptActionCompletionCallback { - void onCompleted(PromptActionResult result); - } - - enum PromptActionResult { - PERMISSION_GRANTED, - PERMISSION_DENIED, - LOCATION_PERMISSIONS_MISSING_MANIFEST, - ERROR; - } -} diff --git a/OneSignalSDK/onesignal/src/main/java/com/onesignal/OneSignalStateSynchronizer.java b/OneSignalSDK/onesignal/src/main/java/com/onesignal/OneSignalStateSynchronizer.java deleted file mode 100644 index 3c6f0e08df..0000000000 --- a/OneSignalSDK/onesignal/src/main/java/com/onesignal/OneSignalStateSynchronizer.java +++ /dev/null @@ -1,341 +0,0 @@ -/** - * Modified MIT License - * - * Copyright 2018 OneSignal - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * 1. The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * 2. All copies of substantial portions of the Software may only be used in connection - * with services provided by OneSignal. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ - -package com.onesignal; - -import androidx.annotation.Nullable; - -import com.onesignal.OneSignal.ChangeTagsUpdateHandler; - -import org.json.JSONException; -import org.json.JSONObject; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; - -class OneSignalStateSynchronizer { - - private static final Object LOCK = new Object(); - - enum UserStateSynchronizerType { - PUSH, - EMAIL, - SMS; - - public boolean isPush() { - return this.equals(PUSH); - } - - public boolean isEmail() { - return this.equals(EMAIL); - } - - public boolean isSMS() { - return this.equals(SMS); - } - } - - static class OSDeviceInfoError { - public int errorCode; - public String message; - - OSDeviceInfoError(int errorCode, String message) { - this.errorCode = errorCode; - this.message = message; - } - - public int getCode() { return errorCode; } - - public String getMessage() { return message; } - } - - interface OSDeviceInfoCompletionHandler { - void onSuccess(String results); - void onFailure(OSDeviceInfoError error); - } - - // Each class abstracts from UserStateSynchronizer and this will allow us to handle different channels for specific method calls and requests - // Each time we create a new UserStateSynchronizer we should add it to the userStateSynchronizers HashMap - // Currently we have 2 channels: - // 1. Push - // 2. Email - // 3. SMS - // Add more channels... - private static HashMap userStateSynchronizers = new HashMap<>(); - - // #1 UserStateSynchronizer -> Push Channel - static UserStatePushSynchronizer getPushStateSynchronizer() { - if (!userStateSynchronizers.containsKey(UserStateSynchronizerType.PUSH) || userStateSynchronizers.get(UserStateSynchronizerType.PUSH) == null) { - synchronized (LOCK) { - if (userStateSynchronizers.get(UserStateSynchronizerType.PUSH) == null) - userStateSynchronizers.put(UserStateSynchronizerType.PUSH, new UserStatePushSynchronizer()); - } - } - - return (UserStatePushSynchronizer) userStateSynchronizers.get(UserStateSynchronizerType.PUSH); - } - - // #2 UserStateSynchronizer -> Email Channel - static UserStateEmailSynchronizer getEmailStateSynchronizer() { - if (!userStateSynchronizers.containsKey(UserStateSynchronizerType.EMAIL) || userStateSynchronizers.get(UserStateSynchronizerType.EMAIL) == null) { - synchronized (LOCK) { - if (userStateSynchronizers.get(UserStateSynchronizerType.EMAIL) == null) - userStateSynchronizers.put(UserStateSynchronizerType.EMAIL, new UserStateEmailSynchronizer()); - } - } - - return (UserStateEmailSynchronizer) userStateSynchronizers.get(UserStateSynchronizerType.EMAIL); - } - - // #3 UserStateSynchronizer -> SMS Channel - static UserStateSMSSynchronizer getSMSStateSynchronizer() { - if (!userStateSynchronizers.containsKey(UserStateSynchronizerType.SMS) || userStateSynchronizers.get(UserStateSynchronizerType.SMS) == null) { - synchronized (LOCK) { - if (userStateSynchronizers.get(UserStateSynchronizerType.SMS) == null) - userStateSynchronizers.put(UserStateSynchronizerType.SMS, new UserStateSMSSynchronizer()); - } - } - - return (UserStateSMSSynchronizer) userStateSynchronizers.get(UserStateSynchronizerType.SMS); - } - - static List getUserStateSynchronizers() { - List userStateSynchronizers = new ArrayList<>(); - - userStateSynchronizers.add(getPushStateSynchronizer()); - - // Make sure we are only setting external user id for email when an email is actually set - if (OneSignal.hasEmailId()) - userStateSynchronizers.add(getEmailStateSynchronizer()); - - // Make sure we are only setting external user id for sms when an sms is actually set - if (OneSignal.hasSMSlId()) - userStateSynchronizers.add(getSMSStateSynchronizer()); - - return userStateSynchronizers; - } - - static boolean persist() { - boolean pushPersisted = getPushStateSynchronizer().persist(); - boolean emailPersisted = getEmailStateSynchronizer().persist(); - boolean smsPersisted = getSMSStateSynchronizer().persist(); - - if (emailPersisted) - emailPersisted = getEmailStateSynchronizer().getRegistrationId() != null; - - if (smsPersisted) - smsPersisted = getSMSStateSynchronizer().getRegistrationId() != null; - - return pushPersisted || emailPersisted || smsPersisted; - } - - static void clearLocation() { - getPushStateSynchronizer().clearLocation(); - getEmailStateSynchronizer().clearLocation(); - getSMSStateSynchronizer().clearLocation(); - } - - static void initUserState() { - getPushStateSynchronizer().initUserState(); - getEmailStateSynchronizer().initUserState(); - getSMSStateSynchronizer().initUserState(); - } - - static void syncUserState(boolean fromSyncService) { - getPushStateSynchronizer().syncUserState(fromSyncService); - getEmailStateSynchronizer().syncUserState(fromSyncService); - getSMSStateSynchronizer().syncUserState(fromSyncService); - } - - static void sendTags(JSONObject newTags, @Nullable ChangeTagsUpdateHandler handler) { - try { - JSONObject jsonField = new JSONObject().put("tags", newTags); - getPushStateSynchronizer().sendTags(jsonField, handler); - getEmailStateSynchronizer().sendTags(jsonField, handler); - getSMSStateSynchronizer().sendTags(jsonField, handler); - } catch (JSONException e) { - if (handler != null) - handler.onFailure(new OneSignal.SendTagsError(-1, "Encountered an error attempting to serialize your tags into JSON: " + e.getMessage() + "\n" + e.getStackTrace())); - e.printStackTrace(); - } - } - - static void setSMSNumber(String smsNumber, String smsAuthHash) { - getPushStateSynchronizer().setSMSNumber(smsNumber, smsAuthHash); - getSMSStateSynchronizer().setChannelId(smsNumber, smsAuthHash); - } - - static void setEmail(String email, String emailAuthHash) { - getPushStateSynchronizer().setEmail(email, emailAuthHash); - getEmailStateSynchronizer().setChannelId(email, emailAuthHash); - } - - static void setSubscription(boolean enable) { - getPushStateSynchronizer().setSubscription(enable); - } - - static boolean getUserSubscribePreference() { - return getPushStateSynchronizer().getUserSubscribePreference(); - } - - static String getLanguage() { - return getPushStateSynchronizer().getLanguage(); - } - - static void setPermission(boolean enable) { - getPushStateSynchronizer().setPermission(enable); - } - - static void updateLocation(LocationController.LocationPoint point) { - getPushStateSynchronizer().updateLocation(point); - getEmailStateSynchronizer().updateLocation(point); - getSMSStateSynchronizer().updateLocation(point); - } - - static boolean getSubscribed() { - return getPushStateSynchronizer().getSubscribed(); - } - - static String getRegistrationId() { - return getPushStateSynchronizer().getRegistrationId(); - } - - static UserStateSynchronizer.GetTagsResult getTags(boolean fromServer) { - return getPushStateSynchronizer().getTags(fromServer); - } - - static void resetCurrentState() { - getPushStateSynchronizer().resetCurrentState(); - getEmailStateSynchronizer().resetCurrentState(); - getSMSStateSynchronizer().resetCurrentState(); - - getPushStateSynchronizer().saveChannelId(null); - getEmailStateSynchronizer().saveChannelId(null); - getSMSStateSynchronizer().saveChannelId(null); - - OneSignal.setLastSessionTime(-60 * 61); - } - - static void updateDeviceInfo(JSONObject deviceInfo, OSDeviceInfoCompletionHandler handler) { - getPushStateSynchronizer().updateDeviceInfo(deviceInfo, handler); - getEmailStateSynchronizer().updateDeviceInfo(deviceInfo, handler); - getSMSStateSynchronizer().updateDeviceInfo(deviceInfo, handler); - } - - static void updatePushState(JSONObject pushState) { - getPushStateSynchronizer().updateState(pushState); - } - - static void refreshSecondaryChannelState() { - getEmailStateSynchronizer().refresh(); - getSMSStateSynchronizer().refresh(); - } - - static void setNewSession() { - getPushStateSynchronizer().setNewSession(); - getEmailStateSynchronizer().setNewSession(); - getSMSStateSynchronizer().setNewSession(); - } - - static boolean getSyncAsNewSession() { - return getPushStateSynchronizer().getSyncAsNewSession() || - getEmailStateSynchronizer().getSyncAsNewSession() || - getSMSStateSynchronizer().getSyncAsNewSession(); - } - - static void setNewSessionForEmail() { - getEmailStateSynchronizer().setNewSession(); - } - - static void logoutEmail() { - getPushStateSynchronizer().logoutEmail(); - getEmailStateSynchronizer().logoutChannel(); - } - - static void logoutSMS() { - getSMSStateSynchronizer().logoutChannel(); - getPushStateSynchronizer().logoutSMS(); - } - - static void setExternalUserId(String externalId, String externalIdAuthHash, final OneSignal.OSExternalUserIdUpdateCompletionHandler completionHandler) throws JSONException { - final JSONObject responses = new JSONObject(); - // Create a handler for internal usage, this is where the developers completion handler will be called, - // which happens once all handlers for each channel have been processed - OneSignal.OSInternalExternalUserIdUpdateCompletionHandler handler = new OneSignal.OSInternalExternalUserIdUpdateCompletionHandler() { - @Override - public void onComplete(String channel, boolean success) { - OneSignal.onesignalLog(OneSignal.LOG_LEVEL.VERBOSE, "Completed request to update external user id for channel: " + channel + " and success: " + success); - try { - // Latest channel success status will be overwriting previous ones since we only care about latest request status - // That way the completion handler is allowing the developer to handle the most recent scenario - responses.put(channel, new JSONObject().put("success", success)); - } catch (JSONException e) { - OneSignal.onesignalLog(OneSignal.LOG_LEVEL.ERROR, "Error while adding the success status of external id for channel: " + channel); - e.printStackTrace(); - } - - for (UserStateSynchronizer userStateSynchronizer : userStateSynchronizers.values()) { - if (userStateSynchronizer.hasQueuedHandlers()) { - OneSignal.onesignalLog(OneSignal.LOG_LEVEL.VERBOSE, "External user id handlers are still being processed for channel: " + userStateSynchronizer.getChannelString() + " , wait until finished before proceeding"); - return; - } - } - - CallbackThreadManager.Companion.runOnPreferred(new Runnable() { - @Override - public void run() { - if (completionHandler != null) - completionHandler.onSuccess(responses); - } - }); - } - }; - - List userStateSynchronizers = getUserStateSynchronizers(); - for (UserStateSynchronizer userStateSynchronizer : userStateSynchronizers) { - userStateSynchronizer.setExternalUserId(externalId, externalIdAuthHash, handler); - } - } - - static void sendPurchases(JSONObject jsonBody, OneSignalRestClient.ResponseHandler responseHandler) { - List userStateSynchronizers = getUserStateSynchronizers(); - for (UserStateSynchronizer userStateSynchronizer : userStateSynchronizers) { - userStateSynchronizer.sendPurchases(jsonBody, responseHandler); - } - } - - // This is to indicate that StateSynchronizer can start making REST API calls - // We do this to roll up as many field updates in a single create / on_session call to - // optimize the number of api calls that are made - static void readyToUpdate(boolean canMakeUpdates) { - getPushStateSynchronizer().readyToUpdate(canMakeUpdates); - getEmailStateSynchronizer().readyToUpdate(canMakeUpdates); - getSMSStateSynchronizer().readyToUpdate(canMakeUpdates); - } - -} \ No newline at end of file diff --git a/OneSignalSDK/onesignal/src/main/java/com/onesignal/PackageInfoHelper.kt b/OneSignalSDK/onesignal/src/main/java/com/onesignal/PackageInfoHelper.kt deleted file mode 100644 index 1645d28559..0000000000 --- a/OneSignalSDK/onesignal/src/main/java/com/onesignal/PackageInfoHelper.kt +++ /dev/null @@ -1,50 +0,0 @@ -package com.onesignal - -import android.annotation.TargetApi -import android.content.Context -import android.content.pm.PackageInfo -import android.content.pm.PackageManager -import android.os.DeadSystemException -import android.util.AndroidException - -data class GetPackageInfoResult( - // Check this value first, if false ignore other properties - // - If false this means Android throw an error, so it's not possible - // to know if the app we are checking is even installed. - val successful: Boolean, - - // Raw PackageInfo from Android API - // NOTE: Ignore this value if successful == false - // Will be null if package is not installed. - val packageInfo: PackageInfo?, -) - -class PackageInfoHelper { - companion object { - @TargetApi(24) - fun getInfo(appContext: Context, packageName: String, flags: Int): GetPackageInfoResult { - val packageManager = appContext.packageManager - return try { - GetPackageInfoResult( - true, - packageManager.getPackageInfo( - packageName, - flags, - ), - ) - } catch (e: PackageManager.NameNotFoundException) { - // Expected if package is not installed on the device. - GetPackageInfoResult(true, null) - } catch (e: AndroidException) { - // Suppressing DeadSystemException as the app is already dying for - // another reason and allowing this exception to bubble up would - // create a red herring for app developers. We still re-throw - // others, as we don't want to silently hide other issues. - if (e !is DeadSystemException) { - throw e - } - GetPackageInfoResult(false, null) - } - } - } -} diff --git a/OneSignalSDK/unittest/build.gradle b/OneSignalSDK/unittest/build.gradle deleted file mode 100644 index 906f1f32da..0000000000 --- a/OneSignalSDK/unittest/build.gradle +++ /dev/null @@ -1,81 +0,0 @@ -ext { - buildVersions = [ - minSdkVersion: 19, - versionCode: 1, - versionName: '1.0' - ] - androidTestCoreVersion = '1.3.0' - androidWorkTestVersion = '2.7.1' - awaitilityVersion = '3.1.5' - firebaseAnalytics = '19.0.1' - googlePlayServicesVersion = '18.0.0' - jUnitVersion = '4.13.2' - reflectionsVersion = '0.9.12' - roboelectricVersion = '4.8.1' -} - -apply plugin: 'com.android.application' -apply plugin: 'kotlin-android' - -android { - compileSdkVersion rootProject.buildVersions.compileSdkVersion - defaultConfig { - applicationId 'com.onesignal.example' - targetSdkVersion rootProject.buildVersions.targetSdkVersion - minSdkVersion buildVersions.minSdkVersion - versionCode buildVersions.versionCode - versionName buildVersions.versionName - testOptions.unitTests.includeAndroidResources = true - } - buildTypes { - debug { - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' - } - } - testOptions { - unitTests.all { - maxParallelForks 1 - maxHeapSize '2048m' - } - unitTests { - includeAndroidResources = true - } - } - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - } - namespace 'com.onesignal.example' -} - -tasks.withType(Test) { - testLogging { - exceptionFormat "full" - events "started", "skipped", "passed", "failed" - showStandardStreams false // Enable to have logging print - } -} - -dependencies { - compileOnly fileTree(dir: 'libs', include: ['*.jar']) - - // Use local OneSignal SDK - implementation(project(':onesignal')) - - implementation "com.google.android.gms:play-services-location:$googlePlayServicesVersion" - implementation "com.google.firebase:firebase-analytics:$firebaseAnalytics" - - implementation "com.huawei.hms:push:$huaweiHMSPushVersion" - implementation "com.huawei.hms:location:$huaweiHMSLocationVersion" - - testImplementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion" - testImplementation "junit:junit:$jUnitVersion" - - testImplementation "androidx.test:core:$androidTestCoreVersion" - testImplementation "androidx.work:work-testing:$androidWorkTestVersion" - - testImplementation "org.robolectric:robolectric:$roboelectricVersion" - testImplementation "org.awaitility:awaitility:$awaitilityVersion" - testImplementation "org.reflections:reflections:$reflectionsVersion" -} \ No newline at end of file diff --git a/OneSignalSDK/unittest/src/test/java/com/test/onesignal/GenerateNotificationRunner.java b/OneSignalSDK/unittest/src/test/java/com/test/onesignal/GenerateNotificationRunner.java deleted file mode 100644 index 66bcc82014..0000000000 --- a/OneSignalSDK/unittest/src/test/java/com/test/onesignal/GenerateNotificationRunner.java +++ /dev/null @@ -1,2626 +0,0 @@ -/** - * Modified MIT License - * - * Copyright 2018 OneSignal - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * 1. The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * 2. All copies of substantial portions of the Software may only be used in connection - * with services provided by OneSignal. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ - -package com.test.onesignal; - -import static com.onesignal.OneSignalPackagePrivateHelper.FCMBroadcastReceiver_onReceived_withIntent; -import static com.onesignal.OneSignalPackagePrivateHelper.FCMBroadcastReceiver_processBundle; -import static com.onesignal.OneSignalPackagePrivateHelper.GenerateNotification.BUNDLE_KEY_ACTION_ID; -import static com.onesignal.OneSignalPackagePrivateHelper.GenerateNotification.BUNDLE_KEY_ANDROID_NOTIFICATION_ID; -import static com.onesignal.OneSignalPackagePrivateHelper.GenerateNotification.BUNDLE_KEY_ONESIGNAL_DATA; -import static com.onesignal.OneSignalPackagePrivateHelper.NotificationBundleProcessor.PUSH_MINIFIED_BUTTONS_LIST; -import static com.onesignal.OneSignalPackagePrivateHelper.NotificationBundleProcessor.PUSH_MINIFIED_BUTTON_ID; -import static com.onesignal.OneSignalPackagePrivateHelper.NotificationBundleProcessor.PUSH_MINIFIED_BUTTON_TEXT; -import static com.onesignal.OneSignalPackagePrivateHelper.NotificationBundleProcessor_ProcessFromFCMIntentService; -import static com.onesignal.OneSignalPackagePrivateHelper.NotificationBundleProcessor_ProcessFromFCMIntentService_NoWrap; -import static com.onesignal.OneSignalPackagePrivateHelper.NotificationOpenedProcessor_processFromContext; -import static com.onesignal.OneSignalPackagePrivateHelper.NotificationSummaryManager_updateSummaryNotificationAfterChildRemoved; -import static com.onesignal.OneSignalPackagePrivateHelper.OneSignal_getAccentColor; -import static com.onesignal.OneSignalPackagePrivateHelper.OneSignal_setTime; -import static com.onesignal.OneSignalPackagePrivateHelper.OneSignal_setupNotificationServiceExtension; -import static com.onesignal.OneSignalPackagePrivateHelper.createInternalPayloadBundle; -import static com.onesignal.ShadowOneSignalRestClient.setRemoteParamsGetHtmlResponse; -import static com.onesignal.ShadowRoboNotificationManager.getNotificationsInGroup; -import static com.test.onesignal.RestClientAsserts.assertReportReceivedAtIndex; -import static com.test.onesignal.RestClientAsserts.assertRestCalls; -import static com.test.onesignal.TestHelpers.fastColdRestartApp; -import static com.test.onesignal.TestHelpers.pauseActivity; -import static com.test.onesignal.TestHelpers.startRemoteNotificationReceivedHandlerService; -import static com.test.onesignal.TestHelpers.threadAndTaskWait; -import static junit.framework.Assert.assertEquals; -import static junit.framework.Assert.assertFalse; -import static junit.framework.Assert.assertNotNull; -import static junit.framework.Assert.assertNull; -import static junit.framework.Assert.assertTrue; -import static org.hamcrest.CoreMatchers.not; -import static org.junit.Assert.assertNotEquals; -import static org.junit.Assert.assertThat; -import static org.robolectric.Shadows.shadowOf; - -import android.app.Activity; -import android.app.Notification; -import android.app.NotificationManager; -import android.app.PendingIntent; -import android.app.job.JobScheduler; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.database.Cursor; -import android.database.sqlite.SQLiteDatabase; -import android.net.Uri; -import android.os.Build; -import android.os.Bundle; -import android.util.Log; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.core.app.NotificationCompat; -import androidx.test.core.app.ApplicationProvider; - -import com.onesignal.BundleCompat; -import com.onesignal.FCMBroadcastReceiver; -import com.onesignal.MockOSTimeImpl; -import com.onesignal.MockOneSignalDBHelper; -import com.onesignal.OSMutableNotification; -import com.onesignal.OSNotification; -import com.onesignal.OSNotificationOpenedResult; -import com.onesignal.OSNotificationReceivedEvent; -import com.onesignal.OneSignal; -import com.onesignal.OneSignalNotificationManagerPackageHelper; -import com.onesignal.OneSignalPackagePrivateHelper; -import com.onesignal.OneSignalPackagePrivateHelper.NotificationTable; -import com.onesignal.OneSignalPackagePrivateHelper.OSNotificationRestoreWorkManager; -import com.onesignal.OneSignalPackagePrivateHelper.TestOneSignalPrefs; -import com.onesignal.OneSignalShadowPackageManager; -import com.onesignal.ShadowBadgeCountUpdater; -import com.onesignal.ShadowCustomTabsClient; -import com.onesignal.ShadowCustomTabsSession; -import com.onesignal.ShadowFCMBroadcastReceiver; -import com.onesignal.ShadowFocusHandler; -import com.onesignal.ShadowGenerateNotification; -import com.onesignal.ShadowNotificationManagerCompat; -import com.onesignal.ShadowNotificationReceivedEvent; -import com.onesignal.ShadowOSUtils; -import com.onesignal.ShadowOSViewUtils; -import com.onesignal.ShadowOSWebView; -import com.onesignal.ShadowOneSignal; -import com.onesignal.ShadowOneSignalRestClient; -import com.onesignal.ShadowReceiveReceiptController; -import com.onesignal.ShadowResources; -import com.onesignal.ShadowRoboNotificationManager; -import com.onesignal.ShadowRoboNotificationManager.PostedNotification; -import com.onesignal.ShadowTimeoutHandler; -import com.onesignal.StaticResetHelper; -import com.onesignal.example.BlankActivity; - -import org.awaitility.Awaitility; -import org.awaitility.Duration; -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; -import org.junit.After; -import org.junit.AfterClass; -import org.junit.Before; -import org.junit.BeforeClass; -import org.junit.Ignore; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.robolectric.Robolectric; -import org.robolectric.RobolectricTestRunner; -import org.robolectric.Shadows; -import org.robolectric.android.controller.ActivityController; -import org.robolectric.annotation.Config; -import org.robolectric.annotation.LooperMode; -import org.robolectric.shadows.ShadowLog; - -import java.lang.reflect.Field; -import java.math.BigInteger; -import java.util.HashMap; -import java.util.Iterator; -import java.util.Map; -import java.util.concurrent.TimeUnit; - -@Config(packageName = "com.onesignal.example", - shadows = { - ShadowRoboNotificationManager.class, - ShadowOneSignalRestClient.class, - ShadowBadgeCountUpdater.class, - ShadowNotificationManagerCompat.class, - ShadowOSUtils.class, - ShadowOSViewUtils.class, - ShadowCustomTabsClient.class, - ShadowFocusHandler.class, - ShadowCustomTabsSession.class, - OneSignalShadowPackageManager.class - }, - sdk = 21 -) -@RunWith(RobolectricTestRunner.class) -@LooperMode(LooperMode.Mode.LEGACY) -public class GenerateNotificationRunner { - - private static int callbackCounter = 0; - - private static final String ONESIGNAL_APP_ID = "b4f7f966-d8cc-11e4-bed1-df8f05be55ba"; - private static final String notifMessage = "Robo test message"; - - private Activity blankActivity; - private static ActivityController blankActivityController; - - private OSNotificationReceivedEvent lastForegroundNotificationReceivedEvent; - private static OSNotificationReceivedEvent lastServiceNotificationReceivedEvent; - private MockOneSignalDBHelper dbHelper; - private MockOSTimeImpl time; - private String notificationReceivedBody; - private int androidNotificationId; - - private static String lastNotificationOpenedBody; - private static OneSignal.OSNotificationOpenedHandler getNotificationOpenedHandler() { - return openedResult -> { - lastNotificationOpenedBody = openedResult.getNotification().getBody(); - }; - } - - @BeforeClass // Runs only once, before any tests - public static void setUpClass() throws Exception { - ShadowLog.stream = System.out; - TestHelpers.beforeTestSuite(); - StaticResetHelper.saveStaticValues(); - } - - @Before // Before each test - public void beforeEachTest() throws Exception { - blankActivityController = Robolectric.buildActivity(BlankActivity.class).create(); - blankActivity = blankActivityController.get(); - blankActivity.getApplicationInfo().name = "UnitTestApp"; - dbHelper = new MockOneSignalDBHelper(ApplicationProvider.getApplicationContext()); - time = new MockOSTimeImpl(); - - callbackCounter = 0; - lastForegroundNotificationReceivedEvent = null; - lastServiceNotificationReceivedEvent = null; - - TestHelpers.beforeTestInitAndCleanup(); - - setClearGroupSummaryClick(true); - - NotificationManager notificationManager = OneSignalNotificationManagerPackageHelper.getNotificationManager(blankActivity); - notificationManager.cancelAll(); - OSNotificationRestoreWorkManager.restored = false; - - OneSignal_setTime(time); - - // Set remote_params GET response - setRemoteParamsGetHtmlResponse(); - - OneSignal.setLogLevel(OneSignal.LOG_LEVEL.VERBOSE, OneSignal.LOG_LEVEL.NONE); - } - - @AfterClass - public static void afterEverything() throws Exception { - TestHelpers.beforeTestInitAndCleanup(); - } - - @After - public void afterEachTest() throws Exception { - TestHelpers.afterTestCleanup(); - } - - public static Bundle getBaseNotifBundle() { - return getBaseNotifBundle("UUID"); - } - - public static Bundle getBaseNotifBundle(String id) { - Bundle bundle = new Bundle(); - bundle.putString("alert", notifMessage); - bundle.putString("custom", "{\"i\": \"" + id + "\"}"); - - return bundle; - } - - private static Intent createOpenIntent(int notifId, Bundle bundle) { - return new Intent() - .putExtra(BUNDLE_KEY_ANDROID_NOTIFICATION_ID, notifId) - .putExtra(BUNDLE_KEY_ONESIGNAL_DATA, OneSignalPackagePrivateHelper.bundleAsJSONObject(bundle).toString()); - } - - private Intent createOpenIntent(Bundle bundle) { - return createOpenIntent(ShadowRoboNotificationManager.lastNotifId, bundle); - } - - @Test - @Config (sdk = 22, shadows = { ShadowGenerateNotification.class }) - public void shouldSetTitleCorrectly() throws Exception { - // Should use app's Title by default - Bundle bundle = getBaseNotifBundle(); - NotificationBundleProcessor_ProcessFromFCMIntentService(blankActivity, bundle); - threadAndTaskWait(); - - assertEquals("UnitTestApp", ShadowRoboNotificationManager.getLastShadowNotif().getContentTitle()); - - // Should allow title from FCM payload. - bundle = getBaseNotifBundle("UUID2"); - bundle.putString("title", "title123"); - NotificationBundleProcessor_ProcessFromFCMIntentService(blankActivity, bundle); - threadAndTaskWait(); - - assertEquals("title123", ShadowRoboNotificationManager.getLastShadowNotif().getContentTitle()); - } - - @Test - @Config (sdk = 22, shadows = { ShadowGenerateNotification.class }) - public void shouldProcessRestore() throws Exception { - BundleCompat bundle = createInternalPayloadBundle(getBaseNotifBundle()); - bundle.putInt("android_notif_id", 0); - bundle.putBoolean("is_restoring", true); - - NotificationBundleProcessor_ProcessFromFCMIntentService_NoWrap(blankActivity, bundle); - threadAndTaskWait(); - - assertEquals("UnitTestApp", ShadowRoboNotificationManager.getLastShadowNotif().getContentTitle()); - } - - @Test - @Config(sdk = Build.VERSION_CODES.O) - public void shouldNotRestoreActiveNotifs() throws Exception { - // Display a notification - Bundle bundle = getBaseNotifBundle("UUID1"); - bundle.putString("grp", "test1"); - NotificationBundleProcessor_ProcessFromFCMIntentService(blankActivity, bundle); - threadAndTaskWait(); - - // Restore notifications - OSNotificationRestoreWorkManager.beginEnqueueingWork(blankActivity, false); - threadAndTaskWait(); - - // Assert that no restoration jobs were scheduled - JobScheduler scheduler = (JobScheduler) blankActivity.getSystemService(Context.JOB_SCHEDULER_SERVICE); - assertTrue(scheduler.getAllPendingJobs().isEmpty()); - } - - private static OSNotificationOpenedResult lastOpenResult; - - @Test - @Config(shadows = { ShadowGenerateNotification.class }) - public void shouldContainPayloadWhenOldSummaryNotificationIsOpened() throws Exception { - OneSignal.setAppId("b2f7f966-d8cc-11e4-bed1-df8f05be55ba"); - OneSignal.initWithContext(blankActivity); - OneSignal.setNotificationOpenedHandler(new OneSignal.OSNotificationOpenedHandler() { - @Override - public void notificationOpened(OSNotificationOpenedResult result) { - lastOpenResult = result; - } - }); - - // Display 2 notifications that will be grouped together. - Bundle bundle = getBaseNotifBundle("UUID1"); - bundle.putString("grp", "test1"); - NotificationBundleProcessor_ProcessFromFCMIntentService(blankActivity, bundle); - - bundle = getBaseNotifBundle("UUID2"); - bundle.putString("grp", "test1"); - NotificationBundleProcessor_ProcessFromFCMIntentService(blankActivity, bundle); - - // Go forward 4 weeks - time.advanceSystemTimeBy(2_419_202); - - // Display a 3 normal notification. - NotificationBundleProcessor_ProcessFromFCMIntentService(blankActivity, getBaseNotifBundle("UUID3")); - threadAndTaskWait(); - - Map postedNotifs = ShadowRoboNotificationManager.notifications; - - // Open the summary notification - Iterator> postedNotifsIterator = postedNotifs.entrySet().iterator(); - PostedNotification postedNotification = postedNotifsIterator.next().getValue(); - Intent intent = Shadows.shadowOf(postedNotification.notif.contentIntent).getSavedIntent(); - NotificationOpenedProcessor_processFromContext(blankActivity, intent); - threadAndTaskWait(); - - // Make sure we get a payload when it is opened. - assertNotNull(lastOpenResult.getNotification().getBody()); - // Make sure that the summary head notification is the second one, because the third was opened - assertEquals("UUID2", lastOpenResult.getNotification().getNotificationId()); - } - - @Test - @Config(shadows = { ShadowGenerateNotification.class }) - public void shouldSetCorrectNumberOfButtonsOnSummaryNotification() throws Exception { - // Setup - Init - OneSignal.setAppId("b2f7f966-d8cc-11e4-bed1-df8f05be55ba"); - OneSignal.initWithContext(blankActivity); - threadAndTaskWait(); - - // Setup - Display a single notification with a grouped. - Bundle bundle = getBaseNotifBundle("UUID1"); - bundle.putString("grp", "test1"); - bundle.putString("custom", "{\"i\": \"some_UUID\", \"a\": {\"actionButtons\": [{\"text\": \"test\"} ]}}"); - NotificationBundleProcessor_ProcessFromFCMIntentService(blankActivity, bundle); - threadAndTaskWait(); - - Map postedNotifs = ShadowRoboNotificationManager.notifications; - Iterator> postedNotifsIterator = postedNotifs.entrySet().iterator(); - PostedNotification postedSummaryNotification = postedNotifsIterator.next().getValue(); - - assertEquals(Notification.FLAG_GROUP_SUMMARY, postedSummaryNotification.notif.flags & Notification.FLAG_GROUP_SUMMARY); - assertEquals(1, postedSummaryNotification.notif.actions.length); - } - - @Test - @Config(shadows = { ShadowGenerateNotification.class }) - public void shouldCancelAllNotificationsPartOfAGroup() throws Exception { - // Setup - Init - OneSignal.setAppId("b2f7f966-d8cc-11e4-bed1-df8f05be55ba"); - OneSignal.initWithContext(blankActivity); - threadAndTaskWait(); - - // Setup - Display 3 notifications, 2 of which that will be grouped together. - Bundle bundle = getBaseNotifBundle("UUID0"); - NotificationBundleProcessor_ProcessFromFCMIntentService(blankActivity, bundle); - threadAndTaskWait(); - - bundle = getBaseNotifBundle("UUID1"); - bundle.putString("grp", "test1"); - NotificationBundleProcessor_ProcessFromFCMIntentService(blankActivity, bundle); - threadAndTaskWait(); - - bundle = getBaseNotifBundle("UUID2"); - bundle.putString("grp", "test1"); - NotificationBundleProcessor_ProcessFromFCMIntentService(blankActivity, bundle); - threadAndTaskWait(); - - assertEquals(4, ShadowRoboNotificationManager.notifications.size()); - - OneSignal.removeGroupedNotifications("test1"); - threadAndTaskWait(); - - assertEquals(1, ShadowRoboNotificationManager.notifications.size()); - } - - @Test - @Config(sdk = Build.VERSION_CODES.N, shadows = { ShadowGenerateNotification.class }) - public void testFourNotificationsUseProvidedGroup() throws Exception { - OneSignal.setAppId("b2f7f966-d8cc-11e4-bed1-df8f05be55ba"); - OneSignal.initWithContext(blankActivity.getApplicationContext()); - threadAndTaskWait(); - - // Add 4 grouped notifications - postNotificationWithOptionalGroup(4, "test1"); - threadAndTaskWait(); - - assertEquals(4, getNotificationsInGroup("test1").size()); - } - - @Test - @Config(sdk = Build.VERSION_CODES.N, shadows = { ShadowGenerateNotification.class }) - public void testFourGrouplessNotificationsUseDefaultGroup() throws Exception { - OneSignal.setAppId("b2f7f966-d8cc-11e4-bed1-df8f05be55ba"); - OneSignal.initWithContext(blankActivity.getApplicationContext()); - threadAndTaskWait(); - - // Add 4 groupless notifications - postNotificationWithOptionalGroup(4, null); - threadAndTaskWait(); - - assertEquals(4, getNotificationsInGroup("os_group_undefined").size()); - } - - @Test - @Config(sdk = Build.VERSION_CODES.N, shadows = { ShadowGenerateNotification.class }) - public void testGrouplessSummaryNotificationIsDismissedOnClear() throws Exception { - OneSignal.setAppId("b2f7f966-d8cc-11e4-bed1-df8f05be55ba"); - OneSignal.initWithContext(blankActivity.getApplicationContext()); - threadAndTaskWait(); - - // Add 4 groupless notifications - postNotificationWithOptionalGroup(4, null); - threadAndTaskWait(); - - // Obtain the posted notifications - Map postedNotifs = ShadowRoboNotificationManager.notifications; - assertEquals(5, postedNotifs.size()); - // Clear OneSignal Notifications - OneSignal.clearOneSignalNotifications(); - threadAndTaskWait(); - assertEquals(0, postedNotifs.size()); - } - - @Test - @Config(sdk = Build.VERSION_CODES.N, shadows = { ShadowGenerateNotification.class }) - public void testIndividualGrouplessSummaryNotificationDismissal() throws Exception { - OneSignal.setAppId("b2f7f966-d8cc-11e4-bed1-df8f05be55ba"); - OneSignal.initWithContext(blankActivity.getApplicationContext()); - threadAndTaskWait(); - - // Add 4 groupless notifications - postNotificationWithOptionalGroup(4, null); - threadAndTaskWait(); - - // Obtain the posted notifications - Map postedNotifs = ShadowRoboNotificationManager.notifications; - Iterator> iterator = postedNotifs.entrySet().iterator(); - Map.Entry entry = iterator.next(); - Map.Entry entry2 = iterator.next(); - Map.Entry entry3 = iterator.next(); - Map.Entry entry4 = iterator.next(); - Integer id4 = entry4.getKey(); - assertNotNull(id4); - - assertEquals(5, postedNotifs.size()); - // Clear a OneSignal Notification - OneSignal.removeNotification(id4); - threadAndTaskWait(); - assertEquals(4, postedNotifs.size()); - } - - @Test - @Config(sdk = Build.VERSION_CODES.LOLLIPOP, shadows = { ShadowGenerateNotification.class }) - public void testNotifDismissAllOnGroupSummaryClickForAndroidUnderM() throws Exception { - OneSignal.setAppId("b2f7f966-d8cc-11e4-bed1-df8f05be55ba"); - OneSignal.initWithContext(blankActivity); - threadAndTaskWait(); - - SQLiteDatabase readableDb = dbHelper.getSQLiteDatabaseWithRetries(); - - postNotificationsAndSimulateSummaryClick(true, "test1"); - - // Validate SQL DB has removed all grouped notifications - int activeGroupNotifCount = queryNotificationCountFromGroup(readableDb, "test1", true); - assertEquals(0, activeGroupNotifCount); - } - - @Test - @Config(sdk = Build.VERSION_CODES.LOLLIPOP, shadows = { ShadowGenerateNotification.class }) - public void testNotifDismissRecentOnGroupSummaryClickForAndroidUnderM() throws Exception { - OneSignal.setAppId("b2f7f966-d8cc-11e4-bed1-df8f05be55ba"); - OneSignal.initWithContext(blankActivity); - threadAndTaskWait(); - - SQLiteDatabase readableDb = dbHelper.getSQLiteDatabaseWithRetries(); - - postNotificationsAndSimulateSummaryClick(false, "test1"); - - // Validate SQL DB has removed most recent grouped notif - int activeGroupNotifCount = queryNotificationCountFromGroup(readableDb, "test1", true); - assertEquals(3, activeGroupNotifCount); - } - - @Test - @Config(sdk = Build.VERSION_CODES.N, shadows = { ShadowGenerateNotification.class }) - public void testNotifDismissAllOnGroupSummaryClick() throws Exception { - OneSignal.setAppId("b2f7f966-d8cc-11e4-bed1-df8f05be55ba"); - OneSignal.initWithContext(blankActivity); - threadAndTaskWait(); - - SQLiteDatabase readableDb = dbHelper.getSQLiteDatabaseWithRetries(); - - postNotificationsAndSimulateSummaryClick(true, "test1"); - - // Validate SQL DB has removed all grouped notifications - int activeGroupNotifCount = queryNotificationCountFromGroup(readableDb, "test1", true); - assertEquals(0, activeGroupNotifCount); - } - - @Test - @Config(sdk = Build.VERSION_CODES.N, shadows = { ShadowGenerateNotification.class }) - public void testNotifDismissRecentOnGroupSummaryClick() throws Exception { - OneSignal.setAppId("b2f7f966-d8cc-11e4-bed1-df8f05be55ba"); - OneSignal.initWithContext(blankActivity); - threadAndTaskWait(); - - postNotificationsAndSimulateSummaryClick(false, "test1"); - threadAndTaskWait(); - - // Validate SQL DB has removed most recent grouped notif - SQLiteDatabase readableDb = dbHelper.getSQLiteDatabaseWithRetries(); - int activeGroupNotifCount = queryNotificationCountFromGroup(readableDb, "test1", true); - threadAndTaskWait(); - - assertEquals(3, activeGroupNotifCount); - } - - @Test - @Config(sdk = Build.VERSION_CODES.N, shadows = { ShadowGenerateNotification.class }) - public void testNotifDismissAllOnGrouplessSummaryClick() throws Exception { - OneSignal.setAppId("b2f7f966-d8cc-11e4-bed1-df8f05be55ba"); - OneSignal.initWithContext(blankActivity); - threadAndTaskWait(); - - SQLiteDatabase readableDb = dbHelper.getSQLiteDatabaseWithRetries(); - - postNotificationsAndSimulateSummaryClick(true, null); - - // Validate SQL DB has removed all groupless notifications - int activeGroupNotifCount = queryNotificationCountFromGroup(readableDb, null, true); - assertEquals(0, activeGroupNotifCount); - } - - @Test - @Config(sdk = Build.VERSION_CODES.N, shadows = { ShadowGenerateNotification.class }) - public void testNotifDismissRecentOnGrouplessSummaryClick() throws Exception { - OneSignal.setAppId("b2f7f966-d8cc-11e4-bed1-df8f05be55ba"); - OneSignal.initWithContext(blankActivity); - threadAndTaskWait(); - - SQLiteDatabase readableDb = dbHelper.getSQLiteDatabaseWithRetries(); - - postNotificationsAndSimulateSummaryClick(false, null); - threadAndTaskWait(); - - // Validate SQL DB has removed most recent groupless notif - int activeGroupNotifCount = queryNotificationCountFromGroup(readableDb, null, true); - assertEquals(3, activeGroupNotifCount); - } - - private void postNotificationsAndSimulateSummaryClick(boolean shouldDismissAll, String group) throws Exception { - // Add 4 notifications - Bundle bundle = postNotificationWithOptionalGroup(4, group); - setClearGroupSummaryClick(shouldDismissAll); - threadAndTaskWait(); - - // Obtain the summary id - Map postedNotifs = ShadowRoboNotificationManager.notifications; - Iterator> postedNotifsIterator = postedNotifs.entrySet().iterator(); - PostedNotification postedSummaryNotification = postedNotifsIterator.next().getValue(); - - // Simulate summary click - String groupKey = group != null ? - group : - OneSignalNotificationManagerPackageHelper.getGrouplessSummaryKey(); - - Intent intent = createOpenIntent(postedSummaryNotification.id, bundle).putExtra("summary", groupKey); - NotificationOpenedProcessor_processFromContext(blankActivity, intent); - threadAndTaskWait(); - } - - private void setClearGroupSummaryClick(boolean shouldDismissAll) { - TestOneSignalPrefs.saveBool(TestOneSignalPrefs.PREFS_ONESIGNAL, TestOneSignalPrefs.PREFS_OS_CLEAR_GROUP_SUMMARY_CLICK, shouldDismissAll); - } - - @Test - @Config(sdk = Build.VERSION_CODES.N, shadows = { ShadowGenerateNotification.class, ShadowGenerateNotification.class }) - public void testGrouplessSummaryKeyReassignmentAtFourOrMoreNotification() throws Exception { - OneSignal.setAppId("b2f7f966-d8cc-11e4-bed1-df8f05be55ba"); - OneSignal.initWithContext(blankActivity); - threadAndTaskWait(); - - // Add 3 groupless notifications - postNotificationWithOptionalGroup(3, null); - threadAndTaskWait(); - - // Assert before 4, no notif summary is created - int count = OneSignalNotificationManagerPackageHelper.getActiveNotifications(blankActivity).length; - assertEquals(3, count); - - // Add 4 groupless notifications - postNotificationWithOptionalGroup(4, null); - threadAndTaskWait(); - - // Assert after 4, a notif summary is created - count = OneSignalNotificationManagerPackageHelper.getActiveNotifications(blankActivity).length; - assertEquals(5, count); - - SQLiteDatabase readableDb = dbHelper.getSQLiteDatabaseWithRetries(); - // Validate no DB changes occurred and this is only a runtime change to the groupless notifications - // Check for 4 null group id notifications - int nullGroupCount = queryNotificationCountFromGroup(readableDb, null, true); - assertEquals(4, nullGroupCount); - - // Check for 0 os_group_undefined group id notifications - int groupCount = queryNotificationCountFromGroup(readableDb, OneSignalNotificationManagerPackageHelper.getGrouplessSummaryKey(), true); - assertEquals(0, groupCount); - } - - private @Nullable Bundle postNotificationWithOptionalGroup(int notifCount, @Nullable String group) { - Bundle bundle = null; - for (int i = 0; i < notifCount; i++) { - bundle = getBaseNotifBundle("UUID" + i); - bundle.putString("grp", group); - - NotificationBundleProcessor_ProcessFromFCMIntentService(blankActivity, bundle); - } - return bundle; - } - - public int queryNotificationCountFromGroup(SQLiteDatabase db, String group, boolean activeNotifs) { - boolean isGroupless = group == null; - - String whereStr = isGroupless ? - NotificationTable.COLUMN_NAME_GROUP_ID + " IS NULL" : - NotificationTable.COLUMN_NAME_GROUP_ID + " = ?"; - whereStr += " AND " + NotificationTable.COLUMN_NAME_DISMISSED + " = ? AND " + - NotificationTable.COLUMN_NAME_OPENED + " = ? AND " + - NotificationTable.COLUMN_NAME_IS_SUMMARY + " = 0"; - - String active = activeNotifs ? "0" : "1"; - String[] whereArgs = isGroupless ? - new String[]{ active, active } : - new String[]{ group, active, active }; - - Cursor cursor = db.query(NotificationTable.TABLE_NAME, - null, - whereStr, - whereArgs, - null, - null, - null); - cursor.moveToFirst(); - - return cursor.getCount(); - } - - @Test - // We need ShadowTimeoutHandler because RestoreJobService run under an AsyncTask, in that way we can avoid deadlock due to Roboelectric tying to shadow - // Handlers under AsyncTask, and Roboelectric doesn't support handler outside it's custom Main Thread - @Config(shadows = { ShadowGenerateNotification.class, ShadowTimeoutHandler.class }) - public void shouldCancelNotificationAndUpdateSummary() throws Exception { - // Setup - Init - OneSignal.setAppId("b2f7f966-d8cc-11e4-bed1-df8f05be55ba"); - OneSignal.initWithContext(blankActivity); - threadAndTaskWait(); - runImplicitServices(); // Flushes out other services, seems to be a roboelectric bug - - // Setup - Display 3 notifications that will be grouped together. - Bundle bundle = getBaseNotifBundle("UUID1"); - bundle.putString("grp", "test1"); - NotificationBundleProcessor_ProcessFromFCMIntentService(blankActivity, bundle); - threadAndTaskWait(); - - bundle = getBaseNotifBundle("UUID2"); - bundle.putString("grp", "test1"); - NotificationBundleProcessor_ProcessFromFCMIntentService(blankActivity, bundle); - threadAndTaskWait(); - - bundle = getBaseNotifBundle("UUID3"); - bundle.putString("grp", "test1"); - NotificationBundleProcessor_ProcessFromFCMIntentService(blankActivity, bundle); - threadAndTaskWait(); - - Map postedNotifs = ShadowRoboNotificationManager.notifications; - Iterator> postedNotifsIterator = postedNotifs.entrySet().iterator(); - - // Test - 3 notifis + 1 summary - assertEquals(4, postedNotifs.size()); - - // Test - First notification should be the summary - PostedNotification postedSummaryNotification = postedNotifsIterator.next().getValue(); - assertEquals("3 new messages", postedSummaryNotification.getShadow().getContentText()); - assertEquals(Notification.FLAG_GROUP_SUMMARY, postedSummaryNotification.notif.flags & Notification.FLAG_GROUP_SUMMARY); - - // Setup - Let's cancel a child notification. - PostedNotification postedNotification = postedNotifsIterator.next().getValue(); - OneSignal.removeNotification(postedNotification.id); - threadAndTaskWait(); - - // Test - It should update summary text to say 2 notifications - postedNotifs = ShadowRoboNotificationManager.notifications; - assertEquals(3, postedNotifs.size()); // 2 notifis + 1 summary - postedNotifsIterator = postedNotifs.entrySet().iterator(); - postedSummaryNotification = postedNotifsIterator.next().getValue(); - assertEquals("2 new messages", postedSummaryNotification.getShadow().getContentText()); - assertEquals(Notification.FLAG_GROUP_SUMMARY, postedSummaryNotification.notif.flags & Notification.FLAG_GROUP_SUMMARY); - - // Setup - Let's cancel a 2nd child notification. - postedNotification = postedNotifsIterator.next().getValue(); - OneSignal.removeNotification(postedNotification.id); - threadAndTaskWait(); - - runImplicitServices(); - Thread.sleep(1_000); // TODO: Service runs AsyncTask. Need to wait for this - - // Test - It should update summary notification to be the text of the last remaining one. - postedNotifs = ShadowRoboNotificationManager.notifications; - assertEquals(2, postedNotifs.size()); // 1 notifis + 1 summary - postedNotifsIterator = postedNotifs.entrySet().iterator(); - postedSummaryNotification = postedNotifsIterator.next().getValue(); - assertEquals(notifMessage, postedSummaryNotification.getShadow().getContentText()); - assertEquals(Notification.FLAG_GROUP_SUMMARY, postedSummaryNotification.notif.flags & Notification.FLAG_GROUP_SUMMARY); - - // Test - Let's make sure we will have our last notification too - postedNotification = postedNotifsIterator.next().getValue(); - assertEquals(notifMessage, postedNotification.getShadow().getContentText()); - - // Setup - Let's cancel our 3rd and last child notification. - OneSignal.removeNotification(postedNotification.id); - threadAndTaskWait(); - - // Test - No more notifications! :) - postedNotifs = ShadowRoboNotificationManager.notifications; - assertEquals(0, postedNotifs.size()); - } - - // NOTE: SIDE EFFECT: Consumes non-Implicit without running them. - private void runImplicitServices() throws Exception { - do { - Intent intent = Shadows.shadowOf(blankActivity).getNextStartedService(); - if (intent == null) - break; - - ComponentName componentName = intent.getComponent(); - if (componentName == null) - break; - - Class serviceClass = Class.forName(componentName.getClassName()); - - Robolectric.buildService(serviceClass, intent).create().startCommand(0, 0); - } while (true); - } - - @Test - @Config(shadows = { ShadowGenerateNotification.class }) - public void shouldUpdateBadgesWhenDismissingNotification() throws Exception { - Bundle bundle = getBaseNotifBundle(); - NotificationBundleProcessor_ProcessFromFCMIntentService(blankActivity, bundle); - threadAndTaskWait(); - assertEquals(notifMessage, ShadowRoboNotificationManager.getLastShadowNotif().getContentText()); - assertEquals(1, ShadowBadgeCountUpdater.lastCount); - - Map postedNotifs = ShadowRoboNotificationManager.notifications; - Iterator> postedNotifsIterator = postedNotifs.entrySet().iterator(); - PostedNotification postedNotification = postedNotifsIterator.next().getValue(); - Intent intent = Shadows.shadowOf(postedNotification.notif.deleteIntent).getSavedIntent(); - NotificationOpenedProcessor_processFromContext(blankActivity, intent); - - assertEquals(0, ShadowBadgeCountUpdater.lastCount); - } - - @Test - public void shouldNotSetBadgesWhenNotificationPermissionIsDisabled() throws Exception { - ShadowNotificationManagerCompat.enabled = false; - OneSignal.setAppId("b2f7f966-d8cc-11e4-bed1-df8f05be55ba"); - OneSignal.initWithContext(blankActivity); - threadAndTaskWait(); - - Bundle bundle = getBaseNotifBundle(); - NotificationBundleProcessor_ProcessFromFCMIntentService(blankActivity, bundle); - assertEquals(0, ShadowBadgeCountUpdater.lastCount); - } - - @Test - @Config(shadows = { ShadowGenerateNotification.class }) - public void shouldSetBadgesWhenRestoringNotifications() throws Exception { - NotificationBundleProcessor_ProcessFromFCMIntentService(blankActivity, getBaseNotifBundle()); - ShadowBadgeCountUpdater.lastCount = 0; - - OSNotificationRestoreWorkManager.beginEnqueueingWork(blankActivity, false); - threadAndTaskWait(); - - assertEquals(1, ShadowBadgeCountUpdater.lastCount); - } - - @Test - public void shouldNotRestoreNotificationsIfPermissionIsDisabled() throws Exception { - NotificationBundleProcessor_ProcessFromFCMIntentService(blankActivity, getBaseNotifBundle()); - - ShadowNotificationManagerCompat.enabled = false; - OSNotificationRestoreWorkManager.beginEnqueueingWork(blankActivity, false); - threadAndTaskWait(); - - assertNull(Shadows.shadowOf(blankActivity).getNextStartedService()); - } - - @Test - public void shouldNotShowNotificationWhenAlertIsBlankOrNull() throws Exception { - Bundle bundle = getBaseNotifBundle(); - bundle.remove("alert"); - NotificationBundleProcessor_ProcessFromFCMIntentService(blankActivity, bundle); - threadAndTaskWait(); - - assertNoNotifications(); - - bundle = getBaseNotifBundle("UUID2"); - bundle.putString("alert", ""); - NotificationBundleProcessor_ProcessFromFCMIntentService(blankActivity, bundle); - threadAndTaskWait(); - - assertNoNotifications(); - - assertNotificationDbRecords(2); - } - - @Test - @Config(shadows = { ShadowGenerateNotification.class }) - public void shouldUpdateNormalNotificationDisplayWhenReplacingANotification() throws Exception { - // Setup - init - OneSignal.setAppId("b2f7f966-d8cc-11e4-bed1-df8f05be55ba"); - OneSignal.initWithContext(blankActivity); - threadAndTaskWait(); - - // Setup - Display 2 notifications with the same group and collapse_id - Bundle bundle = getBaseNotifBundle("UUID1"); - bundle.putString("grp", "test1"); - bundle.putString("collapse_key", "1"); - NotificationBundleProcessor_ProcessFromFCMIntentService(blankActivity, bundle); - threadAndTaskWait(); - - bundle = getBaseNotifBundle("UUID2"); - bundle.putString("grp", "test1"); - bundle.putString("collapse_key", "1"); - NotificationBundleProcessor_ProcessFromFCMIntentService(blankActivity, bundle); - threadAndTaskWait(); - - // Test - Summary created and sub notification. Summary will look the same as the normal notification. - Map postedNotifs = ShadowRoboNotificationManager.notifications; - Iterator> postedNotifsIterator = postedNotifs.entrySet().iterator(); - assertEquals(2, postedNotifs.size()); - PostedNotification postedSummaryNotification = postedNotifsIterator.next().getValue(); - assertEquals(notifMessage, postedSummaryNotification.getShadow().getContentText()); - assertEquals(Notification.FLAG_GROUP_SUMMARY, postedSummaryNotification.notif.flags & Notification.FLAG_GROUP_SUMMARY); - - int lastNotifId = postedNotifsIterator.next().getValue().id; - ShadowRoboNotificationManager.notifications.clear(); - - // Setup - Restore - BundleCompat bundle2 = createInternalPayloadBundle(bundle); - bundle2.putInt("android_notif_id", lastNotifId); - bundle2.putBoolean("is_restoring", true); - NotificationBundleProcessor_ProcessFromFCMIntentService_NoWrap(blankActivity, bundle2); - threadAndTaskWait(); - - // Test - Restored notifications display exactly the same as they did when received. - postedNotifs = ShadowRoboNotificationManager.notifications; - postedNotifsIterator = postedNotifs.entrySet().iterator(); - // Test - 1 notifi + 1 summary - assertEquals(2, postedNotifs.size()); - assertEquals(notifMessage, postedSummaryNotification.getShadow().getContentText()); - assertEquals(Notification.FLAG_GROUP_SUMMARY, postedSummaryNotification.notif.flags & Notification.FLAG_GROUP_SUMMARY); - } - - @Test - @Config(shadows = { ShadowGenerateNotification.class, ShadowNotificationReceivedEvent.class }) - public void shouldHandleBasicNotifications() throws Exception { - // Make sure the notification got posted and the content is correct. - Bundle bundle = getBaseNotifBundle(); - NotificationBundleProcessor_ProcessFromFCMIntentService(blankActivity, bundle); - threadAndTaskWait(); - - assertEquals(notifMessage, ShadowRoboNotificationManager.getLastShadowNotif().getContentText()); - assertEquals(1, ShadowBadgeCountUpdater.lastCount); - - // Should have 1 DB record with the correct time stamp - SQLiteDatabase readableDb = dbHelper.getSQLiteDatabaseWithRetries(); - Cursor cursor = readableDb.query(NotificationTable.TABLE_NAME, new String[] { "created_time" }, null, null, null, null, null); - assertEquals(1, cursor.getCount()); - // Time stamp should be set and within a small range. - long currentTime = System.currentTimeMillis() / 1000; - cursor.moveToFirst(); - long displayTime = cursor.getLong(0); - // Display time on notification record is within 1 second - assertTrue((displayTime + 1_000) > currentTime && displayTime <= currentTime); - cursor.close(); - - // Should get marked as opened. - NotificationOpenedProcessor_processFromContext(blankActivity, createOpenIntent(bundle)); - cursor = readableDb.query(NotificationTable.TABLE_NAME, new String[] { "opened", "android_notification_id" }, null, null, null, null, null); - cursor.moveToFirst(); - assertEquals(1, cursor.getInt(0)); - assertEquals(0, ShadowBadgeCountUpdater.lastCount); - cursor.close(); - - // Should not display a duplicate notification, count should still be 1 - NotificationBundleProcessor_ProcessFromFCMIntentService(blankActivity, bundle); - threadAndTaskWait(); - - readableDb = dbHelper.getSQLiteDatabaseWithRetries(); - cursor = readableDb.query(NotificationTable.TABLE_NAME, null, null, null, null, null, null); - assertEquals(1, cursor.getCount()); - assertEquals(0, ShadowBadgeCountUpdater.lastCount); - cursor.close(); - - // Display a second notification - bundle = getBaseNotifBundle("UUID2"); - NotificationBundleProcessor_ProcessFromFCMIntentService(blankActivity, bundle); - threadAndTaskWait(); - - // Go forward 4 weeks - // Note: Does not effect the SQL function strftime - time.advanceSystemTimeBy(2_419_202); - - // Restart the app so OneSignalCacheCleaner can clean out old notifications - fastColdRestartApp(); - OneSignal_setTime(time); - threadAndTaskWait(); - - // Display a 3rd notification - // Should of been added for a total of 2 records now. - // First opened should of been cleaned up, 1 week old non opened notification should stay, and one new record. - bundle = getBaseNotifBundle("UUID3"); - NotificationBundleProcessor_ProcessFromFCMIntentService(blankActivity, bundle); - threadAndTaskWait(); - - readableDb = dbHelper.getSQLiteDatabaseWithRetries(); - cursor = readableDb.query(NotificationTable.TABLE_NAME, new String[] { }, null, null, null, null, null); - - assertEquals(1, cursor.getCount()); - - cursor.close(); - } - - @Test - @Config(shadows = { ShadowGenerateNotification.class }) - public void shouldRestoreNotifications() throws Exception { - restoreNotifications(); - threadAndTaskWait(); - - assertEquals(0, ShadowBadgeCountUpdater.lastCount); - - NotificationBundleProcessor_ProcessFromFCMIntentService(blankActivity, getBaseNotifBundle()); - - restoreNotifications(); - threadAndTaskWait(); - - assertEquals(1, ShadowBadgeCountUpdater.lastCount); - -// Intent intent = Shadows.shadowOf(blankActivity).getNextStartedService(); -// assertEquals(RestoreJobService.class.getName(), intent.getComponent().getClassName()); - - // Go forward 1 week - // Note: Does not effect the SQL function strftime - time.advanceSystemTimeBy(604_801); - - // Restorer should not fire service since the notification is over 1 week old. - restoreNotifications(); - threadAndTaskWait(); -// assertNull(Shadows.shadowOf(blankActivity).getNextStartedService()); - - assertEquals(0, ShadowBadgeCountUpdater.lastCount); - } - - private void restoreNotifications() { - OSNotificationRestoreWorkManager.restored = false; - OSNotificationRestoreWorkManager.beginEnqueueingWork(blankActivity, false); - } - - private void helperShouldRestoreNotificationsPastExpireTime(boolean should) throws Exception { - long sentTime = System.currentTimeMillis(); - long ttl = 60L; - - Bundle bundle = getBaseNotifBundle(); - bundle.putLong(OneSignalPackagePrivateHelper.GOOGLE_SENT_TIME_KEY, sentTime); - bundle.putLong(OneSignalPackagePrivateHelper.GOOGLE_TTL_KEY, ttl); - NotificationBundleProcessor_ProcessFromFCMIntentService(blankActivity, bundle); - - restoreNotifications(); - threadAndTaskWait(); - - assertEquals(1, ShadowBadgeCountUpdater.lastCount); - - // Go forward just past the TTL of the notification - time.advanceSystemTimeBy(ttl + 1); - restoreNotifications(); - - if (should) - assertEquals(1, ShadowBadgeCountUpdater.lastCount); - else - assertEquals(0, ShadowBadgeCountUpdater.lastCount); - } - - @Test - @Config(shadows = { ShadowGenerateNotification.class }) - public void doNotRestoreNotificationsPastExpireTime() throws Exception { - helperShouldRestoreNotificationsPastExpireTime(false); - } - - @Test - @Config(shadows = { ShadowGenerateNotification.class }) - public void restoreNotificationsPastExpireTimeIfSettingIsDisabled() throws Exception { - TestOneSignalPrefs.saveBool(TestOneSignalPrefs.PREFS_ONESIGNAL, TestOneSignalPrefs.PREFS_OS_RESTORE_TTL_FILTER, false); - helperShouldRestoreNotificationsPastExpireTime(true); - } - - @Test - public void badgeCountShouldNotIncludeOldNotifications() { - NotificationBundleProcessor_ProcessFromFCMIntentService(blankActivity, getBaseNotifBundle()); - - // Go forward 1 week - time.advanceSystemTimeBy(604_801); - - // Should not count as a badge - OneSignalPackagePrivateHelper.BadgeCountUpdater.update(dbHelper, blankActivity); - assertEquals(0, ShadowBadgeCountUpdater.lastCount); - } - - @Test - @Config(shadows = { ShadowGenerateNotification.class }) - public void shouldGenerate2BasicGroupNotifications() throws Exception { - ShadowOSUtils.hasAllRecommendedFCMLibraries(true); - // First init run for appId to be saved - // At least OneSignal was init once for user to be subscribed - // If this doesn't' happen, notifications will not arrive - OneSignal.setAppId(ONESIGNAL_APP_ID); - OneSignal.initWithContext(blankActivity); - threadAndTaskWait(); - - pauseActivity(blankActivityController); - fastColdRestartApp(); - // We only care about request post first init - ShadowOneSignalRestClient.resetStatics(); - - Log.i(GenerateNotificationRunner.class.getCanonicalName(), "****** AFTER RESET STATICS ******"); - setRemoteParamsGetHtmlResponse(); - ShadowOSUtils.hasAllRecommendedFCMLibraries(true); - - // Make sure the notification got posted and the content is correct. - Bundle bundle = getBaseNotifBundle(); - bundle.putString("grp", "test1"); - NotificationBundleProcessor_ProcessFromFCMIntentService(blankActivity, bundle); - threadAndTaskWait(); - - Map postedNotifs = ShadowRoboNotificationManager.notifications; - assertEquals(2, postedNotifs.size()); - - // Test summary notification - Iterator> postedNotifsIterator = postedNotifs.entrySet().iterator(); - PostedNotification postedNotification = postedNotifsIterator.next().getValue(); - - assertEquals(notifMessage, postedNotification.getShadow().getContentText()); - assertEquals(Notification.FLAG_GROUP_SUMMARY, postedNotification.notif.flags & Notification.FLAG_GROUP_SUMMARY); - - // Test Android Wear notification - postedNotification = postedNotifsIterator.next().getValue(); - assertEquals(notifMessage, postedNotification.getShadow().getContentText()); - assertEquals(0, postedNotification.notif.flags & Notification.FLAG_GROUP_SUMMARY); - // Badge count should only be one as only one notification is visible in the notification area. - assertEquals(1, ShadowBadgeCountUpdater.lastCount); - - // Should be 2 DB entries (summary and individual) - SQLiteDatabase readableDb = dbHelper.getSQLiteDatabaseWithRetries(); - Cursor cursor = readableDb.query(NotificationTable.TABLE_NAME, null, null, null, null, null, null); - assertEquals(2, cursor.getCount()); - cursor.close(); - - // Add another notification to the group. - ShadowRoboNotificationManager.notifications.clear(); - bundle = new Bundle(); - bundle.putString("alert", "Notif test 2"); - bundle.putString("custom", "{\"i\": \"UUID2\"}"); - bundle.putString("grp", "test1"); - NotificationBundleProcessor_ProcessFromFCMIntentService(blankActivity, bundle); - threadAndTaskWait(); - - postedNotifs = ShadowRoboNotificationManager.notifications; - assertEquals(2, postedNotifs.size()); - assertEquals(2, ShadowBadgeCountUpdater.lastCount); - - postedNotifsIterator = postedNotifs.entrySet().iterator(); - postedNotification = postedNotifsIterator.next().getValue(); - assertEquals("2 new messages",postedNotification.getShadow().getContentText()); - assertEquals(Notification.FLAG_GROUP_SUMMARY, postedNotification.notif.flags & Notification.FLAG_GROUP_SUMMARY); - - // Test Android Wear notification - postedNotification = postedNotifsIterator.next().getValue(); - assertEquals("Notif test 2", postedNotification.getShadow().getContentText()); - assertEquals(0, postedNotification.notif.flags & Notification.FLAG_GROUP_SUMMARY); - - // Should be 3 DB entries (summary and 2 individual) - readableDb = dbHelper.getSQLiteDatabaseWithRetries(); - cursor = readableDb.query(NotificationTable.TABLE_NAME, null, null, null, null, null, null); - assertEquals(3, cursor.getCount()); - - // Open summary notification - postedNotifsIterator = postedNotifs.entrySet().iterator(); - postedNotification = postedNotifsIterator.next().getValue(); - Intent intent = createOpenIntent(postedNotification.id, bundle).putExtra("summary", "test1"); - NotificationOpenedProcessor_processFromContext(blankActivity, intent); - // Wait for remote params call - threadAndTaskWait(); - - assertEquals(0, ShadowBadgeCountUpdater.lastCount); - // 2 open calls should fire + remote params + 1 players call - int networkCallCount = ShadowOneSignalRestClient.networkCallCount; - assertEquals("notifications/UUID2", ShadowOneSignalRestClient.requests.get(networkCallCount - 2).url); - assertEquals("notifications/UUID", ShadowOneSignalRestClient.requests.get(networkCallCount - 1).url); - ShadowRoboNotificationManager.notifications.clear(); - - // Send 3rd notification - bundle = new Bundle(); - bundle.putString("alert", "Notif test 3"); - bundle.putString("custom", "{\"i\": \"UUID3\"}"); - bundle.putString("grp", "test1"); - NotificationBundleProcessor_ProcessFromFCMIntentService(blankActivity, bundle); - threadAndTaskWait(); - - postedNotifsIterator = postedNotifs.entrySet().iterator(); - postedNotification = postedNotifsIterator.next().getValue(); - assertEquals("Notif test 3", postedNotification.getShadow().getContentText()); - assertEquals(Notification.FLAG_GROUP_SUMMARY, postedNotification.notif.flags & Notification.FLAG_GROUP_SUMMARY); - assertEquals(1, ShadowBadgeCountUpdater.lastCount); - cursor.close(); - } - - @Test - public void shouldHandleOpeningInAppAlertWithGroupKeySet() { - NotificationSummaryManager_updateSummaryNotificationAfterChildRemoved(blankActivity, dbHelper, "some_group", false); - } - - @Test - @Config(shadows = {ShadowFCMBroadcastReceiver.class, ShadowGenerateNotification.class }) - public void shouldSetButtonsCorrectly() throws Exception { - final Intent intent = new Intent(); - intent.setAction("com.google.android.c2dm.intent.RECEIVE"); - intent.putExtra("message_type", "gcm"); - Bundle bundle = getBaseNotifBundle(); - addButtonsToReceivedPayload(bundle); - intent.putExtras(bundle); - - FCMBroadcastReceiver_onReceived_withIntent(blankActivity, intent); - threadAndTaskWait(); - - // Normal notifications should be generated right from the BroadcastReceiver - // without creating a service. - assertNull(Shadows.shadowOf(blankActivity).getNextStartedService()); - - Map postedNotifs = ShadowRoboNotificationManager.notifications; - Iterator> postedNotifsIterator = postedNotifs.entrySet().iterator(); - PostedNotification lastNotification = postedNotifsIterator.next().getValue(); - - assertEquals(1, lastNotification.notif.actions.length); - String json_data = shadowOf(lastNotification.notif.actions[0].actionIntent).getSavedIntent().getStringExtra(BUNDLE_KEY_ONESIGNAL_DATA); - assertEquals("id1", new JSONObject(json_data).optString(BUNDLE_KEY_ACTION_ID)); - } - - @Test - @Config(sdk = 23, shadows = { ShadowGenerateNotification.class }) - public void shouldUseCorrectActivityForAndroid23Plus() throws Exception { - NotificationBundleProcessor_ProcessFromFCMIntentService(blankActivity, getBaseNotifBundle()); - threadAndTaskWait(); - - Intent[] intents = lastNotificationIntents(); - assertEquals( - com.onesignal.NotificationOpenedReceiver.class.getName(), - intents[0].getComponent().getClassName() - ); - assertEquals(1, intents.length); - } - - @Test - @Config(sdk = 21, shadows = { ShadowGenerateNotification.class }) - public void shouldUseCorrectActivityForLessThanAndroid23() throws Exception { - NotificationBundleProcessor_ProcessFromFCMIntentService(blankActivity, getBaseNotifBundle()); - threadAndTaskWait(); - - Intent[] intents = lastNotificationIntents(); - assertEquals( - com.onesignal.NotificationOpenedReceiverAndroid22AndOlder.class.getName(), - intents[0].getComponent().getClassName() - ); - assertEquals(1, intents.length); - } - - private Intent[] lastNotificationIntents() { - PendingIntent pendingIntent = ShadowRoboNotificationManager.getLastNotif().contentIntent; - // NOTE: This is fragile until this robolectric issue is fixed: https://github.com/robolectric/robolectric/issues/6660 - return shadowOf(pendingIntent).getSavedIntents(); - } - - @Test - @Config(shadows = { ShadowGenerateNotification.class }) - public void shouldSetAlertnessFieldsOnNormalPriority() { - Bundle bundle = getBaseNotifBundle(); - bundle.putString("pri", "5"); // Notifications from dashboard have priority 5 by default - NotificationBundleProcessor_ProcessFromFCMIntentService(blankActivity, bundle); - - assertEquals(NotificationCompat.PRIORITY_DEFAULT, ShadowRoboNotificationManager.getLastNotif().priority); - final int alertnessFlags = Notification.DEFAULT_SOUND | Notification.DEFAULT_LIGHTS | Notification.DEFAULT_VIBRATE; - assertEquals(alertnessFlags, ShadowRoboNotificationManager.getLastNotif().defaults); - } - - @Test - @Config(shadows = { ShadowGenerateNotification.class }) - public void shouldNotSetAlertnessFieldsOnLowPriority() throws Exception { - Bundle bundle = getBaseNotifBundle(); - bundle.putString("pri", "4"); - NotificationBundleProcessor_ProcessFromFCMIntentService(blankActivity, bundle); - threadAndTaskWait(); - - assertEquals(NotificationCompat.PRIORITY_LOW, ShadowRoboNotificationManager.getLastNotif().priority); - assertEquals(0, ShadowRoboNotificationManager.getLastNotif().defaults); - } - - @Test - @Config(shadows = { ShadowGenerateNotification.class }) - public void shouldSetExpireTimeCorrectlyFromGoogleTTL() throws Exception { - long sentTime = 1_553_035_338_000L; - long ttl = 60L; - - Bundle bundle = getBaseNotifBundle(); - bundle.putLong(OneSignalPackagePrivateHelper.GOOGLE_SENT_TIME_KEY, sentTime); - bundle.putLong(OneSignalPackagePrivateHelper.GOOGLE_TTL_KEY, ttl); - NotificationBundleProcessor_ProcessFromFCMIntentService(blankActivity, bundle); - threadAndTaskWait(); - - HashMap notification = TestHelpers.getAllNotificationRecords(dbHelper).get(0); - long expireTime = (Long)notification.get(NotificationTable.COLUMN_NAME_EXPIRE_TIME); - assertEquals(sentTime + (ttl * 1_000), expireTime * 1_000); - } - - @Test - @Config (sdk = 23, shadows = { ShadowGenerateNotification.class }) - public void notShowNotificationPastTTL() throws Exception { - long sentTime = time.getCurrentTimeMillis(); - long ttl = 60L; - - Bundle bundle = getBaseNotifBundle(); - bundle.putLong(OneSignalPackagePrivateHelper.GOOGLE_SENT_TIME_KEY, sentTime); - bundle.putLong(OneSignalPackagePrivateHelper.GOOGLE_TTL_KEY, ttl); - - // Go forward just past the TTL of the notification - time.advanceSystemTimeBy(ttl + 1); - - NotificationBundleProcessor_ProcessFromFCMIntentService(blankActivity, bundle); - threadAndTaskWait(); - - assertEquals(0, ShadowBadgeCountUpdater.lastCount); - } - - @Test - @Config(shadows = { ShadowGenerateNotification.class }) - public void shouldSetExpireTimeCorrectlyWhenMissingFromPayload() throws Exception { - time.freezeTime(); - NotificationBundleProcessor_ProcessFromFCMIntentService(blankActivity, getBaseNotifBundle()); - threadAndTaskWait(); - - long currentTime = time.getCurrentTimeMillis() / 1_000L; - long expireTime = (Long)TestHelpers.getAllNotificationRecords(dbHelper).get(0).get(NotificationTable.COLUMN_NAME_EXPIRE_TIME); - long expectedExpireTime = currentTime + 259_200; - assertEquals(expectedExpireTime, expireTime); - } - - // TODO: Once we figure out the correct way to process notifications with high priority using the WorkManager -// @Test -// @Config(shadows = {ShadowFCMBroadcastReceiver.class}, sdk = 26) -// public void shouldStartFCMServiceOnAndroidOWhenPriorityIsHighAndContainsRemoteResource() { -// Intent intentFCM = new Intent(); -// intentFCM.setAction("com.google.android.c2dm.intent.RECEIVE"); -// intentFCM.putExtra("message_type", "gcm"); -// Bundle bundle = getBaseNotifBundle(); -// bundle.putString("pri", "10"); -// bundle.putString("licon", "http://domain.com/image.jpg"); -// addButtonsToReceivedPayload(bundle); -// intentFCM.putExtras(bundle); -// -// FCMBroadcastReceiver broadcastReceiver = new FCMBroadcastReceiver(); -// broadcastReceiver.onReceive(ApplicationProvider.getApplicationContext(), intentFCM); -// -// Intent blankActivityIntent = Shadows.shadowOf(blankActivity).getNextStartedService(); -// assertEquals(FCMIntentService.class.getName(), blankActivityIntent.getComponent().getClassName()); -// } - - private static @NonNull Bundle inAppPreviewMockPayloadBundle() throws JSONException { - Bundle bundle = new Bundle(); - bundle.putString("custom", new JSONObject() {{ - put("i", "UUID"); - put("a", new JSONObject() {{ - put("os_in_app_message_preview_id", "UUID"); - }}); - }}.toString()); - return bundle; - } - - @Test - @Config(shadows = { ShadowOneSignalRestClient.class, ShadowOSWebView.class }) - public void shouldShowInAppPreviewWhenInFocus() throws Exception { - OneSignal.setAppId("b2f7f966-d8cc-11e4-bed1-df8f05be55ba"); - OneSignal.initWithContext(blankActivity); - blankActivityController.resume(); - threadAndTaskWait(); - - Intent intent = new Intent(); - intent.setAction("com.google.android.c2dm.intent.RECEIVE"); - intent.putExtra("message_type", "gcm"); - intent.putExtras(inAppPreviewMockPayloadBundle()); - - new FCMBroadcastReceiver().onReceive(blankActivity, intent); - threadAndTaskWait(); - threadAndTaskWait(); - assertEquals("PGh0bWw+PC9odG1sPgoKPHNjcmlwdD4KICAgIHNldFBsYXllclRhZ3MoKTsKPC9zY3JpcHQ+", ShadowOSWebView.lastData); - } - - @Test - @Config(shadows = { ShadowOneSignalRestClient.class, ShadowOSWebView.class }) - public void shouldShowInAppPreviewWhenOpeningPreviewNotification() throws Exception { - OneSignal.setAppId("b2f7f966-d8cc-11e4-bed1-df8f05be55ba"); - OneSignal.initWithContext(blankActivity); - blankActivityController.resume(); - threadAndTaskWait(); - - Bundle bundle = new Bundle(); - bundle.putString("custom", new JSONObject() {{ - put("i", "UUID1"); - put("a", new JSONObject() {{ - put("os_in_app_message_preview_id", "UUID"); - }}); - }}.toString()); - - // Grab activity to remove it from the unit test's tracked list. - shadowOf(blankActivity).getNextStartedActivity(); - - Intent notificationOpenIntent = createOpenIntent(2, inAppPreviewMockPayloadBundle()); - NotificationOpenedProcessor_processFromContext(blankActivity, notificationOpenIntent); - threadAndTaskWait(); - threadAndTaskWait(); - assertEquals("PGh0bWw+PC9odG1sPgoKPHNjcmlwdD4KICAgIHNldFBsYXllclRhZ3MoKTsKPC9zY3JpcHQ+", ShadowOSWebView.lastData); - - // Ensure the app is foregrounded. - assertNotNull(shadowOf(blankActivity).getNextStartedActivity()); - } - - @Test - @Config(shadows = { ShadowGenerateNotification.class, ShadowReceiveReceiptController.class }) - public void shouldSendReceivedReceiptWhenEnabled() throws Exception { - ShadowOneSignalRestClient.setRemoteParamsReceiveReceiptsEnable(true); - - String appId = "b2f7f966-d8cc-11e4-bed1-df8f05be55ba"; - OneSignal.setAppId(appId); - OneSignal.initWithContext(blankActivity); - threadAndTaskWait(); - NotificationBundleProcessor_ProcessFromFCMIntentService(blankActivity, getBaseNotifBundle()); - threadAndTaskWait(); - - assertReportReceivedAtIndex( - 2, - "UUID", - new JSONObject() - .put("app_id", appId) - .put("player_id", ShadowOneSignalRestClient.pushUserId) - .put("device_type", 1) - ); - } - - @Test - public void shouldNotSendReceivedReceiptWhenDisabled() throws Exception { - String appId = "b2f7f966-d8cc-11e4-bed1-df8f05be55ba"; - OneSignal.setAppId(appId); - OneSignal.initWithContext(blankActivity); - threadAndTaskWait(); - - NotificationBundleProcessor_ProcessFromFCMIntentService(blankActivity, getBaseNotifBundle()); - threadAndTaskWait(); - - assertRestCalls(2); - } - - @Test - @Config(sdk = 17, shadows = { ShadowGenerateNotification.class }) - public void testNotificationExtensionServiceOverridePropertiesWithSummaryApi17() throws Exception { - // 1. Setup notification extension service as well as notifications to receive - setupNotificationExtensionServiceOverridePropertiesWithSummary(); - - Map postedNotifs = ShadowRoboNotificationManager.notifications; - Iterator> postedNotifsIterator = postedNotifs.entrySet().iterator(); - - // 2. First notification should be the summary with the custom sound set - PostedNotification postedSummaryNotification = postedNotifsIterator.next().getValue(); - assertThat(postedSummaryNotification.notif.flags & Notification.DEFAULT_SOUND, not(Notification.DEFAULT_SOUND)); - assertEquals("content://media/internal/audio/media/32", postedSummaryNotification.notif.sound.toString()); - - // 3. Make sure only 1 summary notification has been posted - assertEquals(1, postedNotifs.size()); - } - - @Test - @Config(sdk = 21, shadows = { ShadowGenerateNotification.class }) - public void testNotificationExtensionServiceOverridePropertiesWithSummary() throws Exception { - // 1. Setup notification extension service as well as received notifications/summary - setupNotificationExtensionServiceOverridePropertiesWithSummary(); - - Map postedNotifs = ShadowRoboNotificationManager.notifications; - Iterator> postedNotifsIterator = postedNotifs.entrySet().iterator(); - - // 2. First notification should be the summary with the custom sound set - PostedNotification postedSummaryNotification = postedNotifsIterator.next().getValue(); - assertThat(postedSummaryNotification.notif.flags & Notification.DEFAULT_SOUND, not(Notification.DEFAULT_SOUND)); - assertEquals("content://media/internal/audio/media/32", postedSummaryNotification.notif.sound.toString()); - - // 3. Individual notification 1 should not play a sound - PostedNotification notification = postedNotifsIterator.next().getValue(); - assertThat(notification.notif.flags & Notification.DEFAULT_SOUND, not(Notification.DEFAULT_SOUND)); - assertNull(notification.notif.sound); - - // 4. Individual notification 2 should not play a sound - notification = postedNotifsIterator.next().getValue(); - assertThat(notification.notif.flags & Notification.DEFAULT_SOUND, not(Notification.DEFAULT_SOUND)); - assertNull(notification.notif.sound); - } - - /** - * Test to make sure changed bodies and titles are used for the summary notification - * @see #testNotificationExtensionServiceOverridePropertiesWithSummaryApi17 - * @see #testNotificationExtensionServiceOverridePropertiesWithSummary - */ - private void setupNotificationExtensionServiceOverridePropertiesWithSummary() throws Exception { - // 1. Setup correct notification extension service class - startRemoteNotificationReceivedHandlerService("com.test.onesignal.GenerateNotificationRunner$" + - "RemoteNotificationReceivedHandler_notificationReceivedOverrideProperties"); - - // 2. Add app context and setup the established notification extension service - OneSignal.initWithContext(ApplicationProvider.getApplicationContext()); - OneSignal_setupNotificationServiceExtension(); - - // 3. Post 2 notifications with the same grp key so a summary is generated - Bundle bundle = getBaseNotifBundle("UUID1"); - bundle.putString("grp", "test1"); - - FCMBroadcastReceiver_processBundle(blankActivity, bundle); - threadAndTaskWait(); - - bundle = getBaseNotifBundle("UUID2"); - bundle.putString("grp", "test1"); - - FCMBroadcastReceiver_processBundle(blankActivity, bundle); - threadAndTaskWait(); - - Map postedNotifs = ShadowRoboNotificationManager.notifications; - Iterator> postedNotifsIterator = postedNotifs.entrySet().iterator(); - - // 4. First notification should be the summary - PostedNotification postedSummaryNotification = postedNotifsIterator.next().getValue(); - assertEquals("2 new messages", postedSummaryNotification.getShadow().getContentText()); - if (Build.VERSION.SDK_INT > Build.VERSION_CODES.KITKAT) - assertEquals(Notification.FLAG_GROUP_SUMMARY, postedSummaryNotification.notif.flags & Notification.FLAG_GROUP_SUMMARY); - - // 5. Make sure summary build saved and used the developer's extender settings for the body and title - if (Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN_MR2) { - CharSequence[] lines = postedSummaryNotification.notif.extras.getCharSequenceArray(Notification.EXTRA_TEXT_LINES); - for (CharSequence line : lines) - assertEquals("[Modified Title] [Modified Body(ContentText)]", line.toString()); - } - } - - /** - * @see #testNotificationExtensionServiceOverridePropertiesWithSummaryApi17 - * @see #testNotificationExtensionServiceOverridePropertiesWithSummary - */ - public static class RemoteNotificationReceivedHandler_notificationReceivedOverrideProperties implements OneSignal.OSRemoteNotificationReceivedHandler { - - @Override - public void remoteNotificationReceived(Context context, OSNotificationReceivedEvent receivedEvent) { - OSMutableNotification mutableNotification = receivedEvent.getNotification().mutableCopy(); - - mutableNotification.setExtender(builder -> { - // Must disable the default sound when setting a custom one - try { - Field mNotificationField = NotificationCompat.Builder.class.getDeclaredField("mNotification"); - mNotificationField.setAccessible(true); - Notification mNotification = (Notification) mNotificationField.get(builder); - - mNotification.flags &= ~Notification.DEFAULT_SOUND; - builder.setDefaults(mNotification.flags); - } catch (Throwable t) { - t.printStackTrace(); - } - - return builder.setSound(Uri.parse("content://media/internal/audio/media/32")) - .setColor(new BigInteger("FF00FF00", 16).intValue()) - .setContentTitle("[Modified Title]") - .setStyle(new NotificationCompat.BigTextStyle().bigText("[Modified Body(bigText)]")) - .setContentText("[Modified Body(ContentText)]"); - }); - - // Complete is called to end NotificationProcessingHandler - // Sending a notifications will display it with the modifications - receivedEvent.complete(mutableNotification); - } - } - - @Test - @Config(shadows = { ShadowOneSignal.class, ShadowGenerateNotification.class }) - public void testRemoteNotificationReceivedHandler_notificationProcessingProperties() throws Exception { - // 1. Setup correct notification extension service class - startRemoteNotificationReceivedHandlerService("com.test.onesignal.GenerateNotificationRunner$" + - "RemoteNotificationReceivedHandler_notificationReceivedProperties"); - - // 2. Add app context and setup the established notification extension service - OneSignal.initWithContext(ApplicationProvider.getApplicationContext()); - OneSignal_setupNotificationServiceExtension(); - final boolean[] callbackEnded = {false}; - OneSignalPackagePrivateHelper.ProcessBundleReceiverCallback processBundleReceiverCallback = new OneSignalPackagePrivateHelper.ProcessBundleReceiverCallback() { - public void onBundleProcessed(OneSignalPackagePrivateHelper.ProcessedBundleResult processedResult) { - assertNotNull(processedResult); - // 3. Test that WorkManager begins processing the notification - assertTrue(processedResult.isProcessed()); - callbackEnded[0] = true; - } - }; - - FCMBroadcastReceiver_processBundle(blankActivity, getBaseNotifBundle(), processBundleReceiverCallback); - Awaitility.await() - .atMost(new Duration(3, TimeUnit.SECONDS)) - .pollInterval(new Duration(100, TimeUnit.MILLISECONDS)) - .untilAsserted(() -> { - assertTrue(callbackEnded[0]); - }); - - // 4. Receive a notification with all data fields used - FCMBroadcastReceiver_processBundle(blankActivity, getBundleWithAllOptionsSet()); - threadAndTaskWait(); - - // 5. Evaluate the notification received within the NotificationProcessingHandler - OSNotificationReceivedEvent receivedEvent = RemoteNotificationReceivedHandler_notificationReceivedProperties.notification; - OSNotification notification = receivedEvent.getNotification(); - assertEquals("Test H", notification.getTitle()); - assertEquals("Test B", notification.getBody()); - assertEquals("9764eaeb-10ce-45b1-a66d-8f95938aaa51", notification.getNotificationId()); - - assertEquals(0, notification.getLockScreenVisibility()); - assertEquals("FF0000FF", notification.getSmallIconAccentColor()); - assertEquals("703322744261", notification.getFromProjectNumber()); - assertEquals("FFFFFF00", notification.getLedColor()); - assertEquals("big_picture", notification.getBigPicture()); - assertEquals("large_icon", notification.getLargeIcon()); - assertEquals("small_icon", notification.getSmallIcon()); - assertEquals("test_sound", notification.getSound()); - assertEquals("You test $[notif_count] MSGs!", notification.getGroupMessage()); - assertEquals("http://google.com", notification.getLaunchURL()); - assertEquals(10, notification.getPriority()); - assertEquals("a_key", notification.getCollapseId()); - - assertEquals("id1", notification.getActionButtons().get(0).getId()); - assertEquals("button1", notification.getActionButtons().get(0).getText()); - assertEquals("ic_menu_share", notification.getActionButtons().get(0).getIcon()); - assertEquals("id2", notification.getActionButtons().get(1).getId()); - assertEquals("button2", notification.getActionButtons().get(1).getText()); - assertEquals("ic_menu_send", notification.getActionButtons().get(1).getIcon()); - - assertEquals("test_image_url", notification.getBackgroundImageLayout().getImage()); - assertEquals("FF000000", notification.getBackgroundImageLayout().getTitleTextColor()); - assertEquals("FFFFFFFF", notification.getBackgroundImageLayout().getBodyTextColor()); - - JSONObject additionalData = notification.getAdditionalData(); - assertEquals("myValue", additionalData.getString("myKey")); - assertEquals("nValue", additionalData.getJSONObject("nested").getString("nKey")); - - // 6. Make sure the notification id is not -1 (not restoring) - assertThat(RemoteNotificationReceivedHandler_notificationReceivedProperties.notificationId, not(-1)); - - // 7. Test a basic notification without anything special - FCMBroadcastReceiver_processBundle(blankActivity, getBaseNotifBundle()); - threadAndTaskWait(); - assertFalse(ShadowOneSignal.messages.contains("Error assigning")); - - // 8. Test that a notification is still displayed if the developer's code in onNotificationProcessing throws an Exception - RemoteNotificationReceivedHandler_notificationReceivedProperties.throwInAppCode = true; - FCMBroadcastReceiver_processBundle(blankActivity, getBaseNotifBundle("NewUUID1")); - threadAndTaskWait(); - - // 9. Make sure 3 notifications have been posted - assertTrue(ShadowOneSignal.messages.contains("remoteNotificationReceived throw an exception")); - Map postedNotifs = ShadowRoboNotificationManager.notifications; - assertEquals(3, postedNotifs.size()); - } - - /** - * @see #testRemoteNotificationReceivedHandler_notificationProcessingProperties - */ - public static class RemoteNotificationReceivedHandler_notificationReceivedProperties implements OneSignal.OSRemoteNotificationReceivedHandler { - public static OSNotificationReceivedEvent notification; - public static int notificationId = -1; - public static boolean throwInAppCode; - - @Override - public void remoteNotificationReceived(Context context, OSNotificationReceivedEvent receivedEvent) { - if (throwInAppCode) - throw new NullPointerException(); - - RemoteNotificationReceivedHandler_notificationReceivedProperties.notification = receivedEvent; - RemoteNotificationReceivedHandler_notificationReceivedProperties.notificationId = receivedEvent.getNotification().getAndroidNotificationId(); - - OSNotification notification = receivedEvent.getNotification(); - OSMutableNotification mutableNotification = notification.mutableCopy(); - - receivedEvent.complete(mutableNotification); - } - } - - @Test - @Config(shadows = { ShadowGenerateNotification.class }) - public void testNotificationProcessing_twoNotificationsWithSameOverrideAndroidNotificationId() throws Exception { - // 1. Setup correct notification extension service class - startRemoteNotificationReceivedHandlerService("com.test.onesignal.GenerateNotificationRunner$" + - "RemoteNotificationReceivedHandler_notificationReceivedOverrideAndroidNotification"); - - // 2. Add app context and setup the established notification extension service - OneSignal.initWithContext(ApplicationProvider.getApplicationContext()); - OneSignal_setupNotificationServiceExtension(); - - // 3. Generate two notifications with different API notification ids - FCMBroadcastReceiver_processBundle(blankActivity, getBaseNotifBundle("NewUUID1")); - threadAndTaskWait(); - - // 4. Make sure service was called - assertNotNull(lastServiceNotificationReceivedEvent); - lastServiceNotificationReceivedEvent = null; - - FCMBroadcastReceiver_processBundle(blankActivity, getBaseNotifBundle("NewUUID2")); - threadAndTaskWait(); - - // 5. Make sure service was called - assertNotNull(lastServiceNotificationReceivedEvent); - - // 6. Only 1 notification count should exist since the same Android Notification Id was used - assertEquals(1, ShadowBadgeCountUpdater.lastCount); - } - - /** - * @see #testNotificationProcessing_twoNotificationsWithSameOverrideAndroidNotificationId - */ - public static class RemoteNotificationReceivedHandler_notificationReceivedOverrideAndroidNotification implements OneSignal.OSRemoteNotificationReceivedHandler { - - @Override - public void remoteNotificationReceived(final Context context, OSNotificationReceivedEvent receivedEvent) { - lastServiceNotificationReceivedEvent = receivedEvent; - - OSNotification notification = receivedEvent.getNotification(); - OSMutableNotification mutableNotification = notification.mutableCopy(); - mutableNotification.setAndroidNotificationId(1); - - // Complete is called to end NotificationProcessingHandler - receivedEvent.complete(mutableNotification); - } - } - - @Test - // No MainThread mock since notification is not being display - public void testNotificationProcessing_whenAlertIsNull() throws Exception { - // 1. Setup correct notification extension service class - startRemoteNotificationReceivedHandlerService("com.test.onesignal.GenerateNotificationRunner$" + - "RemoteNotificationReceivedHandler_notificationReceivedDisplayAndComplete"); - - // 2. Add app context and setup the established notification extension service - OneSignal.initWithContext(ApplicationProvider.getApplicationContext()); - OneSignal_setupNotificationServiceExtension(); - - // 3. Receive a notification - Bundle bundle = getBaseNotifBundle(); - bundle.remove("alert"); - FCMBroadcastReceiver_processBundle(blankActivity, bundle); - threadAndTaskWait(); - - // 4. Make sure service was called - assertNotNull(lastServiceNotificationReceivedEvent); - - // 5. Make sure 1 notification exists in DB - assertNotificationDbRecords(1); - } - - @Test - @Config(shadows = { ShadowGenerateNotification.class }) - public void testNotificationProcessing_whenAppSwipedAway() throws Exception { - // 1. Setup correct notification extension service class - startRemoteNotificationReceivedHandlerService("com.test.onesignal.GenerateNotificationRunner$" + - "RemoteNotificationReceivedHandler_notificationReceivedDisplayAndComplete"); - - // First init run for appId to be saved - // At least OneSignal was init once for user to be subscribed - // If this doesn't' happen, notifications will not arrive - OneSignal.setAppId(ONESIGNAL_APP_ID); - OneSignal.initWithContext(blankActivity); - threadAndTaskWait(); - fastColdRestartApp(); - // We only care about request post first init - ShadowOneSignalRestClient.resetStatics(); - - // 2. Add app context and setup the established notification extension service under init - // We should not wait for thread to finish since in true behaviour app will continue processing even if thread are running - // Example: don't wait for remote param call Extension Service should be set independently of the threads - OneSignal.initWithContext(ApplicationProvider.getApplicationContext()); - - // 3. Receive a notification - Bundle bundle = getBaseNotifBundle(); - FCMBroadcastReceiver_processBundle(blankActivity, bundle); - threadAndTaskWait(); - - // 4. Make sure service was called - assertNotNull(lastServiceNotificationReceivedEvent); - - // 5. Make sure 1 notification exists in DB - assertNotificationDbRecords(1); - } - - /** - * @see #testNotificationProcessing_whenAlertIsNull - * @see #testNotificationProcessing_whenAppSwipedAway - */ - public static class RemoteNotificationReceivedHandler_notificationReceivedDisplayAndComplete implements OneSignal.OSRemoteNotificationReceivedHandler { - - @Override - public void remoteNotificationReceived(Context context, OSNotificationReceivedEvent receivedEvent) { - lastServiceNotificationReceivedEvent = receivedEvent; - - // Complete is called to end NotificationProcessingHandler - receivedEvent.complete(receivedEvent.getNotification()); - } - } - - @Test - @Config(shadows = { ShadowGenerateNotification.class }) - public void testNotificationProcessing_silentNotification() throws Exception { - // 1. Setup correct notification extension service class - startRemoteNotificationReceivedHandlerService("com.test.onesignal.GenerateNotificationRunner$" + - "RemoteNotificationReceivedHandler_notificationReceivedDisplayNotCalled"); - - // 2. Add app context and setup the established notification extension service - OneSignal.initWithContext(ApplicationProvider.getApplicationContext()); - OneSignal_setupNotificationServiceExtension(); - - // 3. Receive a notification - Bundle bundle = getBaseNotifBundle(); - FCMBroadcastReceiver_processBundle(blankActivity, bundle); - threadAndTaskWait(); - - // 4. Make sure service was called - assertNotNull(lastServiceNotificationReceivedEvent); - - // 5. Make sure running on main thread check was not called, this is only called for showing the notification - assertFalse(ShadowGenerateNotification.isRunningOnMainThreadCheckCalled()); - - // 6. Make sure 1 notification exists in DB - assertNotificationDbRecords(1); - } - - @Test - @Config(shadows = { ShadowGenerateNotification.class }) - public void testNotificationProcessingAndForegroundHandler_displayNotCalled_notCallsForegroundHandler() throws Exception { - // 1. Setup correct notification extension service class - startRemoteNotificationReceivedHandlerService("com.test.onesignal.GenerateNotificationRunner$" + - "RemoteNotificationReceivedHandler_notificationReceivedDisplayNotCalled"); - - // 2. Init OneSignal - OneSignal.setAppId("b2f7f966-d8cc-11e4-bed1-df8f05be55ba"); - OneSignal.initWithContext(blankActivity); - OneSignal.setNotificationWillShowInForegroundHandler(notificationReceivedEvent -> { - lastForegroundNotificationReceivedEvent = notificationReceivedEvent; - - // Call complete to end without waiting default 30 second timeout - notificationReceivedEvent.complete(notificationReceivedEvent.getNotification()); - }); - threadAndTaskWait(); - - blankActivityController.resume(); - threadAndTaskWait(); - - // 3. Receive a notification in foreground - FCMBroadcastReceiver_processBundle(blankActivity, getBaseNotifBundle()); - threadAndTaskWait(); - - // 4. Make sure service was called - assertNotNull(lastServiceNotificationReceivedEvent); - - // 5. Make sure running on main thread check was not called, this is only called for showing the notification - assertFalse(ShadowGenerateNotification.isRunningOnMainThreadCheckCalled()); - - // 6. Make sure the AppNotificationJob is null - assertNull(lastForegroundNotificationReceivedEvent); - - // 7. Make sure 1 notification exists in DB - assertNotificationDbRecords(1); - } - - /** - * @see #testNotificationProcessing_silentNotification - * @see #testNotificationProcessingAndForegroundHandler_displayNotCalled_notCallsForegroundHandler - */ - public static class RemoteNotificationReceivedHandler_notificationReceivedDisplayNotCalled implements OneSignal.OSRemoteNotificationReceivedHandler { - - @Override - public void remoteNotificationReceived(Context context, OSNotificationReceivedEvent notification) { - lastServiceNotificationReceivedEvent = notification; - - // Complete is called to end NotificationProcessingHandler - // Sending a null notification will avoid notification display - notification.complete(null); - } - } - - @Test - @Ignore - @Config(shadows = { ShadowGenerateNotification.class }) - public void testNotificationProcessingAndForegroundHandler_displayCalled_noMutateId() throws Exception { - // 1. Setup correct notification extension service class - startRemoteNotificationReceivedHandlerService("com.test.onesignal.GenerateNotificationRunner$" + - "RemoteNotificationReceivedHandler_notificationReceivedCompleteCalledNotMutateId"); - - // 2. Init OneSignal - OneSignal.setAppId("b2f7f966-d8cc-11e4-bed1-df8f05be55ba"); - OneSignal.initWithContext(blankActivity); - OneSignal.setNotificationWillShowInForegroundHandler(notificationReceivedEvent -> { - lastForegroundNotificationReceivedEvent = notificationReceivedEvent; - - int androidId = notificationReceivedEvent.getNotification().getAndroidNotificationId(); - // Call complete to end without waiting default 30 second timeout - notificationReceivedEvent.complete(notificationReceivedEvent.getNotification()); - - int androidIdAfterComplete = notificationReceivedEvent.getNotification().getAndroidNotificationId(); - assertEquals(androidId, androidIdAfterComplete); - }); - threadAndTaskWait(); - - blankActivityController.resume(); - threadAndTaskWait(); - - // 3. Receive a notification in foreground - FCMBroadcastReceiver_processBundle(blankActivity, getBaseNotifBundle()); - threadAndTaskWait(); - - // 4. Make sure service was called - assertNotNull(lastServiceNotificationReceivedEvent); - - // 5. Make sure foreground handler was called - assertNotNull(lastForegroundNotificationReceivedEvent); - - // 6. Make sure android Id was set after first process and foreground android Id was retrieved - assertEquals(0, lastServiceNotificationReceivedEvent.getNotification().getAndroidNotificationId()); - assertNotEquals(0, lastForegroundNotificationReceivedEvent.getNotification().getAndroidNotificationId()); - assertNotEquals(-1, lastForegroundNotificationReceivedEvent.getNotification().getAndroidNotificationId()); - - // 7. Make sure 1 notification exists in DB - assertNotificationDbRecords(1); - } - - /** - * @see #testNotificationProcessing_silentNotification - * @see #testNotificationProcessingAndForegroundHandler_displayNotCalled_notCallsForegroundHandler - */ - public static class RemoteNotificationReceivedHandler_notificationReceivedCompleteCalledNotMutateId implements OneSignal.OSRemoteNotificationReceivedHandler { - - @Override - public void remoteNotificationReceived(Context context, OSNotificationReceivedEvent receivedEvent) { - lastServiceNotificationReceivedEvent = receivedEvent; - - // Complete is called to end NotificationProcessingHandler - int androidId = receivedEvent.getNotification().getAndroidNotificationId(); - receivedEvent.complete(receivedEvent.getNotification()); - // Call complete to end without waiting default 30 second timeout - int androidIdAfterComplete = receivedEvent.getNotification().getAndroidNotificationId(); - assertEquals(androidId, androidIdAfterComplete); - } - } - - @Test - @Config(shadows = { ShadowGenerateNotification.class, ShadowTimeoutHandler.class }) - public void testNotificationProcessing_completeNotCalled() throws Exception { - // 1. Setup correct notification extension service class - startRemoteNotificationReceivedHandlerService("com.test.onesignal.GenerateNotificationRunner$" + - "RemoteNotificationReceivedHandler_notificationReceivedCompleteNotCalled"); - - // 2. Add app context and setup the established notification extension service - OneSignal.initWithContext(ApplicationProvider.getApplicationContext()); - OneSignal_setupNotificationServiceExtension(); - - // Mock timeout to 1, given that we are not calling complete inside RemoteNotificationReceivedHandler we depend on the timeout complete - ShadowTimeoutHandler.setMockDelayMillis(1); - - // 3. Receive a notification - Bundle bundle = getBaseNotifBundle(); - FCMBroadcastReceiver_processBundle(blankActivity, bundle); - threadAndTaskWait(); - - // 4. Make sure service was called - assertNotNull(lastServiceNotificationReceivedEvent); - - // Notification save is done on the OSTimeoutHandler, we wait until the notification is saved - Awaitility.await() - .atMost(new Duration(3, TimeUnit.SECONDS)) - .pollInterval(new Duration(100, TimeUnit.MILLISECONDS)) - .untilAsserted(() -> { - // 5. Make sure 1 notification exists in DB - assertNotificationDbRecords(1); - }); - } - - @Test - @Config(shadows = { ShadowGenerateNotification.class, ShadowTimeoutHandler.class }) - public void testNotificationProcessingAndForegroundHandler_completeNotCalled_callsForegroundHandler() throws Exception { - // 1. Setup correct notification extension service class - startRemoteNotificationReceivedHandlerService("com.test.onesignal.GenerateNotificationRunner$" + - "RemoteNotificationReceivedHandler_notificationReceivedCompleteNotCalled"); - - // 2. Init OneSignal - OneSignal.setAppId("b2f7f966-d8cc-11e4-bed1-df8f05be55ba"); - OneSignal.initWithContext(blankActivity); - OneSignal.setNotificationWillShowInForegroundHandler(notificationReceivedEvent -> { - lastForegroundNotificationReceivedEvent = notificationReceivedEvent; - OneSignal.onesignalLog(OneSignal.LOG_LEVEL.DEBUG, "Calling OSNotificationReceivedEvent complete from setNotificationWillShowInForegroundHandler test handler"); - // Call complete to end without waiting default 30 second timeout - notificationReceivedEvent.complete(notificationReceivedEvent.getNotification()); - }); - threadAndTaskWait(); - - blankActivityController.resume(); - threadAndTaskWait(); - - // Mock timeout to 1, given that we are not calling complete inside RemoteNotificationReceivedHandler we depend on the timeout complete - ShadowTimeoutHandler.setMockDelayMillis(1); - - // 3. Receive a notification in foreground - FCMBroadcastReceiver_processBundle(blankActivity, getBaseNotifBundle()); - threadAndTaskWait(); - - // 4. Make sure service was called - assertNotNull(lastServiceNotificationReceivedEvent); - - // 5. Make sure the last OSNotificationReceivedEvent is not null - assertNotNull(lastForegroundNotificationReceivedEvent); - - // Notification save is done on the OSTimeoutHandler, we wait until the notification is saved - Awaitility.await() - .atMost(new Duration(3, TimeUnit.SECONDS)) - .pollInterval(new Duration(100, TimeUnit.MILLISECONDS)) - .untilAsserted(() -> { - // 6. Make sure 1 notification exists in DB - assertNotificationDbRecords(1); - }); - } - - /** - * @see #testNotificationProcessing_completeNotCalled - * @see #testNotificationProcessingAndForegroundHandler_completeNotCalled_callsForegroundHandler - */ - public static class RemoteNotificationReceivedHandler_notificationReceivedCompleteNotCalled implements OneSignal.OSRemoteNotificationReceivedHandler { - - @Override - public void remoteNotificationReceived(Context context, OSNotificationReceivedEvent receivedEvent) { - lastServiceNotificationReceivedEvent = receivedEvent; - - OneSignal.onesignalLog(OneSignal.LOG_LEVEL.DEBUG, "RemoteNotificationReceivedHandler_notificationReceivedCompleteNotCalled remoteNotificationReceived called"); - // Complete not called to end NotificationProcessingHandler, depend on timeout to finish - } - } - - @Test - @Config(shadows = { ShadowGenerateNotification.class }) - public void testNotificationProcessingAndForegroundHandler_callCompleteWithMutableNotification_displays() throws Exception { - // 1. Setup correct notification extension service class - startRemoteNotificationReceivedHandlerService( - RemoteNotificationReceivedHandler_notificationReceivedCallCompleteWithMutableNotification - .class - .getName() - ); - - // 2. Init OneSignal - OneSignal.setAppId("b2f7f966-d8cc-11e4-bed1-df8f05be55ba"); - OneSignal.initWithContext(blankActivity); - OneSignal.setNotificationWillShowInForegroundHandler(notificationReceivedEvent -> { - lastForegroundNotificationReceivedEvent = notificationReceivedEvent; - - // Call complete to end without waiting default 30 second timeout - notificationReceivedEvent.complete(notificationReceivedEvent.getNotification()); - }); - threadAndTaskWait(); - - blankActivityController.resume(); - threadAndTaskWait(); - - // 3. Receive a notification in foreground - FCMBroadcastReceiver_processBundle(blankActivity, getBaseNotifBundle()); - threadAndTaskWait(); - - // 4. Make sure service was called - assertNotNull(lastServiceNotificationReceivedEvent); - - // 5. Make sure foreground handler was called - assertNotNull(lastForegroundNotificationReceivedEvent); - - // 6. Make sure running on main thread check is called, this is only called for showing the notification - assertTrue(ShadowGenerateNotification.isRunningOnMainThreadCheckCalled()); - - // 7. Check badge count to represent the notification is displayed - assertEquals(1, ShadowBadgeCountUpdater.lastCount); - } - - /** - * @see #testNotificationProcessingAndForegroundHandler_callCompleteWithMutableNotification_displays - */ - public static class RemoteNotificationReceivedHandler_notificationReceivedCallCompleteWithMutableNotification implements OneSignal.OSRemoteNotificationReceivedHandler { - - @Override - public void remoteNotificationReceived(final Context context, OSNotificationReceivedEvent receivedEvent) { - lastServiceNotificationReceivedEvent = receivedEvent; - - OSNotification notification = receivedEvent.getNotification(); - OSMutableNotification mutableNotification = notification.mutableCopy(); - - // Complete is called to end NotificationProcessingHandler - receivedEvent.complete(mutableNotification); - } - } - - @Test - @Config(shadows = { ShadowGenerateNotification.class }) - public void testNotificationWillShowInForegroundHandlerIsCallWhenReceivingNotificationInForeground() throws Exception { - // 1. Init OneSignal - OneSignal.setAppId("b2f7f966-d8cc-11e4-bed1-df8f05be55ba"); - OneSignal.initWithContext(blankActivity); - OneSignal.setNotificationWillShowInForegroundHandler(notificationReceivedEvent -> { - lastForegroundNotificationReceivedEvent = notificationReceivedEvent; - // Call complete to end without waiting default 30 second timeout - notificationReceivedEvent.complete(notificationReceivedEvent.getNotification()); - }); - threadAndTaskWait(); - - blankActivityController.resume(); - threadAndTaskWait(); - - // 2. Receive a notification - FCMBroadcastReceiver_processBundle(blankActivity, getBaseNotifBundle()); - threadAndTaskWait(); - - // 3. Make sure last OSNotificationReceivedEvent is not null, this indicates was displayed - assertNotNull(lastForegroundNotificationReceivedEvent); - - // 4. Make sure 1 notification exists in DB - assertNotificationDbRecords(1); - } - - @Test - @Config(shadows = { ShadowGenerateNotification.class }) - public void testNotificationWillShowInForegroundHandler_setNotificationDisplayOptionToNotification() throws Exception { - // 1. Init OneSignal - OneSignal.setAppId("b2f7f966-d8cc-11e4-bed1-df8f05be55ba"); - OneSignal.initWithContext(blankActivity); - OneSignal.setNotificationWillShowInForegroundHandler(notificationReceivedEvent -> { - lastForegroundNotificationReceivedEvent = notificationReceivedEvent; - - // Call complete to end without waiting default 30 second timeout - notificationReceivedEvent.complete(notificationReceivedEvent.getNotification()); - }); - threadAndTaskWait(); - - blankActivityController.resume(); - threadAndTaskWait(); - - // 2. Receive a notification - FCMBroadcastReceiver_processBundle(blankActivity, getBaseNotifBundle()); - threadAndTaskWait(); - - // 3. Make sure the AppNotificationJob is not null - assertNotNull(lastForegroundNotificationReceivedEvent); - - // 4. Make sure 1 notification exists in DB - assertNotificationDbRecords(1); - } - - @Test - @Config(shadows = { ShadowGenerateNotification.class }) - public void testNotificationWillShowInForegroundHandler_setNotificationDisplayOptionToSilent() throws Exception { - // 1. Init OneSignal - OneSignal.setAppId("b2f7f966-d8cc-11e4-bed1-df8f05be55ba"); - OneSignal.initWithContext(blankActivity); - OneSignal.setNotificationWillShowInForegroundHandler(notificationReceivedEvent -> { - lastForegroundNotificationReceivedEvent = notificationReceivedEvent; - - // Call complete to end without waiting default 30 second timeout - notificationReceivedEvent.complete(null); - }); - threadAndTaskWait(); - - blankActivityController.resume(); - threadAndTaskWait(); - - // 2. Receive a notification - FCMBroadcastReceiver_processBundle(blankActivity, getBaseNotifBundle()); - threadAndTaskWait(); - - // 3. Make sure the AppNotificationJob is not null - assertNotNull(lastForegroundNotificationReceivedEvent); - - // 4. Make sure 1 notification exists in DB - assertNotificationDbRecords(1); - } - - @Test - @Config(shadows = { ShadowGenerateNotification.class }) - public void testNotificationCompletesFlows_withNullNotificationWillShowInForegroundHandler() throws Exception { - // 1. Init OneSignal - OneSignal.setAppId("b2f7f966-d8cc-11e4-bed1-df8f05be55ba"); - OneSignal.initWithContext(blankActivity); - threadAndTaskWait(); - - blankActivityController.resume(); - threadAndTaskWait(); - - // 3. Receive a notification - FCMBroadcastReceiver_processBundle(blankActivity, getBaseNotifBundle()); - threadAndTaskWait(); - - // 4. Make sure the AppNotificationJob is null - assertNull(lastForegroundNotificationReceivedEvent); - - // 5. Make sure 1 notification exists in DB - assertNotificationDbRecords(1); - } - - @Test - @Config(shadows = { ShadowGenerateNotification.class }) - public void testNotificationWillShowInForegroundHandlerWithActivityAlreadyResumed_doesNotFireWhenAppBackgrounded() throws Exception { - blankActivityController.resume(); - - // 1. Init OneSignal - OneSignal.setAppId("b2f7f966-d8cc-11e4-bed1-df8f05be55ba"); - OneSignal.initWithContext(blankActivity); - OneSignal.setNotificationWillShowInForegroundHandler(notificationReceivedEvent -> { - lastForegroundNotificationReceivedEvent = notificationReceivedEvent; - - // Call complete to end without waiting default 30 second timeout - notificationReceivedEvent.complete(notificationReceivedEvent.getNotification()); - }); - threadAndTaskWait(); - - // 3. Background the app - pauseActivity(blankActivityController); - - // 4. Receive a notification - FCMBroadcastReceiver_processBundle(blankActivity, getBaseNotifBundle()); - threadAndTaskWait(); - - // 5. Make sure the AppNotificationJob is null since the app is in background when receiving a notification - assertNull(lastForegroundNotificationReceivedEvent); - - // 6. Make sure 1 notification exists in DB - assertNotificationDbRecords(1); - } - - @Test - @Config(shadows = { ShadowGenerateNotification.class }) - public void testNotificationWillShowInForegroundHandler_doesNotFireWhenAppBackgrounded() throws Exception { - // 1. Init OneSignal - OneSignal.setAppId("b2f7f966-d8cc-11e4-bed1-df8f05be55ba"); - OneSignal.initWithContext(blankActivity); - OneSignal.setNotificationWillShowInForegroundHandler(notificationReceivedEvent -> { - lastForegroundNotificationReceivedEvent = notificationReceivedEvent; - - // Call complete to end without waiting default 30 second timeout - notificationReceivedEvent.complete(notificationReceivedEvent.getNotification()); - }); - threadAndTaskWait(); - - blankActivityController.resume(); - threadAndTaskWait(); - - // 3. Background the app - pauseActivity(blankActivityController); - - // 4. Receive a notification - FCMBroadcastReceiver_processBundle(blankActivity, getBaseNotifBundle()); - threadAndTaskWait(); - - // 5. Make sure the AppNotificationJob is null since the app is in background when receiving a notification - assertNull(lastForegroundNotificationReceivedEvent); - - // 6. Make sure 1 notification exists in DB - assertNotificationDbRecords(1); - } - - @Test - @Config(shadows = { ShadowGenerateNotification.class, ShadowTimeoutHandler.class, ShadowNotificationReceivedEvent.class }) - public void testNotificationWillShowInForegroundHandler_notCallCompleteShowsNotificationAfterTimeout() throws Exception { - OneSignal.setLogLevel(OneSignal.LOG_LEVEL.VERBOSE, OneSignal.LOG_LEVEL.NONE); - // 1. Init OneSignal - OneSignal.setAppId("b2f7f966-d8cc-11e4-bed1-df8f05be55ba"); - OneSignal.initWithContext(blankActivity); - OneSignal.setNotificationWillShowInForegroundHandler(notificationReceivedEvent -> { - callbackCounter++; - lastForegroundNotificationReceivedEvent = notificationReceivedEvent; - }); - threadAndTaskWait(); - - blankActivityController.resume(); - threadAndTaskWait(); - - // Mock timeout to 1, given that we are not calling complete inside NotificationWillShowInForegroundHandler we depend on the timeout complete - ShadowTimeoutHandler.setMockDelayMillis(1); - // 2. Receive a notification - FCMBroadcastReceiver_processBundle(blankActivity, getBaseNotifBundle()); - threadAndTaskWait(); - - // 3. Make sure the AppNotificationJob is not null - assertNotNull(lastForegroundNotificationReceivedEvent); - - // 4. Make sure the callback counter is only fired once for App NotificationWillShowInForegroundHandler - assertEquals(1, callbackCounter); - - // Complete being call from Notification Receiver Handler thread - Awaitility.await() - .atMost(new Duration(5, TimeUnit.SECONDS)) - .pollInterval(new Duration(500, TimeUnit.MILLISECONDS)) - .untilAsserted(() -> { - // 5. Make sure 1 notification exists in DB - assertNotificationDbRecords(1); - // 6. Notification not opened then badges should be 1 - assertEquals(1, ShadowBadgeCountUpdater.lastCount); - }); - } - - @Test - @Config(shadows = { ShadowGenerateNotification.class }) - public void testNotificationWillShowInForegroundHandler_silentNotificationSaved() throws Exception { - // 1. Init OneSignal - OneSignal.setAppId("b2f7f966-d8cc-11e4-bed1-df8f05be55ba"); - OneSignal.initWithContext(blankActivity); - OneSignal.setNotificationWillShowInForegroundHandler(notificationReceivedEvent -> { - callbackCounter++; - lastForegroundNotificationReceivedEvent = notificationReceivedEvent; - notificationReceivedEvent.complete(null); - }); - threadAndTaskWait(); - - blankActivityController.resume(); - threadAndTaskWait(); - - // 2. Receive a notification - FCMBroadcastReceiver_processBundle(blankActivity, getBaseNotifBundle()); - threadAndTaskWait(); - - // 3. Make sure the AppNotificationJob is not null - assertNotNull(lastForegroundNotificationReceivedEvent); - - // 4. Make sure the callback counter is only fired once for NotificationWillShowInForegroundHandler - assertEquals(1, callbackCounter); - - // 5. Make sure silent notification exists in DB - assertNotificationDbRecords(1, true); - } - - @Test - @Config(shadows = { ShadowGenerateNotification.class }) - public void testNotificationWillShowInForegroundHandler_silentNotificationNotSavedUntilTimerCompleteIsDone() throws Exception { - // 1. Init OneSignal - OneSignal.setAppId("b2f7f966-d8cc-11e4-bed1-df8f05be55ba"); - OneSignal.initWithContext(blankActivity); - OneSignal.setNotificationWillShowInForegroundHandler(notificationReceivedEvent -> { - callbackCounter++; - lastForegroundNotificationReceivedEvent = notificationReceivedEvent; - - new Thread(() -> { - try { - // Simulate long work - Thread.sleep(5000); - // Check notification is still not saved - assertNotificationDbRecords(0); - notificationReceivedEvent.complete(null); - } catch (InterruptedException e) { - e.printStackTrace(); - } - }).start(); - }); - threadAndTaskWait(); - - blankActivityController.resume(); - threadAndTaskWait(); - - // 2. Receive a notification - FCMBroadcastReceiver_processBundle(blankActivity, getBaseNotifBundle()); - threadAndTaskWait(); - - // 3. Make sure the AppNotificationJob is not null - assertNotNull(lastForegroundNotificationReceivedEvent); - - // 4. Make sure the callback counter is only fired once for App NotificationWillShowInForegroundHandler - assertEquals(1, callbackCounter); - - // 5. Make sure 0 notification exists in DB because complete was not called yet - assertNotificationDbRecords(0); - - // Complete being call from User thread - Awaitility.await() - .atMost(new Duration(10, TimeUnit.SECONDS)) - .pollInterval(new Duration(500, TimeUnit.MILLISECONDS)) - .untilAsserted(() -> { - // Make sure 1 notification exists in DB - assertNotificationDbRecords(1, true); - }); - } - - @Test - @Config (shadows = { ShadowGenerateNotification.class }) - public void testNotificationReceived_duplicatesInShortTime() throws Exception { - // 1. Init OneSignal - OneSignal.setAppId(ONESIGNAL_APP_ID); - OneSignal.initWithContext(blankActivity); - OneSignal.setNotificationWillShowInForegroundHandler(notificationReceivedEvent -> { - androidNotificationId = notificationReceivedEvent.getNotification().getAndroidNotificationId(); - notificationReceivedBody = notificationReceivedEvent.getNotification().getBody(); - // Not call complete to test duplicate arriving before notification processing is completed - }); - OneSignal.setNotificationOpenedHandler(getNotificationOpenedHandler()); - - // 2. Foreground the app - blankActivityController.resume(); - threadAndTaskWait(); - - final boolean[] callbackEnded = {false}; - OneSignalPackagePrivateHelper.ProcessBundleReceiverCallback processBundleReceiverCallback = new OneSignalPackagePrivateHelper.ProcessBundleReceiverCallback() { - public void onBundleProcessed(OneSignalPackagePrivateHelper.ProcessedBundleResult processedResult) { - assertNotNull(processedResult); - assertTrue(processedResult.isProcessed()); - callbackEnded[0] = true; - } - }; - - Bundle bundle = getBaseNotifBundle(); - FCMBroadcastReceiver_processBundle(blankActivity, bundle, processBundleReceiverCallback); - Awaitility.await() - .atMost(new Duration(3, TimeUnit.SECONDS)) - .pollInterval(new Duration(100, TimeUnit.MILLISECONDS)) - .untilAsserted(() -> { - assertTrue(callbackEnded[0]); - }); - - assertNull(lastNotificationOpenedBody); - - assertEquals("Robo test message", notificationReceivedBody); - assertNotEquals(0, androidNotificationId); - - // Don't fire for duplicates - lastNotificationOpenedBody = null; - notificationReceivedBody = null; - - FCMBroadcastReceiver_processBundle(blankActivity, bundle); - threadAndTaskWait(); - - assertNull(lastNotificationOpenedBody); - assertNull(notificationReceivedBody); - - lastNotificationOpenedBody = null; - notificationReceivedBody = null; - - // Test that only NotificationReceivedHandler fires - bundle = getBaseNotifBundle("UUID2"); - FCMBroadcastReceiver_processBundle(blankActivity, bundle); - threadAndTaskWait(); - - assertNull(lastNotificationOpenedBody); - assertEquals("Robo test message", notificationReceivedBody); - } - - @Test - @Config(shadows = { ShadowGenerateNotification.class }) - public void testNotificationWillShowInForegroundHandler_notificationJobPayload() throws Exception { - // 1. Init OneSignal - OneSignal.setAppId("b2f7f966-d8cc-11e4-bed1-df8f05be55ba"); - OneSignal.initWithContext(blankActivity); - OneSignal.setNotificationWillShowInForegroundHandler(notificationReceivedEvent -> { - lastForegroundNotificationReceivedEvent = notificationReceivedEvent; - - OSNotification notification = notificationReceivedEvent.getNotification(); - try { - // Make sure all of the accessible getters have the expected values -// assertEquals("UUID1", notification.getApiNotificationId()); - assertEquals("title1", notification.getTitle()); - assertEquals("Notif message 1", notification.getBody()); - JsonAsserts.equals(new JSONObject("{\"myKey1\": \"myValue1\", \"myKey2\": \"myValue2\"}"), notification.getAdditionalData()); - } catch (JSONException e) { - e.printStackTrace(); - } - - // Call complete to end without waiting default 30 second timeout - notificationReceivedEvent.complete(notification); - }); - - blankActivityController.resume(); - threadAndTaskWait(); - - // 2. Receive a notification - // Setup - Display a single notification with a grouped. - Bundle bundle = getBaseNotifBundle("UUID1"); - bundle.putString("title", "title1"); - bundle.putString("alert", "Notif message 1"); - bundle.putString("custom", "{\"i\": \"UUID1\", " + - " \"a\": {" + - " \"myKey1\": \"myValue1\", " + - " \"myKey2\": \"myValue2\"" + - " }" + - " }"); - FCMBroadcastReceiver_processBundle(blankActivity, bundle); - threadAndTaskWait(); - - // 3. Make sure the AppNotificationJob is not null - assertNotNull(lastForegroundNotificationReceivedEvent); - // Not restoring or duplicate notification, so the Android notif id should not be -1 - assertNotEquals(-1, lastForegroundNotificationReceivedEvent.getNotification().getAndroidNotificationId()); - - // 4. Make sure 1 notification exists in DB - assertNotificationDbRecords(1); - } - - @Test - @Config(shadows = { ShadowTimeoutHandler.class }) - public void testNotificationWillShowInForegroundHandler_workTimeLongerThanTimeout() throws Exception { - // 1. Init OneSignal - OneSignal.setAppId("b2f7f966-d8cc-11e4-bed1-df8f05be55ba"); - OneSignal.initWithContext(blankActivity); - OneSignal.setNotificationWillShowInForegroundHandler(notificationReceivedEvent -> { - callbackCounter++; - lastForegroundNotificationReceivedEvent = notificationReceivedEvent; - - // Simulate doing work for 3 seconds - try { - Thread.sleep(3_000L); - notificationReceivedEvent.complete(notificationReceivedEvent.getNotification()); - } catch (InterruptedException e) { - e.printStackTrace(); - } - }); - threadAndTaskWait(); - - blankActivityController.resume(); - threadAndTaskWait(); - - // Mock timeout to 1, given that we are delaying the complete inside NotificationExtensionService and NotificationWillShowInForegroundHandler - // We depend on the timeout complete - ShadowTimeoutHandler.setMockDelayMillis(1); - // 2. Receive a notification - FCMBroadcastReceiver_processBundle(blankActivity, getBaseNotifBundle()); - threadAndTaskWait(); - - // 3. Make sure AppNotificationJob is not null - assertNotNull(lastForegroundNotificationReceivedEvent); - - // 4. Make sure the callback counter is only fired once for App NotificationWillShowInForegroundHandler - assertEquals(1, callbackCounter); - - // 5. Make sure 1 notification exists in DB - assertNotificationDbRecords(1); - } - - /** - * Small icon accent color uses string.xml value in values-night when device in dark mode - */ - @Test - @Config(qualifiers = "night") - public void shouldUseDarkIconAccentColorInDarkMode_hasMetaData() throws Exception { - OneSignal.initWithContext(blankActivity); - // Add the 'com.onesignal.NotificationAccentColor.DEFAULT' as 'FF0000AA' meta-data tag - OneSignalShadowPackageManager.addManifestMetaData("com.onesignal.NotificationAccentColor.DEFAULT", "FF0000AA"); - - BigInteger defaultColor = OneSignal_getAccentColor(new JSONObject()); - assertEquals("FFFF0000", defaultColor.toString(16).toUpperCase()); - } - - /** - * Small icon accent color uses string.xml value in values when device in day (non-dark) mode - */ - @Test - public void shouldUseDayIconAccentColorInDayMode() throws Exception { - OneSignal.initWithContext(blankActivity); - BigInteger defaultColor = OneSignal_getAccentColor(new JSONObject()); - assertEquals("FF00FF00", defaultColor.toString(16).toUpperCase()); - } - - /** - * Small icon accent color uses value from Manifest if there are no resource strings provided - */ - @Test - @Config(shadows = { ShadowResources.class }) - public void shouldUseManifestIconAccentColor() throws Exception { - OneSignal.initWithContext(blankActivity); - - // Add the 'com.onesignal.NotificationAccentColor.DEFAULT' as 'FF0000AA' meta-data tag - OneSignalShadowPackageManager.addManifestMetaData("com.onesignal.NotificationAccentColor.DEFAULT", "FF0000AA"); - - BigInteger defaultColor = OneSignal_getAccentColor(new JSONObject()); - assertEquals("FF0000AA", defaultColor.toString(16).toUpperCase()); - } - - /** - * Small icon accent color uses value of 'bgac' if available - */ - @Test - public void shouldUseBgacAccentColor_hasMetaData() throws Exception { - OneSignal.initWithContext(blankActivity); - // Add the 'com.onesignal.NotificationAccentColor.DEFAULT' as 'FF0000AA' meta-data tag - OneSignalShadowPackageManager.addManifestMetaData("com.onesignal.NotificationAccentColor.DEFAULT", "FF0000AA"); - JSONObject fcmJson = new JSONObject(); - fcmJson.put("bgac", "FF0F0F0F"); - BigInteger defaultColor = OneSignal_getAccentColor(fcmJson); - assertEquals("FF0F0F0F", defaultColor.toString(16).toUpperCase()); - } - - /* Helpers */ - - private static void assertNoNotifications() { - assertEquals(0, ShadowRoboNotificationManager.notifications.size()); - } - - private static Bundle getBundleWithAllOptionsSet() { - Bundle bundle = new Bundle(); - - bundle.putString("title", "Test H"); - bundle.putString("alert", "Test B"); - bundle.putString("vis", "0"); - bundle.putString("bgac", "FF0000FF"); - bundle.putString("from", "703322744261"); - bundle.putString("ledc", "FFFFFF00"); - bundle.putString("bicon", "big_picture"); - bundle.putString("licon", "large_icon"); - bundle.putString("sicon", "small_icon"); - bundle.putString("sound", "test_sound"); - bundle.putString("grp_msg", "You test $[notif_count] MSGs!"); - bundle.putString("collapse_key", "a_key"); // FCM sets this to 'do_not_collapse' when not set. - bundle.putString("bg_img", "{\"img\": \"test_image_url\"," + - "\"tc\": \"FF000000\"," + - "\"bc\": \"FFFFFFFF\"}"); - - bundle.putInt("pri", 10); - bundle.putString("custom", - "{\"a\": {" + - " \"myKey\": \"myValue\"," + - " \"nested\": {\"nKey\": \"nValue\"}," + - " \"actionButtons\": [{\"id\": \"id1\", \"text\": \"button1\", \"icon\": \"ic_menu_share\"}," + - " {\"id\": \"id2\", \"text\": \"button2\", \"icon\": \"ic_menu_send\"}" + - " ]," + - " \"actionId\": \"__DEFAULT__\"" + - " }," + - "\"u\":\"http://google.com\"," + - "\"i\":\"9764eaeb-10ce-45b1-a66d-8f95938aaa51\"" + - "}"); - - return bundle; - } - - private void assertNotificationDbRecords(int expected) { - SQLiteDatabase readableDb = dbHelper.getSQLiteDatabaseWithRetries(); - Cursor cursor = readableDb.query(NotificationTable.TABLE_NAME, null, null, null, null, null, null); - assertEquals(expected, cursor.getCount()); - cursor.close(); - } - - private void assertNotificationDbRecords(int expected, boolean silentNotification) { - SQLiteDatabase readableDb = dbHelper.getSQLiteDatabaseWithRetries(); - Cursor cursor = readableDb.query(NotificationTable.TABLE_NAME, null, null, null, null, null, null); - assertEquals(expected, cursor.getCount()); - cursor.moveToFirst(); - do { - int androidNotificationId = cursor.getInt(cursor.getColumnIndex("android_notification_id")); - int opened = cursor.getInt(cursor.getColumnIndex("opened")); - - if (silentNotification) { - assertEquals(1, opened); - assertEquals(0, androidNotificationId); - } else { - assertEquals(0, opened); - assertNotEquals(0, androidNotificationId); - } - } while (cursor.moveToNext()); - cursor.close(); - } - - private void addButtonsToReceivedPayload(@NonNull Bundle bundle) { - try { - JSONArray buttonList = new JSONArray() {{ - put(new JSONObject() {{ - put(PUSH_MINIFIED_BUTTON_ID, "id1"); - put(PUSH_MINIFIED_BUTTON_TEXT, "text1"); - }}); - }}; - bundle.putString(PUSH_MINIFIED_BUTTONS_LIST, buttonList.toString()); - } catch (JSONException e) { - e.printStackTrace(); - } - } - -} diff --git a/OneSignalSDK/unittest/src/test/java/com/test/onesignal/NotificationOpenedActivityHMSIntegrationTestsRunner.java b/OneSignalSDK/unittest/src/test/java/com/test/onesignal/NotificationOpenedActivityHMSIntegrationTestsRunner.java deleted file mode 100644 index 182a16443e..0000000000 --- a/OneSignalSDK/unittest/src/test/java/com/test/onesignal/NotificationOpenedActivityHMSIntegrationTestsRunner.java +++ /dev/null @@ -1,216 +0,0 @@ -package com.test.onesignal; - -import android.app.Activity; -import android.app.Application; -import android.content.Intent; - -import androidx.annotation.NonNull; -import androidx.test.core.app.ApplicationProvider; - -import com.onesignal.NotificationOpenedActivityHMS; -import com.onesignal.OSNotificationOpenedResult; -import com.onesignal.OneSignal; -import com.onesignal.OneSignalPackagePrivateHelper.UserState; -import com.onesignal.ShadowCustomTabsClient; -import com.onesignal.ShadowCustomTabsSession; -import com.onesignal.ShadowHmsInstanceId; -import com.onesignal.ShadowOSUtils; -import com.onesignal.ShadowOSViewUtils; -import com.onesignal.ShadowOSWebView; -import com.onesignal.ShadowOneSignalRestClient; -import com.onesignal.ShadowPushRegistratorHMS; -import com.onesignal.StaticResetHelper; -import com.onesignal.example.BlankActivity; - -import org.json.JSONException; -import org.json.JSONObject; -import org.junit.After; -import org.junit.AfterClass; -import org.junit.Before; -import org.junit.BeforeClass; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.robolectric.Robolectric; -import org.robolectric.RobolectricTestRunner; -import org.robolectric.android.controller.ActivityController; -import org.robolectric.annotation.Config; -import org.robolectric.annotation.LooperMode; -import org.robolectric.shadows.ShadowLog; - -import java.util.UUID; - -import static com.onesignal.OneSignalPackagePrivateHelper.NotificationBundleProcessor.PUSH_ADDITIONAL_DATA_KEY; -import static com.onesignal.OneSignalPackagePrivateHelper.OSNotificationFormatHelper.PAYLOAD_OS_ROOT_CUSTOM; -import static com.onesignal.OneSignalPackagePrivateHelper.OSNotificationFormatHelper.PAYLOAD_OS_NOTIFICATION_ID; -import static com.onesignal.OneSignalPackagePrivateHelper.GenerateNotification.BUNDLE_KEY_ACTION_ID; -import static com.onesignal.InAppMessagingHelpers.ONESIGNAL_APP_ID; -import static com.onesignal.ShadowOneSignalRestClient.setRemoteParamsGetHtmlResponse; -import static com.test.onesignal.RestClientAsserts.assertNotificationOpenAtIndex; -import static com.test.onesignal.TestHelpers.fastColdRestartApp; -import static com.test.onesignal.TestHelpers.threadAndTaskWait; -import static junit.framework.Assert.assertEquals; -import static junit.framework.Assert.assertNotNull; -import static org.robolectric.Shadows.shadowOf; - -@Config( - packageName = "com.onesignal.example", - shadows = { - ShadowOSUtils.class, - ShadowPushRegistratorHMS.class, - ShadowOneSignalRestClient.class, - ShadowCustomTabsClient.class, - ShadowOSWebView.class, - ShadowOSViewUtils.class, - ShadowCustomTabsClient.class, - ShadowCustomTabsSession.class, - ShadowHmsInstanceId.class, - }, - sdk = 26 -) -@RunWith(RobolectricTestRunner.class) -@LooperMode(LooperMode.Mode.LEGACY) -public class NotificationOpenedActivityHMSIntegrationTestsRunner { - - private static final String TEST_ACTION_ID = "myTestActionId"; - - @BeforeClass // Runs only once, before any tests - public static void setUpClass() throws Exception { - ShadowLog.stream = System.out; - TestHelpers.beforeTestSuite(); - StaticResetHelper.saveStaticValues(); - } - - @Before - public void beforeEachTest() throws Exception { - TestHelpers.beforeTestInitAndCleanup(); - ShadowOSUtils.supportsHMS(true); - // Set remote_params GET response - setRemoteParamsGetHtmlResponse(); - } - - @AfterClass - public static void afterEverything() throws Exception { - TestHelpers.beforeTestInitAndCleanup(); - } - - @After - public void afterEachTest() throws Exception { - TestHelpers.afterTestCleanup(); - } - - private static @NonNull Intent helper_baseHMSOpenIntent() { - return new Intent() - .setFlags(Intent.FLAG_ACTIVITY_NO_HISTORY | Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_MULTIPLE_TASK) - .setAction("android.intent.action.VIEW"); - } - - private static @NonNull Intent helper_basicOSHMSOpenIntent() throws JSONException { - return helper_baseHMSOpenIntent() - .putExtra( - PAYLOAD_OS_ROOT_CUSTOM, - new JSONObject() {{ - put(PAYLOAD_OS_NOTIFICATION_ID, UUID.randomUUID().toString()); - }}.toString() - ); - } - - private static @NonNull Intent helper_basicOSHMSOpenIntentWithActionId(final @NonNull String actionId) throws JSONException { - return helper_baseHMSOpenIntent() - .putExtra( - PAYLOAD_OS_ROOT_CUSTOM, - new JSONObject() {{ - put(PAYLOAD_OS_NOTIFICATION_ID, UUID.randomUUID().toString()); - put(BUNDLE_KEY_ACTION_ID, actionId); - }}.toString() - ); - } - - private static void helper_startHMSOpenActivity(@NonNull Intent intent) { - Robolectric.buildActivity(NotificationOpenedActivityHMS.class, intent).create(); - } - - private static void helper_initSDKAndFireHMSNotificationBarebonesOSOpenIntent() throws Exception { - Intent intent = helper_basicOSHMSOpenIntent(); - helper_initSDKAndFireHMSNotificationOpenWithIntent(intent); - } - - private static void helper_initSDKAndFireHMSNotificationActionButtonTapIntent(@NonNull String actionId) throws Exception { - Intent intent = helper_basicOSHMSOpenIntentWithActionId(actionId); - helper_initSDKAndFireHMSNotificationOpenWithIntent(intent); - } - - private static void helper_initSDKAndFireHMSNotificationOpenWithIntent(@NonNull Intent intent) throws Exception { - OneSignal.setAppId(ONESIGNAL_APP_ID); - OneSignal.initWithContext(ApplicationProvider.getApplicationContext()); - fastColdRestartApp(); - - helper_startHMSOpenActivity(intent); - } - - // Since the Activity has to be public it could be started outside of a OneSignal flow. - // Ensure it doesn't crash the app. - @Test - public void emptyIntent_doesNotThrow() { - helper_startHMSOpenActivity(helper_baseHMSOpenIntent()); - } - - @Test - public void barebonesOSPayload_startsMainActivity() throws Exception { - helper_initSDKAndFireHMSNotificationBarebonesOSOpenIntent(); - - Intent startedActivity = shadowOf((Application) ApplicationProvider.getApplicationContext()).getNextStartedActivity(); - assertNotNull(startedActivity); - assertEquals(startedActivity.getComponent().getClassName(), BlankActivity.class.getName()); - } - - @Test - public void barebonesOSPayload_makesNotificationOpenRequest() throws Exception { - helper_initSDKAndFireHMSNotificationBarebonesOSOpenIntent(); - assertNotificationOpenAtIndex(2, UserState.DEVICE_TYPE_HUAWEI); - } - - private static String lastActionId; - @Test - public void firesOSNotificationOpenCallbackWithActionId() throws Exception { - helper_initSDKAndFireHMSNotificationActionButtonTapIntent(TEST_ACTION_ID); - - OneSignal.setAppId(ONESIGNAL_APP_ID); - OneSignal.initWithContext(ApplicationProvider.getApplicationContext()); - OneSignal.setNotificationOpenedHandler(new OneSignal.OSNotificationOpenedHandler() { - @Override - public void notificationOpened(OSNotificationOpenedResult result) { - lastActionId = result.getAction().getActionId(); - } - }); - - assertEquals(TEST_ACTION_ID, lastActionId); - } - - @Test - public void osIAMPreview_showsPreview() throws Exception { - ActivityController blankActivityController = Robolectric.buildActivity(BlankActivity.class).create(); - Activity blankActivity = blankActivityController.get(); - OneSignal.setAppId(ONESIGNAL_APP_ID); - OneSignal.initWithContext(blankActivity); - threadAndTaskWait(); - - blankActivityController.resume(); - threadAndTaskWait(); - - Intent intent = helper_baseHMSOpenIntent() - .putExtra( - PAYLOAD_OS_ROOT_CUSTOM, - new JSONObject() {{ - put(PAYLOAD_OS_NOTIFICATION_ID, UUID.randomUUID().toString()); - put(PUSH_ADDITIONAL_DATA_KEY, new JSONObject() {{ - put("os_in_app_message_preview_id", "UUID"); - }}); - }}.toString() - ); - - helper_startHMSOpenActivity(intent); - threadAndTaskWait(); - threadAndTaskWait(); - assertEquals("PGh0bWw+PC9odG1sPgoKPHNjcmlwdD4KICAgIHNldFBsYXllclRhZ3MoKTsKPC9zY3JpcHQ+", ShadowOSWebView.lastData); - } -}