diff --git a/README.md b/README.md index c274af797..f41c36557 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ We publish the SDK to `mavenCentral` as an `AAR` file. Just declare it as depend ```groovy dependencies { - implementation "com.clevertap.android:clevertap-android-sdk:4.6.6" + implementation "com.clevertap.android:clevertap-android-sdk:4.7.0" } ``` @@ -34,7 +34,7 @@ Alternatively, you can download and add the AAR file included in this repo in yo ```groovy dependencies { - implementation (name: "clevertap-android-sdk-4.6.6", ext: 'aar') + implementation (name: "clevertap-android-sdk-4.7.0", ext: 'aar') } ``` @@ -46,9 +46,9 @@ Add the Firebase Messaging library and Android Support Library v4 as dependencie ```groovy dependencies { - implementation "com.clevertap.android:clevertap-android-sdk:4.6.6" - implementation "androidx.core:core:1.3.0" - implementation "com.google.firebase:firebase-messaging:21.0.0" + implementation "com.clevertap.android:clevertap-android-sdk:4.7.0" + implementation "androidx.core:core:1.9.0" + implementation "com.google.firebase:firebase-messaging:23.0.6" implementation "com.google.android.gms:play-services-ads:19.4.0" // Required only if you enable Google ADID collection in the SDK (turned off by default). implementation "com.android.installreferrer:installreferrer:2.2" // Mandatory for v3.6.4 and above } @@ -71,7 +71,7 @@ Also be sure to include the `google-services.json` classpath in your Project lev } dependencies { - classpath "com.android.tools.build:gradle:7.2.1" + classpath "com.android.tools.build:gradle:7.3.0" classpath "com.google.gms:google-services:4.3.3" // NOTE: Do not place your application dependencies here; they belong diff --git a/buildSrc/settings.gradle.kts b/buildSrc/settings.gradle.kts index 40e2e0bdb..824ede0a8 100644 --- a/buildSrc/settings.gradle.kts +++ b/buildSrc/settings.gradle.kts @@ -7,6 +7,15 @@ pluginManagement { //// # available:"0.21.0" //// # available:"0.22.0" //// # available:"0.23.0" +//// # available:"0.30.0" +//// # available:"0.30.1" +//// # available:"0.30.2" +//// # available:"0.40.0" +//// # available:"0.40.1" +//// # available:"0.40.2" +//// # available:"0.50.0" +//// # available:"0.50.1" +//// # available:"0.50.2" } } diff --git a/buildSrc/src/main/kotlin/Libs.kt b/buildSrc/src/main/kotlin/Libs.kt index d7e340627..e888bc1c2 100644 --- a/buildSrc/src/main/kotlin/Libs.kt +++ b/buildSrc/src/main/kotlin/Libs.kt @@ -177,10 +177,10 @@ object Libs { object Android { // Android SDK - const val compileSdkVersionVal = 31 - const val targetSdkVersionVal = 31 - const val buildToolsVersionVal = "30.0.3" - const val minSdkVersionVal = 16 + const val compileSdkVersionVal = 33 + const val targetSdkVersionVal = 33 + const val buildToolsVersionVal = "33.0.0" + const val minSdkVersionVal = 19 } object SDKTest { diff --git a/clevertap-core/src/main/AndroidManifest.xml b/clevertap-core/src/main/AndroidManifest.xml index e32b0c7f8..dbd82138a 100644 --- a/clevertap-core/src/main/AndroidManifest.xml +++ b/clevertap-core/src/main/AndroidManifest.xml @@ -2,6 +2,9 @@ + + + ().execute("buildCache") { + firstTimeRequest = StorageHelper.getBoolean( + context, + InAppController.IS_FIRST_TIME_PERMISSION_REQUEST, true + ) + null + } + return CTPreferenceCache() + } + + @JvmStatic + fun updateCacheToDisk(context: Context, config: CleverTapInstanceConfig) { + CTExecutorFactory.executors(config).ioTask().execute("updateCacheToDisk") { + StorageHelper.putBooleanImmediate( + context, InAppController.IS_FIRST_TIME_PERMISSION_REQUEST, + firstTimeRequest + ) + null + } + } + } +} \ No newline at end of file diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/CTStringResources.kt b/clevertap-core/src/main/java/com/clevertap/android/sdk/CTStringResources.kt new file mode 100644 index 000000000..0c28a01eb --- /dev/null +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/CTStringResources.kt @@ -0,0 +1,21 @@ +package com.clevertap.android.sdk + +import android.content.Context +import androidx.annotation.RestrictTo +import androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP + +@RestrictTo(LIBRARY_GROUP) +class CTStringResources(private val context: Context, vararg sRID: Int) { + + private var sArray: Array + + init { + sArray = Array(sRID.size) { context.getString(sRID[it]) } + } + + operator fun component1(): String? = sArray.getOrNull(0) + operator fun component2(): String? = sArray.getOrNull(1) + operator fun component3(): String? = sArray.getOrNull(2) + operator fun component4(): String? = sArray.getOrNull(3) + operator fun component5(): String? = sArray.getOrNull(4) +} \ No newline at end of file diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/CTWebInterface.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/CTWebInterface.java index daee93d65..d3284eeb3 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/CTWebInterface.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/CTWebInterface.java @@ -1,6 +1,8 @@ package com.clevertap.android.sdk; import android.webkit.JavascriptInterface; +import androidx.annotation.RestrictTo; +import com.clevertap.android.sdk.inapp.CTInAppBaseFullFragment; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.HashMap; @@ -15,11 +17,47 @@ public class CTWebInterface { private final WeakReference weakReference; + private CTInAppBaseFullFragment inAppBaseFullFragment; public CTWebInterface(CleverTapAPI instance) { this.weakReference = new WeakReference<>(instance); } + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + public CTWebInterface(CleverTapAPI instance, CTInAppBaseFullFragment inAppBaseFullFragment){ + this.weakReference = new WeakReference<>(instance); + this.inAppBaseFullFragment = inAppBaseFullFragment; + } + + /** + * Method to be called from WebView Javascript to request permission for notification + * for Android 13 and above + */ + @JavascriptInterface + public void promptPushPermission(boolean shouldShowFallbackSettings) { + CleverTapAPI cleverTapAPI = weakReference.get(); + if (cleverTapAPI == null) { + Logger.d("CleverTap Instance is null."); + } else { + //Dismisses current IAM and proceeds to call promptForPushPermission() + dismissInAppNotification(); + cleverTapAPI.promptForPushPermission(shouldShowFallbackSettings); + } + } + /** + * Method to be called from WebView Javascript to dismiss the InApp notification + */ + @JavascriptInterface + public void dismissInAppNotification() { + CleverTapAPI cleverTapAPI = weakReference.get(); + if (cleverTapAPI == null) { + Logger.d("CleverTap Instance is null."); + } else { + //Dismisses current IAM and proceeds to call promptForPushPermission() + inAppBaseFullFragment.didDismiss(null); + } + } + /** * Method to be called from WebView Javascript to add profile properties in CleverTap * diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/CTXtensions.kt b/clevertap-core/src/main/java/com/clevertap/android/sdk/CTXtensions.kt new file mode 100644 index 000000000..3509fada0 --- /dev/null +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/CTXtensions.kt @@ -0,0 +1,12 @@ +@file:JvmName("CTXtensions") + +package com.clevertap.android.sdk + +import android.content.Context +import android.os.Build.VERSION + +fun Context.isPackageAndOsTargetsAbove(apiLevel: Int) = + VERSION.SDK_INT > apiLevel && targetSdkVersion > apiLevel + +val Context.targetSdkVersion + get() = applicationContext.applicationInfo.targetSdkVersion \ No newline at end of file diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/CallbackManager.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/CallbackManager.java index 31615791a..d67619a04 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/CallbackManager.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/CallbackManager.java @@ -28,6 +28,8 @@ public class CallbackManager extends BaseCallbackManager { private InAppNotificationListener inAppNotificationListener; + private PushPermissionResponseListener pushPermissionResponseListener; + private CTInboxListener inboxListener; private final CleverTapInstanceConfig config; @@ -131,11 +133,21 @@ public InAppNotificationListener getInAppNotificationListener() { return inAppNotificationListener; } + @Override + public PushPermissionResponseListener getPushPermissionResponseListener() { + return pushPermissionResponseListener; + } + @Override public void setInAppNotificationListener(final InAppNotificationListener inAppNotificationListener) { this.inAppNotificationListener = inAppNotificationListener; } + @Override + public void setPushPermissionResponseListener(PushPermissionResponseListener pushPermissionResponseListener) { + this.pushPermissionResponseListener = pushPermissionResponseListener; + } + @Override public CTInboxListener getInboxListener() { return inboxListener; diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/CleverTapAPI.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/CleverTapAPI.java index 800f3243c..95b409ccc 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/CleverTapAPI.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/CleverTapAPI.java @@ -1,11 +1,13 @@ package com.clevertap.android.sdk; import static android.content.Context.NOTIFICATION_SERVICE; +import static com.clevertap.android.sdk.CTXtensions.isPackageAndOsTargetsAbove; import static com.clevertap.android.sdk.Utils.getSCDomain; import static com.clevertap.android.sdk.pushnotification.PushConstants.FCM_LOG_TAG; import static com.clevertap.android.sdk.pushnotification.PushConstants.LOG_TAG; import static com.clevertap.android.sdk.pushnotification.PushConstants.PushType.FCM; +import android.annotation.SuppressLint; import android.app.Activity; import android.app.NotificationChannel; import android.app.NotificationChannelGroup; @@ -20,7 +22,6 @@ import android.os.Build; import android.os.Bundle; import android.text.TextUtils; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; @@ -32,13 +33,14 @@ import com.clevertap.android.sdk.events.EventDetail; import com.clevertap.android.sdk.events.EventGroup; import com.clevertap.android.sdk.featureFlags.CTFeatureFlagsController; +import com.clevertap.android.sdk.inapp.CTLocalInApp; import com.clevertap.android.sdk.inbox.CTInboxActivity; import com.clevertap.android.sdk.inbox.CTInboxMessage; import com.clevertap.android.sdk.inbox.CTMessageDAO; -import com.clevertap.android.sdk.interfaces.SCDomainListener; import com.clevertap.android.sdk.interfaces.NotificationHandler; import com.clevertap.android.sdk.interfaces.NotificationRenderedListener; import com.clevertap.android.sdk.interfaces.OnInitCleverTapIDListener; +import com.clevertap.android.sdk.interfaces.SCDomainListener; import com.clevertap.android.sdk.network.NetworkManager; import com.clevertap.android.sdk.product_config.CTProductConfigController; import com.clevertap.android.sdk.product_config.CTProductConfigListener; @@ -1032,6 +1034,46 @@ public static void tokenRefresh(Context context, String token, PushType pushType } } + /** + * Checks whether notification permission is granted or denied for Android 13 and above devices. + * @return boolean Returns true/false based on whether permission is granted or denied. + */ + @SuppressLint("NewApi") + public boolean isPushPermissionGranted(){ + if (isPackageAndOsTargetsAbove(context, 32)) { + return coreState.getInAppController().isPushPermissionGranted(); + } else { + return false; + } + } + + /** + * Calls the push primer flow for Android 13 and above devices. + * @param jsonObject JSONObject - Accepts jsonObject created by {@link CTLocalInApp} object + */ + @SuppressLint("NewApi") + public void promptPushPrimer(JSONObject jsonObject) { + if (isPackageAndOsTargetsAbove(context, 32)) { + coreState.getInAppController().promptPushPrimer(jsonObject); + } else { + Logger.v("Ensure your app supports Android 13 to verify permission access for notifications."); + } + } + + /** + * Calls directly hard permission dialog, if push primer is not required. + * @param showFallbackSettings - boolean - If `showFallbackSettings` is true then we show a alert + * dialog which routes to app's notification settings page. + */ + @SuppressLint("NewApi") + public void promptForPushPermission(boolean showFallbackSettings){ + if (isPackageAndOsTargetsAbove(context, 32)) { + coreState.getInAppController().promptPermission(showFallbackSettings); + } else { + Logger.v("Ensure your app supports Android 13 to verify permission access for notifications."); + } + } + // Initialize private CleverTapAPI(final Context context, final CleverTapInstanceConfig config, String cleverTapID) { this.context = context; @@ -1574,8 +1616,6 @@ public Map getHistory() { return coreState.getLocalDataStore().getEventHistory(context); } - //DeepLink - /** * Returns the InAppNotificationListener object * @@ -1596,6 +1636,28 @@ public void setInAppNotificationListener(InAppNotificationListener inAppNotifica coreState.getCallbackManager().setInAppNotificationListener(inAppNotificationListener); } + /** + * Returns the PushPermissionNotificationResponseListener object + * + * @return An {@link PushPermissionResponseListener} object + */ + @SuppressWarnings({"unused", "WeakerAccess"}) + public PushPermissionResponseListener getPushPermissionNotificationResponseListener() { + return coreState.getCallbackManager().getPushPermissionResponseListener(); + } + + /** + * This method sets the PushPermissionNotificationResponseListener + * + * @param pushPermissionResponseListener An {@link PushPermissionResponseListener} object + */ + @SuppressWarnings({"unused"}) + public void setPushPermissionNotificationResponseListener(PushPermissionResponseListener + pushPermissionResponseListener) { + coreState.getCallbackManager(). + setPushPermissionResponseListener(pushPermissionResponseListener); + } + /** * Returns the count of all inbox messages for the user * diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/CleverTapFactory.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/CleverTapFactory.java index 10f01193d..9aa159d69 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/CleverTapFactory.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/CleverTapFactory.java @@ -14,7 +14,6 @@ import com.clevertap.android.sdk.task.Task; import com.clevertap.android.sdk.validation.ValidationResultStack; import com.clevertap.android.sdk.validation.Validator; - import java.util.concurrent.Callable; class CleverTapFactory { @@ -49,6 +48,8 @@ static CoreState getCoreState(Context context, CleverTapInstanceConfig cleverTap DeviceInfo deviceInfo = new DeviceInfo(context, config, cleverTapID, coreMetaData); coreState.setDeviceInfo(deviceInfo); + CTPreferenceCache.getInstance(context,config); + BaseCallbackManager callbackManager = new CallbackManager(config, deviceInfo); coreState.setCallbackManager(callbackManager); @@ -99,7 +100,7 @@ public Void call() throws Exception { coreState.setAnalyticsManager(analyticsManager); InAppController inAppController = new InAppController(context, config, mainLooperHandler, - controllerManager, callbackManager, analyticsManager, coreMetaData); + controllerManager, callbackManager, analyticsManager, coreMetaData, deviceInfo); coreState.setInAppController(inAppController); coreState.getControllerManager().setInAppController(inAppController); diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/Constants.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/Constants.java index 522f5dc1e..fd7e489c8 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/Constants.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/Constants.java @@ -172,6 +172,9 @@ public interface Constants { String KEY_TDC = "tdc"; String KEY_KV = "kv"; String KEY_TYPE = "type"; + String KEY_FALLBACK_NOTIFICATION_SETTINGS = "fbSettings"; + String KEY_REQUEST_FOR_NOTIFICATION_PERMISSION = "rfp"; + int NOTIFICATION_PERMISSION_REQUEST_CODE = 102; String KEY_IS_TABLET = "tablet"; String KEY_BG = "bg"; String KEY_TITLE = "title"; @@ -216,6 +219,8 @@ public interface Constants { String BLACK = "#000000"; String WHITE = "#FFFFFF"; String BLUE = "#0000FF"; + String GREEN = "#00FF00"; + String LIGHT_BLUE = "#818ce5"; /** * Profile command constants. */ diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/DeviceInfo.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/DeviceInfo.java index 3bd70d8cd..282cf5992 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/DeviceInfo.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/DeviceInfo.java @@ -1,5 +1,8 @@ package com.clevertap.android.sdk; +import static android.content.Context.USAGE_STATS_SERVICE; +import static com.clevertap.android.sdk.inapp.InAppController.LOCAL_INAPP_COUNT; + import android.Manifest; import android.annotation.SuppressLint; import android.app.UiModeManager; @@ -23,6 +26,7 @@ import androidx.annotation.RequiresApi; import androidx.annotation.RestrictTo; import androidx.annotation.RestrictTo.Scope; +import androidx.annotation.WorkerThread; import androidx.core.app.NotificationManagerCompat; import com.clevertap.android.sdk.login.LoginInfoProvider; import com.clevertap.android.sdk.task.CTExecutorFactory; @@ -39,8 +43,6 @@ import java.util.concurrent.Callable; import org.json.JSONObject; -import static android.content.Context.USAGE_STATS_SERVICE; - @RestrictTo(Scope.LIBRARY) public class DeviceInfo { @@ -88,6 +90,8 @@ private class DeviceCachedInfo { private String appBucket; + private int localInAppCount; + DeviceCachedInfo() { versionName = getVersionName(); osName = getOsName(); @@ -106,6 +110,7 @@ private class DeviceCachedInfo { widthPixels = getWidthPixels(); dpi = getDPI(); notificationsEnabled = getNotificationEnabledForUser(); + localInAppCount = getLocalInAppCountFromPreference(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { appBucket = getAppBucket(); } @@ -391,12 +396,12 @@ private double toTwoPlaces(double n) { /** * Device is a smart phone */ - static final int SMART_PHONE = 1; + public static final int SMART_PHONE = 1; /** * Device is a tablet */ - static final int TABLET = 2; + public static final int TABLET = 2; /** * Device is a television @@ -625,6 +630,14 @@ public int getSdkVersion() { return getDeviceCachedInfo().sdkVersion; } + public int getLocalInAppCount() { + return getDeviceCachedInfo().localInAppCount; + } + + public void incrementLocalInAppCount() { + getDeviceCachedInfo().localInAppCount++; + } + public ArrayList getValidationResults() { // noinspection unchecked ArrayList tempValidationResults = (ArrayList) validationResults.clone(); @@ -721,6 +734,11 @@ int getWidthPixels() { return getDeviceCachedInfo().widthPixels; } + @WorkerThread + private int getLocalInAppCountFromPreference() { + return StorageHelper.getInt(context, LOCAL_INAPP_COUNT, 0); + } + void onInitDeviceInfo(final String cleverTapID) { Task taskDeviceCachedInfo = CTExecutorFactory.executors(config).ioTask(); taskDeviceCachedInfo.execute("getDeviceCachedInfo", new Callable() { diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/DidClickForHardPermissionListener.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/DidClickForHardPermissionListener.java new file mode 100644 index 000000000..374075925 --- /dev/null +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/DidClickForHardPermissionListener.java @@ -0,0 +1,9 @@ +package com.clevertap.android.sdk; + +/** + * Internal interface for communication between fragment and its respective activity when action buttons + * are clicked via InApp/Inbox payload. + */ +public interface DidClickForHardPermissionListener { + void didClickForHardPermissionWithFallbackSettings(boolean fallbackToSettings); +} \ No newline at end of file diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/InAppNotificationActivity.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/InAppNotificationActivity.java index e8faae6d7..1f2d6b11e 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/InAppNotificationActivity.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/InAppNotificationActivity.java @@ -1,13 +1,22 @@ package com.clevertap.android.sdk; +import static com.clevertap.android.sdk.Constants.NOTIFICATION_PERMISSION_REQUEST_CODE; +import static com.clevertap.android.sdk.inapp.InAppController.DISPLAY_HARD_PERMISSION_BUNDLE_KEY; +import static com.clevertap.android.sdk.inapp.InAppController.SHOW_FALLBACK_SETTINGS_BUNDLE_KEY; + +import android.Manifest; +import android.annotation.SuppressLint; import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; +import android.content.pm.PackageManager; import android.content.res.Configuration; import android.net.Uri; import android.os.Bundle; import android.view.WindowManager; +import androidx.annotation.NonNull; +import androidx.core.content.ContextCompat; import androidx.fragment.app.FragmentActivity; import com.clevertap.android.sdk.inapp.CTInAppBaseFullFragment; import com.clevertap.android.sdk.inapp.CTInAppHtmlCoverFragment; @@ -25,7 +34,8 @@ import java.lang.ref.WeakReference; import java.util.HashMap; -public final class InAppNotificationActivity extends FragmentActivity implements InAppListener { +public final class InAppNotificationActivity extends FragmentActivity implements InAppListener, + DidClickForHardPermissionListener { private static boolean isAlertVisible = false; @@ -35,6 +45,17 @@ public final class InAppNotificationActivity extends FragmentActivity implements private WeakReference listenerWeakReference; + private WeakReference pushPermissionResultCallbackWeakReference; + + private PushPermissionManager pushPermissionManager; + + public interface PushPermissionResultCallback { + + void onPushPermissionAccept(); + + void onPushPermissionDeny(); + } + public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); int orientation = this.getResources().getConfiguration().orientation; @@ -46,12 +67,26 @@ public void onCreate(Bundle savedInstanceState) { if (notif == null) { throw new IllegalArgumentException(); } - inAppNotification = notif.getParcelable("inApp"); + inAppNotification = notif.getParcelable(Constants.INAPP_KEY); + boolean showHardNotificationPermission = notif.getBoolean(DISPLAY_HARD_PERMISSION_BUNDLE_KEY, + false); // Using this boolean for a directly showing hard permission dialog flow Bundle configBundle = notif.getBundle("configBundle"); if (configBundle != null) { config = configBundle.getParcelable("config"); } + setListener(CleverTapAPI.instanceWithConfig(this, config).getCoreState().getInAppController()); + setPermissionCallback(CleverTapAPI.instanceWithConfig(this, config).getCoreState() + .getInAppController()); + + pushPermissionManager = new PushPermissionManager(this, config); + + if (showHardNotificationPermission) { + boolean shouldShowFallbackSettings = notif.getBoolean(SHOW_FALLBACK_SETTINGS_BUNDLE_KEY, + false); + showHardPermissionPrompt(shouldShowFallbackSettings); + return; + } } catch (Throwable t) { Logger.v("Cannot find a valid notification bundle to show!", t); finish(); @@ -103,6 +138,23 @@ public void onCreate(Bundle savedInstanceState) { } } + @Override + protected void onResume() { + super.onResume(); + if (pushPermissionManager.isFromNotificationSettingsActivity()){ + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU) { + int permissionStatus = ContextCompat.checkSelfPermission(this, + Manifest.permission.POST_NOTIFICATIONS); + if (permissionStatus == PackageManager.PERMISSION_GRANTED){ + pushPermissionResultCallbackWeakReference.get().onPushPermissionAccept(); + } else { + pushPermissionResultCallbackWeakReference.get().onPushPermissionDeny(); + } + didDismiss(null); + } + } + } + @Override public void finish() { super.finish(); @@ -146,13 +198,43 @@ void didClick(Bundle data, HashMap keyValueMap) { } } + @Override + public void didClickForHardPermissionWithFallbackSettings(boolean fallbackToSettings) { + showHardPermissionPrompt(fallbackToSettings); + } + + @SuppressLint("NewApi") + public void showHardPermissionPrompt(boolean isFallbackSettingsEnabled){ + pushPermissionManager.showHardPermissionPrompt(isFallbackSettingsEnabled, + pushPermissionResultCallbackWeakReference.get()); + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, + @NonNull int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + CTPreferenceCache.getInstance(this, config).setFirstTimeRequest(false); + CTPreferenceCache.updateCacheToDisk(this, config); + + if (requestCode == NOTIFICATION_PERMISSION_REQUEST_CODE) { + boolean granted = grantResults.length > 0 && grantResults[0] == + PackageManager.PERMISSION_GRANTED; + if (granted) { + pushPermissionResultCallbackWeakReference.get().onPushPermissionAccept(); + } else { + pushPermissionResultCallbackWeakReference.get().onPushPermissionDeny(); + } + didDismiss(null); + } + } + void didDismiss(Bundle data) { if (isAlertVisible) { isAlertVisible = false; } finish(); InAppListener listener = getListener(); - if (listener != null && getBaseContext() != null) { + if (listener != null && getBaseContext() != null && inAppNotification != null) { listener.inAppNotificationDidDismiss(getBaseContext(), inAppNotification, data); } } @@ -192,6 +274,10 @@ void setListener(InAppListener listener) { listenerWeakReference = new WeakReference<>(listener); } + public void setPermissionCallback(PushPermissionResultCallback callback) { + pushPermissionResultCallbackWeakReference = new WeakReference<>(callback); + } + private CTInAppBaseFullFragment createContentFragment() { CTInAppType type = inAppNotification.getInAppType(); CTInAppBaseFullFragment viewFragment = null; @@ -257,6 +343,19 @@ public void onClick(DialogInterface dialogInterface, int i) { fireUrlThroughIntent(actionUrl, data); return; } + if (inAppNotification.isLocalInApp()) { + showHardPermissionPrompt(inAppNotification.fallBackToNotificationSettings()); + return; + } + + if (inAppNotification.getButtons().get(0).getType() != null && + inAppNotification.getButtons().get(0).getType() + .equalsIgnoreCase(Constants.KEY_REQUEST_FOR_NOTIFICATION_PERMISSION)){ + showHardPermissionPrompt(inAppNotification. + getButtons().get(0).isFallbackToSettings()); + return; + } + didDismiss(data); } }) @@ -278,6 +377,15 @@ public void onClick(DialogInterface dialog, int which) { fireUrlThroughIntent(actionUrl, data); return; } + + if (inAppNotification.getButtons().get(1).getType() != null && + inAppNotification.getButtons().get(1).getType() + .equalsIgnoreCase(Constants.KEY_REQUEST_FOR_NOTIFICATION_PERMISSION)){ + showHardPermissionPrompt(inAppNotification. + getButtons().get(1).isFallbackToSettings()); + return; + } + didDismiss(data); } }); @@ -304,6 +412,7 @@ public void onClick(DialogInterface dialogInterface, int i) { return; } didDismiss(data); + } }).create(); if (inAppNotification.getButtons().size() == 2) { @@ -350,12 +459,13 @@ public void onClick(DialogInterface dialogInterface, int i) { }); } } - if(alertDialog != null){ + if (alertDialog != null) { alertDialog.show(); isAlertVisible = true; didShow(null); - }else{ - config.getLogger().debug("InAppNotificationActivity: Alert Dialog is null, not showing Alert InApp"); + } else { + config.getLogger() + .debug("InAppNotificationActivity: Alert Dialog is null, not showing Alert InApp"); } break; } diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/InAppNotificationListener.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/InAppNotificationListener.java index fffc08d47..d912e8545 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/InAppNotificationListener.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/InAppNotificationListener.java @@ -1,6 +1,9 @@ package com.clevertap.android.sdk; import androidx.annotation.Nullable; + +import com.clevertap.android.sdk.inapp.CTInAppNotification; + import java.util.Map; /** @@ -19,6 +22,14 @@ public interface InAppNotificationListener { */ boolean beforeShow(Map extras); + /** + * This is called when an in-app notification is rendered. + * + * @param ctInAppNotification The CTInAppNotification object for this notification. + * {@link CTInAppNotification} object + */ + void onShow(CTInAppNotification ctInAppNotification); + /** * When an in-app notification is dismissed (either by the close button, or a call to action), * this method will be called. diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/PushPermissionManager.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/PushPermissionManager.java new file mode 100644 index 000000000..c04063379 --- /dev/null +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/PushPermissionManager.java @@ -0,0 +1,99 @@ +package com.clevertap.android.sdk; + +import static com.clevertap.android.sdk.CTXtensions.isPackageAndOsTargetsAbove; +import static com.clevertap.android.sdk.Constants.NOTIFICATION_PERMISSION_REQUEST_CODE; + +import android.Manifest; +import android.annotation.SuppressLint; +import android.app.Activity; +import android.content.pm.PackageManager; +import androidx.annotation.RequiresApi; +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; +import com.clevertap.android.sdk.inapp.AlertDialogPromptForSettings; +import java.util.Objects; +import kotlin.Unit; + +/** + * This class abstracts notification permission request flow for Android 13+ devices. To call Android OS notification + * permission flow from activity, one should call `showHardPermissionPrompt()` which will request for + * notification permission and gives back result of the permission to the caller activity. * + * + * */ +public class PushPermissionManager { + + private final CleverTapInstanceConfig config; + + private boolean isFallbackSettingsEnabled; + + public static final String ANDROID_PERMISSION_STRING = "android.permission.POST_NOTIFICATIONS"; + + private final Activity activity; + + private boolean isFromNotificationSettingsActivity; + + public PushPermissionManager(Activity activity, CleverTapInstanceConfig config) { + this.activity = activity; + this.config = config; + this.isFromNotificationSettingsActivity = false; + } + + public boolean isFromNotificationSettingsActivity(){ + return isFromNotificationSettingsActivity; + } + + @SuppressLint("NewApi") + public void showHardPermissionPrompt(boolean isFallbackSettingsEnabled, + InAppNotificationActivity.PushPermissionResultCallback + pushPermissionResultCallback){ + if (isPackageAndOsTargetsAbove(activity, 32)) { + this.isFallbackSettingsEnabled = isFallbackSettingsEnabled; + requestPermission(pushPermissionResultCallback); + } + } + + @RequiresApi(api = 33) + public void requestPermission(InAppNotificationActivity.PushPermissionResultCallback pushPermissionResultCallback) { + int permissionStatus = ContextCompat.checkSelfPermission(activity, + Manifest.permission.POST_NOTIFICATIONS); + + if (permissionStatus == PackageManager.PERMISSION_DENIED){ + boolean isFirstTimeRequest = CTPreferenceCache.getInstance(activity, config).isFirstTimeRequest(); + boolean shouldShowRequestPermissionRationale = ActivityCompat.shouldShowRequestPermissionRationale( + Objects.requireNonNull(CoreMetaData.getCurrentActivity()), + ANDROID_PERMISSION_STRING); + + if (!isFirstTimeRequest && shouldShowRequestPermissionRationale){ + if (shouldShowFallbackAlertDialog()){ + showFallbackAlertDialog(); + return; + } + } + + ActivityCompat.requestPermissions(activity, + new String[]{ANDROID_PERMISSION_STRING}, NOTIFICATION_PERMISSION_REQUEST_CODE); + }else{ + pushPermissionResultCallback.onPushPermissionAccept(); + if (activity instanceof InAppNotificationActivity) { + ((InAppNotificationActivity) activity).didDismiss(null); + } + } + } + + private boolean shouldShowFallbackAlertDialog() { + return isFallbackSettingsEnabled; + } + + public void showFallbackAlertDialog() { + AlertDialogPromptForSettings.show(activity, () -> { + Utils.navigateToAndroidSettingsForNotifications(activity); + isFromNotificationSettingsActivity = true; + return Unit.INSTANCE; + }, () -> { + if (activity instanceof InAppNotificationActivity) { + ((InAppNotificationActivity) activity).didDismiss(null); + } + return Unit.INSTANCE; + }); + } +} diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/PushPermissionResponseListener.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/PushPermissionResponseListener.java new file mode 100644 index 000000000..b9d60f99b --- /dev/null +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/PushPermissionResponseListener.java @@ -0,0 +1,15 @@ +package com.clevertap.android.sdk; + +/** + * A listener for notification permission. + */ +public interface PushPermissionResponseListener { + + /** + * This is called when user either grants allow/dismiss permission for notifications for Android 13+ + * + * @param accepted This boolean will return true if notification permission is granted and will retrun + * false if permission is denied. + */ + void onPushPermissionResponse(boolean accepted); +} diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/StorageHelper.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/StorageHelper.java index fbf32c901..053171bed 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/StorageHelper.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/StorageHelper.java @@ -107,7 +107,7 @@ public static String storageKeyWithSuffix(@NonNull CleverTapInstanceConfig confi } @SuppressWarnings("SameParameterValue") - static boolean getBoolean(Context context, String key, boolean defaultValue) { + public static boolean getBoolean(Context context, String key, boolean defaultValue) { return getPreferences(context).getBoolean(key, defaultValue); } @@ -162,18 +162,30 @@ static String getString(Context context, String nameSpace, String key, String de return getPreferences(context, nameSpace).getString(key, defaultValue); } - static void putBoolean(Context context, String key, boolean value) { + public static void putBoolean(Context context, String key, boolean value) { SharedPreferences prefs = getPreferences(context); SharedPreferences.Editor editor = prefs.edit().putBoolean(key, value); persist(editor); } + public static void putBooleanImmediate(Context context, String key, boolean value) { + SharedPreferences prefs = getPreferences(context); + SharedPreferences.Editor editor = prefs.edit().putBoolean(key, value); + persistImmediately(editor); + } + public static void putInt(Context context, String key, int value) { SharedPreferences prefs = getPreferences(context); SharedPreferences.Editor editor = prefs.edit().putInt(key, value); persist(editor); } + public static void putIntImmediate(Context context, String key, int value) { + SharedPreferences prefs = getPreferences(context); + SharedPreferences.Editor editor = prefs.edit().putInt(key, value); + persistImmediately(editor); + } + static void putLong(Context context, String key, long value) { SharedPreferences prefs = getPreferences(context); SharedPreferences.Editor editor = prefs.edit().putLong(key, value); diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/Utils.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/Utils.java index ee9f656e5..97723de8a 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/Utils.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/Utils.java @@ -5,8 +5,6 @@ import android.Manifest; import android.annotation.SuppressLint; import android.app.Activity; -import android.app.ActivityManager; -import android.app.ActivityManager.RunningAppProcessInfo; import android.content.Context; import android.content.Intent; import android.content.pm.PackageInfo; @@ -20,12 +18,12 @@ import android.graphics.drawable.Drawable; import android.net.ConnectivityManager; import android.net.NetworkInfo; +import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.Looper; -import android.os.Process; -import android.os.SystemClock; +import android.provider.Settings; import android.telephony.TelephonyManager; import android.text.TextUtils; import androidx.annotation.NonNull; @@ -33,7 +31,6 @@ import androidx.core.content.ContextCompat; import com.clevertap.android.sdk.task.CTExecutorFactory; import com.clevertap.android.sdk.task.Task; -import com.google.android.gms.common.util.PlatformVersion; import com.google.firebase.messaging.RemoteMessage; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; @@ -663,6 +660,24 @@ public static boolean isRenderFallback(RemoteMessage remoteMessage, Context cont } + public static void navigateToAndroidSettingsForNotifications(Context context){ + Intent intent = new Intent(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + intent.setAction(Settings.ACTION_APP_NOTIFICATION_SETTINGS); + intent.putExtra(Settings.EXTRA_APP_PACKAGE, context.getPackageName()); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP){ + intent.setAction("android.settings.APP_NOTIFICATION_SETTINGS"); + intent.putExtra("app_package", context.getPackageName()); + intent.putExtra("app_uid", context.getApplicationInfo().uid); + } else { + intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); + intent.addCategory(Intent.CATEGORY_DEFAULT); + intent.setData(Uri.parse("package:" + context.getPackageName())); + } + context.startActivity(intent); + } + static { haveVideoPlayerSupport = checkForExoPlayer(); } diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/AlertDialogPromptForSettings.kt b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/AlertDialogPromptForSettings.kt new file mode 100644 index 000000000..8ca471a1b --- /dev/null +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/AlertDialogPromptForSettings.kt @@ -0,0 +1,46 @@ +package com.clevertap.android.sdk.inapp + +import android.app.Activity +import android.app.AlertDialog +import android.os.Build.VERSION.SDK_INT +import android.os.Build.VERSION_CODES.LOLLIPOP +import com.clevertap.android.sdk.CTStringResources +import com.clevertap.android.sdk.R + +/** + * This class shows an Alert dialog to display a rationale message if notification permission is + * already denied. + */ +class AlertDialogPromptForSettings private constructor() { + + companion object { + + @JvmStatic + fun show( + activity: Activity, onAccept: () -> Unit, onDecline: () -> Unit + ) { + val (title, message, positiveButtonText, negativeButtonText) = CTStringResources( + activity.applicationContext, + R.string.ct_permission_not_available_title, + R.string.ct_permission_not_available_message, + R.string.ct_permission_not_available_open_settings_option, + R.string.ct_txt_cancel + ) + + val builder = if (SDK_INT >= LOLLIPOP) AlertDialog.Builder( + activity, + android.R.style.Theme_Material_Light_Dialog_Alert + ) else AlertDialog.Builder(activity) + + builder.setTitle(title) + .setMessage(message) + .setPositiveButton(positiveButtonText) { dialog, which -> + onAccept() + } + .setNegativeButton(negativeButtonText) { dialog, which -> + onDecline() + } + .show() + } + } +} \ No newline at end of file diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppBaseFragment.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppBaseFragment.java index 519dda36d..5a124bf2f 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppBaseFragment.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppBaseFragment.java @@ -10,6 +10,7 @@ import androidx.fragment.app.Fragment; import com.clevertap.android.sdk.CleverTapInstanceConfig; import com.clevertap.android.sdk.Constants; +import com.clevertap.android.sdk.DidClickForHardPermissionListener; import com.clevertap.android.sdk.Utils; import com.clevertap.android.sdk.customviews.CloseImageView; import java.lang.ref.WeakReference; @@ -41,15 +42,20 @@ public void onClick(View view) { private WeakReference listenerWeakReference; + private DidClickForHardPermissionListener didClickForHardPermissionListener; + @Override public void onAttach(Context context) { super.onAttach(context); this.context = context; Bundle bundle = getArguments(); - inAppNotification = bundle.getParcelable(Constants.INAPP_KEY); - config = bundle.getParcelable(Constants.KEY_CONFIG); - currentOrientation = getResources().getConfiguration().orientation; - generateListener(); + if (bundle != null) { + inAppNotification = bundle.getParcelable(Constants.INAPP_KEY); + config = bundle.getParcelable(Constants.KEY_CONFIG); + currentOrientation = getResources().getConfiguration().orientation; + generateListener(); + didClickForHardPermissionListener = (DidClickForHardPermissionListener) getActivity(); + } } @Override @@ -67,7 +73,7 @@ void didClick(Bundle data, HashMap keyValueMap) { } } - void didDismiss(Bundle data) { + public void didDismiss(Bundle data) { cleanup(); InAppListener listener = getListener(); if (listener != null && getActivity() != null && getActivity().getBaseContext() != null) { @@ -140,12 +146,28 @@ void handleButtonClickAtIndex(int index) { didClick(data, button.getKeyValues()); + if (index == 0 && inAppNotification.isLocalInApp()) { + didClickForHardPermissionListener.didClickForHardPermissionWithFallbackSettings( + inAppNotification.fallBackToNotificationSettings()); + return; + }else if (index == 1 && inAppNotification.isLocalInApp()){ + didDismiss(data); + return; + } + + if (button.getType() != null && button.getType().contains( + Constants.KEY_REQUEST_FOR_NOTIFICATION_PERMISSION)){ + didClickForHardPermissionListener. + didClickForHardPermissionWithFallbackSettings(button.isFallbackToSettings()); + return; + } String actionUrl = button.getActionUrl(); if (actionUrl != null) { fireUrlThroughIntent(actionUrl, data); return; } didDismiss(data); + } catch (Throwable t) { config.getLogger().debug("Error handling notification button click: " + t.getCause()); didDismiss(null); diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppBaseFullHtmlFragment.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppBaseFullHtmlFragment.java index 565774384..17952ff9b 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppBaseFullHtmlFragment.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppBaseFullHtmlFragment.java @@ -131,7 +131,8 @@ private View displayHTMLView(LayoutInflater inflater, ViewGroup container) { webView.getSettings().setAllowFileAccessFromFileURLs(false); } webView.addJavascriptInterface( - new CTWebInterface(CleverTapAPI.instanceWithConfig(getActivity(), config)), "CleverTap"); + new CTWebInterface(CleverTapAPI.instanceWithConfig(getActivity(), config), + this), "CleverTap"); } if (isDarkenEnabled()) { diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppNativeHalfInterstitialFragment.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppNativeHalfInterstitialFragment.java index 81b341ddd..6483ee4ab 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppNativeHalfInterstitialFragment.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppNativeHalfInterstitialFragment.java @@ -1,6 +1,7 @@ package com.clevertap.android.sdk.inapp; import android.annotation.SuppressLint; +import android.content.Context; import android.content.res.Configuration; import android.graphics.Color; import android.graphics.drawable.ColorDrawable; @@ -19,6 +20,8 @@ import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; + +import com.clevertap.android.sdk.DeviceInfo; import com.clevertap.android.sdk.R; import com.clevertap.android.sdk.customviews.CloseImageView; import java.util.ArrayList; @@ -34,7 +37,8 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c ArrayList