diff --git a/clevertap-core/src/main/AndroidManifest.xml b/clevertap-core/src/main/AndroidManifest.xml index 61d41f7ed..3eb8033b1 100644 --- a/clevertap-core/src/main/AndroidManifest.xml +++ b/clevertap-core/src/main/AndroidManifest.xml @@ -2,6 +2,8 @@ + + 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 4d14d7676..cd89dc86a 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 @@ -31,6 +31,8 @@ import androidx.annotation.RestrictTo; import androidx.annotation.RestrictTo.Scope; import androidx.annotation.WorkerThread; +import androidx.core.app.NotificationCompat; + import com.clevertap.android.sdk.cryption.CryptHandler; import com.clevertap.android.sdk.displayunits.DisplayUnitListener; import com.clevertap.android.sdk.displayunits.model.CleverTapDisplayUnit; @@ -3241,6 +3243,28 @@ public void renderPushNotificationOnCallerThread(@NonNull INotificationRenderer } + @RestrictTo(Scope.LIBRARY_GROUP) + public NotificationCompat.Builder getPushNotificationOnCallerThread(@NonNull INotificationRenderer iNotificationRenderer, Context context, + Bundle extras) { + CleverTapInstanceConfig config = coreState.getConfig(); + try { + synchronized (coreState.getPushProviders().getPushRenderingLock()) { + config.getLogger().verbose(config.getAccountId(), + "returning push on caller thread with id = " + Thread.currentThread().getId()); + coreState.getPushProviders().setPushNotificationRenderer(iNotificationRenderer); + if (extras != null && extras.containsKey(Constants.PT_NOTIF_ID)) { + return coreState.getPushProviders()._getNotification(context, extras, + extras.getInt(Constants.PT_NOTIF_ID)); + } else { + return coreState.getPushProviders()._getNotification(context, extras, Constants.EMPTY_NOTIFICATION_ID); + } + } + } catch (Throwable t) { + config.getLogger().debug(config.getAccountId(), "Failed to process getPushNotification()", t); + return null; + } + } + /** * Retrieves a notification bitmap with a specified timeout and size constraint. * diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/pushnotification/PushProviders.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/pushnotification/PushProviders.java index e74cc427f..384b797b4 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/pushnotification/PushProviders.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/pushnotification/PushProviders.java @@ -35,7 +35,6 @@ import com.clevertap.android.sdk.Logger; import com.clevertap.android.sdk.ManifestInfo; import com.clevertap.android.sdk.StorageHelper; -import com.clevertap.android.sdk.Utils; import com.clevertap.android.sdk.db.BaseDatabaseManager; import com.clevertap.android.sdk.db.DBAdapter; import com.clevertap.android.sdk.interfaces.AudibleNotification; @@ -65,7 +64,6 @@ @RestrictTo(Scope.LIBRARY_GROUP) public class PushProviders implements CTPushProviderListener { - private static final int DEFAULT_FLEX_INTERVAL = 5; private static final int PING_FREQUENCY_VALUE = 240; private static final String PF_JOB_ID = "pfjobid"; @@ -136,7 +134,7 @@ private PushProviders( /** * Launches an asynchronous task to download the notification icon from CleverTap, - * and create the Android notification. + * and render the Android notification. *

* If your app is using CleverTap SDK's built in FCM message handling, * this method does not need to be called explicitly. @@ -148,68 +146,100 @@ private PushProviders( * @param extras The {@link Bundle} object received by the broadcast receiver * @param notificationId A custom id to build a notification */ - public void _createNotification(final Context context, final Bundle extras, final int notificationId) { + public void _createNotification(final Context context, final Bundle extras, int notificationId) { + try { + int generatedNotificationId = generateNotificationId(notificationId, extras); + NotificationCompat.Builder nb = getPreparedNotificationBuilder(context, extras, generatedNotificationId); + if (nb != null) { + triggerNotification(nb, generatedNotificationId); + storePushNotification(extras); + } + } catch (Throwable t) { + config.getLogger() + .debug(config.getAccountId(), "Couldn't render notification: ", t); + } + } + + /** + * Launches an asynchronous task to download the notification icon from CleverTap, + * and get the Android notification builder. This function doesn't render the Android notification + * + * @param context A reference to an Android context + * @param extras The {@link Bundle} object received by the broadcast receiver + * @param notificationId A custom id to build a notification + */ + @RestrictTo(Scope.LIBRARY) + public NotificationCompat.Builder _getNotification(final Context context, final Bundle extras, int notificationId) { + try { + int generatedNotificationId = generateNotificationId(notificationId, extras); + NotificationCompat.Builder nb = getPreparedNotificationBuilder(context, extras, generatedNotificationId); + if (nb != null) { + return nb; + } + } catch (Throwable t) { + config.getLogger() + .debug(config.getAccountId(), "Couldn't get notification: ", t); + } + return null; + } + + private NotificationCompat.Builder getPreparedNotificationBuilder(final Context context, final Bundle extras, int generatedNotificationId) { + boolean proceed = canRenderNotification(extras); + if (!proceed) { + return null; + } + return prepareNotificationBuilder(context, extras, generatedNotificationId); + } + + private boolean canRenderNotification(Bundle extras) { if (extras == null || extras.get(Constants.NOTIFICATION_TAG) == null) { - return; + return false; } if (config.isAnalyticsOnly()) { config.getLogger() .debug(config.getAccountId(), "Instance is set for Analytics only, cannot create notification"); - return; + return false; } - try { - boolean isSilent = extras.getString(Constants.WZRK_PUSH_SILENT, "").equalsIgnoreCase("true"); - if (isSilent) { - analyticsManager.pushNotificationViewedEvent(extras); - return; + boolean isSilent = extras.getString(Constants.WZRK_PUSH_SILENT, "").equalsIgnoreCase("true"); + if (isSilent) { + analyticsManager.pushNotificationViewedEvent(extras); + return false; + } + String extrasFrom = extras.getString(Constants.EXTRAS_FROM); + if (extrasFrom == null || !extrasFrom.equals("PTReceiver")) { + config.getLogger() + .debug(config.getAccountId(), + "Handling notification: " + extras); + + if (extras.getString(Constants.WZRK_PUSH_ID) != null) { + if (baseDatabaseManager.loadDBAdapter(context) + .doesPushNotificationIdExist( + extras.getString(Constants.WZRK_PUSH_ID))) { + config.getLogger().debug(config.getAccountId(), + "Push Notification already rendered, not showing again"); + return false; + } } - String extrasFrom = extras.getString(Constants.EXTRAS_FROM); - if (extrasFrom == null || !extrasFrom.equals("PTReceiver")) { + String notifMessage = iNotificationRenderer.getMessage(extras); + notifMessage = (notifMessage != null) ? notifMessage : ""; + if (notifMessage.isEmpty()) { + //silent notification config.getLogger() - .debug(config.getAccountId(), - "Handling notification: " + extras); - - if (extras.getString(Constants.WZRK_PUSH_ID) != null) { - if (baseDatabaseManager.loadDBAdapter(context) - .doesPushNotificationIdExist( - extras.getString(Constants.WZRK_PUSH_ID))) { - config.getLogger().debug(config.getAccountId(), - "Push Notification already rendered, not showing again"); - return; - } - } - String notifMessage = iNotificationRenderer.getMessage(extras); - notifMessage = (notifMessage != null) ? notifMessage : ""; - if (notifMessage.isEmpty()) { - //silent notification - config.getLogger() - .verbose(config.getAccountId(), - "Push notification message is empty, not rendering"); - baseDatabaseManager.loadDBAdapter(context) - .storeUninstallTimestamp(); - String pingFreq = extras.getString("pf", ""); - if (!TextUtils.isEmpty(pingFreq)) { - updatePingFrequencyIfNeeded(context, Integer.parseInt(pingFreq)); - } - return; + .verbose(config.getAccountId(), + "Push notification message is empty, not rendering"); + baseDatabaseManager.loadDBAdapter(context) + .storeUninstallTimestamp(); + String pingFreq = extras.getString("pf", ""); + if (!TextUtils.isEmpty(pingFreq)) { + updatePingFrequencyIfNeeded(context, Integer.parseInt(pingFreq)); } + return false; } - - String notifTitle = iNotificationRenderer.getTitle(extras, - context);//extras.getString(Constants.NOTIF_TITLE, "");// uncommon - getTitle() - notifTitle = notifTitle.isEmpty() ? context.getApplicationInfo().name - : notifTitle;//common - triggerNotification(context, extras, notificationId); - } catch (Throwable t) { - // Occurs if the notification image was null - // Let's return, as we couldn't get a handle on the app's icon - // Some devices throw a PackageManager* exception too - config.getLogger() - .debug(config.getAccountId(), "Couldn't render notification: ", t); } + return true; } /** @@ -876,74 +906,118 @@ public void setPushNotificationRenderer(@NonNull INotificationRenderer iNotifica this.iNotificationRenderer = iNotificationRenderer; } - private void triggerNotification(Context context, Bundle extras, int notificationId) { + private void triggerNotification(@NonNull NotificationCompat.Builder nb, int notificationId) { + NotificationManager notificationManager = (NotificationManager) context.getSystemService(NOTIFICATION_SERVICE); + Notification n = nb.build(); + notificationManager.notify(notificationId, n); + config.getLogger().debug(config.getAccountId(), "Rendered notification: " + n); + } + + public NotificationCompat.Builder prepareNotificationBuilder(Context context, Bundle extras, int notificationId) { NotificationManager notificationManager = (NotificationManager) context.getSystemService(NOTIFICATION_SERVICE); if (notificationManager == null) { String notificationManagerError = "Unable to render notification, Notification Manager is null."; config.getLogger().debug(config.getAccountId(), notificationManagerError); - return; + return null; } - String channelId = extras.getString(Constants.WZRK_CHANNEL_ID, ""); - String updatedChannelId = null; final boolean requiresChannelId = VERSION.SDK_INT >= VERSION_CODES.O; - + NotificationCompat.Builder nb; if (requiresChannelId) { - int messageCode = -1; - String value = ""; - - if (channelId.isEmpty()) { - messageCode = Constants.CHANNEL_ID_MISSING_IN_PAYLOAD; - value = extras.toString(); - } else if (notificationManager.getNotificationChannel(channelId) == null) { - messageCode = Constants.CHANNEL_ID_NOT_REGISTERED; - value = channelId; + String updatedChannelId = getNotificationChannelId(notificationManager, context, extras); + if (updatedChannelId == null) { + return null; } - if (messageCode != -1) { - ValidationResult channelIdError = ValidationResultFactory.create(512, messageCode, value); - config.getLogger().debug(config.getAccountId(), channelIdError.getErrorDesc()); - validationResultStack.pushValidationResult(channelIdError); + nb = new NotificationCompat.Builder(context, updatedChannelId); + int badgeIconType = getNotificationBadgeIconType(extras); + if (badgeIconType >= 0) { + nb.setBadgeIconType(badgeIconType); } - // get channel using channel id from push payload. If channel id is null or empty then create default - updatedChannelId = CTXtensions.getOrCreateChannel(notificationManager, channelId, context); - - // if no channel gets created then do not render push - if (updatedChannelId == null || updatedChannelId.trim().isEmpty()) { - config.getLogger() - .debug(config.getAccountId(), "Not rendering Push since channel id is null or blank."); - return; + int badgeCount = getNotificationBadgeCount(extras); + if (badgeCount >= 0) { + nb.setNumber(badgeCount); } + } else { + nb = new NotificationCompat.Builder(context); + } - // if channel is blocked by user then do not render push - if (!CTXtensions.isNotificationChannelEnabled(context,updatedChannelId)) { - config.getLogger() - .verbose(config.getAccountId(), - "Not rendering push notification as channel = " + updatedChannelId + " is blocked by user"); - return; - } - config.getLogger().debug(config.getAccountId(), "Rendering Push on channel = " + updatedChannelId); + int smallIcon = getSmallIcon(context); + iNotificationRenderer.setSmallIcon(smallIcon, context); + + nb.setPriority(getNotificationPriority(extras)); + + if (iNotificationRenderer instanceof AudibleNotification) { + nb = ((AudibleNotification) iNotificationRenderer).setSound(context, extras, nb, config); + } + + // Generate the nb based on the rendered + nb = iNotificationRenderer.renderNotification(extras, context, nb, config, notificationId); + + return nb; + } + + @RequiresApi(api = VERSION_CODES.O) + private String getNotificationChannelId(NotificationManager notificationManager, Context context, Bundle extras) { + String channelId = extras.getString(Constants.WZRK_CHANNEL_ID, ""); + String updatedChannelId; + int messageCode = -1; + String value = ""; + + if (channelId.isEmpty()) { + messageCode = Constants.CHANNEL_ID_MISSING_IN_PAYLOAD; + value = extras.toString(); + } else if (notificationManager.getNotificationChannel(channelId) == null) { + messageCode = Constants.CHANNEL_ID_NOT_REGISTERED; + value = channelId; + } + if (messageCode != -1) { + ValidationResult channelIdError = ValidationResultFactory.create(512, messageCode, value); + config.getLogger().debug(config.getAccountId(), channelIdError.getErrorDesc()); + validationResultStack.pushValidationResult(channelIdError); } - int smallIcon; + // Get or create the channel using the channel ID from the push payload + updatedChannelId = CTXtensions.getOrCreateChannel(notificationManager, channelId, context); + + // If no channel is created, do not render the push notification + if (updatedChannelId == null || updatedChannelId.trim().isEmpty()) { + config.getLogger().debug(config.getAccountId(), "Not rendering Push since channel id is null or blank."); + return null; + } + + // If the channel is blocked by the user, do not render the push notification + if (!CTXtensions.isNotificationChannelEnabled(context, updatedChannelId)) { + config.getLogger().verbose(config.getAccountId(), + "Not rendering push notification as channel = " + updatedChannelId + " is blocked by user"); + return null; + } + + config.getLogger().debug(config.getAccountId(), "Rendering Push on channel = " + updatedChannelId); + return updatedChannelId; // Return the valid channel ID + } + + + private int getSmallIcon(Context context) { try { - String x = ManifestInfo.getInstance(context).getNotificationIcon(); - if (x == null) { + String iconName = ManifestInfo.getInstance(context).getNotificationIcon(); + if (iconName == null) { throw new IllegalArgumentException(); } - smallIcon = context.getResources().getIdentifier(x, "drawable", context.getPackageName()); + int smallIcon = context.getResources().getIdentifier(iconName, "drawable", context.getPackageName()); if (smallIcon == 0) { throw new IllegalArgumentException(); } + return smallIcon; } catch (Throwable t) { - smallIcon = DeviceInfo.getAppIconAsIntId(context); + return DeviceInfo.getAppIconAsIntId(context); // Fallback to app icon } + } - iNotificationRenderer.setSmallIcon(smallIcon, context); - + private int getNotificationPriority(Bundle extras) { int priorityInt = NotificationCompat.PRIORITY_DEFAULT; String priority = extras.getString(Constants.NOTIF_PRIORITY); if (priority != null) { @@ -954,7 +1028,39 @@ private void triggerNotification(Context context, Bundle extras, int notificatio priorityInt = NotificationCompat.PRIORITY_MAX; } } + return priorityInt; + } + + private int getNotificationBadgeIconType(Bundle extras) { + // Get badge icon + String badgeIconParam = extras.getString(Constants.WZRK_BADGE_ICON, null); + int badgeIconType = -1; + if (badgeIconParam != null) { + try { + badgeIconType = Integer.parseInt(badgeIconParam); + } catch (Throwable t) { + // no-op + } + } + return badgeIconType; + } + private int getNotificationBadgeCount(Bundle extras) { + // Get badge count + String badgeCountParam = extras.getString(Constants.WZRK_BADGE_COUNT, null); + int badgeCount = -1; + if (badgeCountParam != null) { + try { + badgeCount = Integer.parseInt(badgeCountParam); + } catch (Throwable t) { + // no-op + } + } + return badgeCount; + } + + + private int generateNotificationId(int notificationId, Bundle extras) { // if we have no user set notificationID then try collapse key if (notificationId == Constants.EMPTY_NOTIFICATION_ID) { try { @@ -994,59 +1100,10 @@ private void triggerNotification(Context context, Bundle extras, int notificatio notificationId = (int) (Math.random() * 100); config.getLogger().debug(config.getAccountId(), "Setting random notificationId: " + notificationId); } + return notificationId; + } - NotificationCompat.Builder nb; - if (requiresChannelId) { - nb = new NotificationCompat.Builder(context, updatedChannelId); - - // choices here are Notification.BADGE_ICON_NONE = 0, Notification.BADGE_ICON_SMALL = 1, Notification.BADGE_ICON_LARGE = 2. Default is Notification.BADGE_ICON_LARGE - String badgeIconParam = extras - .getString(Constants.WZRK_BADGE_ICON, null); - if (badgeIconParam != null) { - try { - int badgeIconType = Integer.parseInt(badgeIconParam); - if (badgeIconType >= 0) { - nb.setBadgeIconType(badgeIconType); - } - } catch (Throwable t) { - // no-op - } - } - - String badgeCountParam = extras.getString(Constants.WZRK_BADGE_COUNT, null);//cbi - if (badgeCountParam != null) { - try { - int badgeCount = Integer.parseInt(badgeCountParam); - if (badgeCount >= 0) { - nb.setNumber(badgeCount); - } - } catch (Throwable t) { - // no-op - } - } - - } else { - // noinspection all - nb = new NotificationCompat.Builder(context); - } - - nb.setPriority(priorityInt); - - //remove sound for fallback notif - - if (iNotificationRenderer instanceof AudibleNotification) { - nb = ((AudibleNotification) iNotificationRenderer).setSound(context, extras, nb, config); - } - - nb = iNotificationRenderer.renderNotification(extras, context, nb, config, notificationId); - if (nb == null) {// template renderer can return null if template type is null - return; - } - - Notification n = nb.build(); - notificationManager.notify(notificationId, n); - config.getLogger().debug(config.getAccountId(), "Rendered notification: " + n.toString());//cb - + public void storePushNotification(Bundle extras) { String extrasFrom = extras.getString(Constants.EXTRAS_FROM); if (extrasFrom == null || !extrasFrom.equals("PTReceiver")) { String ttl = extras.getString(Constants.WZRK_TIME_TO_LIVE, diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/validation/ManifestValidator.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/validation/ManifestValidator.java deleted file mode 100644 index 31086c40f..000000000 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/validation/ManifestValidator.java +++ /dev/null @@ -1,178 +0,0 @@ -package com.clevertap.android.sdk.validation; - -import android.app.Application; -import android.content.Context; -import android.content.pm.ActivityInfo; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager; -import android.content.pm.ServiceInfo; -import android.text.TextUtils; -import com.clevertap.android.sdk.ActivityLifecycleCallback; -import com.clevertap.android.sdk.CleverTapAPI; -import com.clevertap.android.sdk.DeviceInfo; -import com.clevertap.android.sdk.InAppNotificationActivity; -import com.clevertap.android.sdk.Logger; -import com.clevertap.android.sdk.ManifestInfo; -import com.clevertap.android.sdk.Utils; -import com.clevertap.android.sdk.inbox.CTInboxActivity; -import com.clevertap.android.sdk.pushnotification.CTNotificationIntentService; -import com.clevertap.android.sdk.pushnotification.CTPushNotificationReceiver; -import com.clevertap.android.sdk.pushnotification.PushConstants.PushType; -import com.clevertap.android.sdk.pushnotification.PushProviders; -import java.util.ArrayList; - - -public final class ManifestValidator { - - private final static String ourApplicationClassName = "com.clevertap.android.sdk.Application"; - - public static void validate(final Context context, DeviceInfo deviceInfo, PushProviders pushProviders) { - if (!Utils.hasPermission(context, "android.permission.INTERNET")) { - Logger.d("Missing Permission: android.permission.INTERNET"); - } - checkSDKVersion(deviceInfo); - validationApplicationLifecyleCallback(context); - checkReceiversServices(context, pushProviders); - if (!TextUtils.isEmpty(ManifestInfo.getInstance(context).getFCMSenderId())){ - Logger.i("We have noticed that your app is using a custom FCM Sender ID, this feature will " + - "be DISCONTINUED from the next version of the CleverTap Android SDK. With the next release, " + - "CleverTap Android SDK will only fetch the token using the google-services.json." + - " Please reach out to CleverTap Support for any questions."); - } - } - - private static void checkApplicationClass(final Context context) { - String appName = context.getApplicationInfo().className; - if (appName == null || appName.isEmpty()) { - Logger.i("Unable to determine Application Class"); - } else if (appName.equals(ourApplicationClassName)) { - Logger.i("AndroidManifest.xml uses the CleverTap Application class, " + - "be sure you have properly added the CleverTap Account ID and Token to your AndroidManifest.xml, \n" - + - "or set them programmatically in the onCreate method of your custom application class prior to calling super.onCreate()"); - } else { - Logger.i("Application Class is " + appName); - } - } - - @SuppressWarnings("ConstantConditions") - private static void checkReceiversServices(final Context context, PushProviders pushProviders) { - try { - validateReceiverInManifest((Application) context.getApplicationContext(), - CTPushNotificationReceiver.class.getName()); - validateServiceInManifest((Application) context.getApplicationContext(), - CTNotificationIntentService.class.getName()); - validateActivityInManifest((Application) context.getApplicationContext(), - InAppNotificationActivity.class); - validateActivityInManifest((Application) context.getApplicationContext(), - CTInboxActivity.class); - validateReceiverInManifest((Application) context.getApplicationContext(), - "com.clevertap.android.geofence.CTGeofenceReceiver"); - validateReceiverInManifest((Application) context.getApplicationContext(), - "com.clevertap.android.geofence.CTLocationUpdateReceiver"); - validateReceiverInManifest((Application) context.getApplicationContext(), - "com.clevertap.android.geofence.CTGeofenceBootReceiver"); - } catch (Exception e) { - Logger.v("Receiver/Service issue : " + e.toString()); - } - ArrayList enabledPushTypes = pushProviders.getAvailablePushTypes(); - if (enabledPushTypes == null) { - return; - } - - for (PushType pushType : enabledPushTypes) { - if (pushType == PushType.FCM) { - try { - // use class name string directly here to avoid class not found issues on class import - validateServiceInManifest((Application) context.getApplicationContext(), - "com.clevertap.android.sdk.pushnotification.fcm.FcmMessageListenerService"); - } catch (Exception e) { - Logger.v("Receiver/Service issue : " + e.toString()); - - } catch (Error error) { - Logger.v("FATAL : " + error.getMessage()); - } - }else if(pushType == PushType.HPS){ - try { - // use class name string directly here to avoid class not found issues on class import - validateServiceInManifest((Application) context.getApplicationContext(), - "com.clevertap.android.hms.CTHmsMessageService"); - } catch (Exception e) { - Logger.v("Receiver/Service issue : " + e.toString()); - - } catch (Error error) { - Logger.v("FATAL : " + error.getMessage()); - } - } - } - - } - - private static void checkSDKVersion(DeviceInfo deviceInfo) { - Logger.i("SDK Version Code is " + deviceInfo.getSdkVersion()); - } - - @SuppressWarnings({"SameParameterValue", "rawtypes"}) - private static void validateActivityInManifest(Application application, Class activityClass) - throws PackageManager.NameNotFoundException { - PackageManager pm = application.getPackageManager(); - String packageName = application.getPackageName(); - - PackageInfo packageInfo = pm.getPackageInfo(packageName, PackageManager.GET_ACTIVITIES); - ActivityInfo[] activities = packageInfo.activities; - String activityClassName = activityClass.getName(); - for (ActivityInfo activityInfo : activities) { - if (activityInfo.name.equals(activityClassName)) { - Logger.i(activityClassName.replaceFirst("com.clevertap.android.sdk.", "") + " is present"); - return; - } - } - Logger.i(activityClassName.replaceFirst("com.clevertap.android.sdk.", "") + " not present"); - } - - private static void validateReceiverInManifest(Application application, String receiverClassName) - throws PackageManager.NameNotFoundException { - PackageManager pm = application.getPackageManager(); - String packageName = application.getPackageName(); - - PackageInfo packageInfo = pm.getPackageInfo(packageName, PackageManager.GET_RECEIVERS); - ActivityInfo[] receivers = packageInfo.receivers; - - for (ActivityInfo activityInfo : receivers) { - if (activityInfo.name.equals(receiverClassName)) { - Logger.i(receiverClassName.replaceFirst("com.clevertap.android.", "") + " is present"); - return; - } - } - Logger.i(receiverClassName.replaceFirst("com.clevertap.android.", "") + " not present"); - } - - private static void validateServiceInManifest(Application application, String serviceClassName) - throws PackageManager.NameNotFoundException { - PackageManager pm = application.getPackageManager(); - String packageName = application.getPackageName(); - - PackageInfo packageInfo = pm.getPackageInfo(packageName, PackageManager.GET_SERVICES); - ServiceInfo[] services = packageInfo.services; - for (ServiceInfo serviceInfo : services) { - if (serviceInfo.name.equals(serviceClassName)) { - Logger.i(serviceClassName.replaceFirst("com.clevertap.android.sdk.", "") + " is present"); - return; - } - } - Logger.i(serviceClassName.replaceFirst("com.clevertap.android.sdk.", "") + " not present"); - } - - private static void validationApplicationLifecyleCallback(final Context context) { - // some of the ancillary SDK wrappers have to manage the activity lifecycle manually because they don't have access to the application object or whatever - // for those cases also consider CleverTapAPI.isAppForeground() as a proxy for the SDK being in sync with the activity lifecycle - if (!ActivityLifecycleCallback.registered && !CleverTapAPI.isAppForeground()) { - Logger.i( - "Activity Lifecycle Callback not registered. Either set the android:name in your AndroidManifest.xml application tag to com.clevertap.android.sdk.Application, \n or, " - + - "if you have a custom Application class, call ActivityLifecycleCallback.register(this); before super.onCreate() in your class"); - //Check for Application class only if the application lifecycle seems to be a problem - checkApplicationClass(context); - } - } -} diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/validation/ManifestValidator.kt b/clevertap-core/src/main/java/com/clevertap/android/sdk/validation/ManifestValidator.kt new file mode 100644 index 000000000..ae81d28ff --- /dev/null +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/validation/ManifestValidator.kt @@ -0,0 +1,177 @@ +package com.clevertap.android.sdk.validation + +import android.content.Context +import android.content.pm.PackageManager +import android.text.TextUtils +import com.clevertap.android.sdk.ActivityLifecycleCallback +import com.clevertap.android.sdk.CleverTapAPI +import com.clevertap.android.sdk.DeviceInfo +import com.clevertap.android.sdk.InAppNotificationActivity +import com.clevertap.android.sdk.Logger +import com.clevertap.android.sdk.ManifestInfo +import com.clevertap.android.sdk.Utils +import com.clevertap.android.sdk.inbox.CTInboxActivity +import com.clevertap.android.sdk.pushnotification.CTNotificationIntentService +import com.clevertap.android.sdk.pushnotification.CTPushNotificationReceiver +import com.clevertap.android.sdk.pushnotification.PushConstants.PushType +import com.clevertap.android.sdk.pushnotification.PushProviders + +object ManifestValidator { + private const val ourApplicationClassName = "com.clevertap.android.sdk.Application" + + @JvmStatic + fun validate(context: Context, deviceInfo: DeviceInfo, pushProviders: PushProviders) { + if (!Utils.hasPermission(context, "android.permission.INTERNET")) { + Logger.d("Missing Permission: android.permission.INTERNET") + } + checkSDKVersion(deviceInfo) + validationApplicationLifecycleCallback(context) + checkReceiversServices(context, pushProviders) + if (!TextUtils.isEmpty(ManifestInfo.getInstance(context).fcmSenderId)) { + Logger.i( + "We have noticed that your app is using a custom FCM Sender ID, this feature will " + + "be DISCONTINUED from the next version of the CleverTap Android SDK. With the next release, " + + "CleverTap Android SDK will only fetch the token using the google-services.json." + + " Please reach out to CleverTap Support for any questions." + ) + } + } + + private fun checkApplicationClass(context: Context) { + val appName = context.applicationInfo.className + if (appName == null || appName.isEmpty()) { + Logger.i("Unable to determine Application Class") + } else if (appName == ourApplicationClassName) { + Logger.i("AndroidManifest.xml uses the CleverTap Application class, " + + "be sure you have properly added the CleverTap Account ID and Token to your AndroidManifest.xml, \n" + + + "or set them programmatically in the onCreate method of your custom application class prior to calling super.onCreate()") + } else { + Logger.i("Application Class is $appName") + } + } + + private fun checkReceiversServices(context: Context, pushProviders: PushProviders) { + validateComponentInManifest( + context.applicationContext, + CTPushNotificationReceiver::class.java.name, ComponentType.RECEIVER + ) + validateComponentInManifest( + context.applicationContext, + CTNotificationIntentService::class.java.name, ComponentType.SERVICE + ) + validateComponentInManifest( + context.applicationContext, + InAppNotificationActivity::class.java.name, ComponentType.ACTIVITY + ) + validateComponentInManifest( + context.applicationContext, + CTInboxActivity::class.java.name, ComponentType.ACTIVITY + ) + validateComponentInManifest( + context.applicationContext, + "com.clevertap.android.geofence.CTGeofenceReceiver", ComponentType.RECEIVER + ) + validateComponentInManifest( + context.applicationContext, + "com.clevertap.android.geofence.CTLocationUpdateReceiver", ComponentType.RECEIVER + ) + validateComponentInManifest( + context.applicationContext, + "com.clevertap.android.geofence.CTGeofenceBootReceiver", ComponentType.RECEIVER + ) + validateComponentInManifest( + context.applicationContext, + "com.clevertap.android.pushtemplates.TimerTemplateService", ComponentType.SERVICE + ) + + val enabledPushTypes = pushProviders.availablePushTypes + + for (pushType in enabledPushTypes) { + if (pushType == PushType.FCM) { + // use class name string directly here to avoid class not found issues on class import + validateComponentInManifest( + context.applicationContext, + "com.clevertap.android.sdk.pushnotification.fcm.FcmMessageListenerService", + ComponentType.SERVICE + ) + } else if (pushType == PushType.HPS) { + // use class name string directly here to avoid class not found issues on class import + validateComponentInManifest( + context.applicationContext, + "com.clevertap.android.hms.CTHmsMessageService", ComponentType.SERVICE + ) + } + } + } + + private fun checkSDKVersion(deviceInfo: DeviceInfo) { + Logger.i("SDK Version Code is " + deviceInfo.sdkVersion) + } + + private fun validateComponentInManifest( + context: Context, + componentClassName: String, + componentType: ComponentType + ) { + if (isComponentPresentInManifest(context, componentClassName, componentType)) { + Logger.i( + componentClassName.replaceFirst( + "com.clevertap.android.sdk.", + "" + ) + " is present" + ) + } else { + Logger.i( + componentClassName.replaceFirst( + "com.clevertap.android.sdk.", + "" + ) + " not present" + ) + } + } + + @JvmStatic + fun isComponentPresentInManifest( + context: Context, + componentClassName: String, + componentType: ComponentType + ): Boolean { + val pm = context.packageManager + val packageName = context.packageName + + return try { + val packageInfo = pm.getPackageInfo(packageName, componentType.flag) + val components = when (componentType) { + ComponentType.SERVICE -> packageInfo.services + ComponentType.RECEIVER -> packageInfo.receivers + ComponentType.ACTIVITY -> packageInfo.activities + } + + components?.any { it.name == componentClassName } ?: false + } catch (e: PackageManager.NameNotFoundException) { + Logger.v("Issue in ${componentType.name.lowercase()}: $componentClassName - $e") + false + } + } + + + private fun validationApplicationLifecycleCallback(context: Context) { + // some of the ancillary SDK wrappers have to manage the activity lifecycle manually because they don't have access to the application object or whatever + // for those cases also consider CleverTapAPI.isAppForeground() as a proxy for the SDK being in sync with the activity lifecycle + if (!ActivityLifecycleCallback.registered && !CleverTapAPI.isAppForeground()) { + Logger.i( + "Activity Lifecycle Callback not registered. Either set the android:name in your AndroidManifest.xml application tag to com.clevertap.android.sdk.Application, \n or, " + + + "if you have a custom Application class, call ActivityLifecycleCallback.register(this); before super.onCreate() in your class") + //Check for Application class only if the application lifecycle seems to be a problem + checkApplicationClass(context) + } + } + + enum class ComponentType(val flag: Int) { + RECEIVER(PackageManager.GET_RECEIVERS), + SERVICE(PackageManager.GET_SERVICES), + ACTIVITY(PackageManager.GET_ACTIVITIES) + } +} diff --git a/clevertap-core/src/test/java/com/clevertap/android/sdk/validation/ManifestValidatorTest.kt b/clevertap-core/src/test/java/com/clevertap/android/sdk/validation/ManifestValidatorTest.kt index 5788b0e61..9e8ee6fba 100644 --- a/clevertap-core/src/test/java/com/clevertap/android/sdk/validation/ManifestValidatorTest.kt +++ b/clevertap-core/src/test/java/com/clevertap/android/sdk/validation/ManifestValidatorTest.kt @@ -1,17 +1,140 @@ package com.clevertap.android.sdk.validation +import android.content.pm.ActivityInfo +import android.content.pm.PackageInfo +import android.content.pm.ServiceInfo import com.clevertap.android.shared.test.BaseTestCase import org.junit.* -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner +import org.robolectric.shadows.ShadowPackageManager +import kotlin.test.assertTrue +import kotlin.test.assertFalse -@RunWith(RobolectricTestRunner::class) -class ManifestValidatorTest :BaseTestCase(){ -// Manifest validator only generates logs using a static logger, so it can't be tested for its working. -// ---- -------- ---- ------ -------- ---- ------ -------- ---- ------ -------- ---- ---- +class ManifestValidatorTest :BaseTestCase() { @Test - fun test(){ - assert(true) + fun `test isComponentPresentInManifest() should return true if service is present in manifest`() { + + val service = ServiceInfo().also { + it.name = "com.example.MyService" + it.packageName = application.applicationInfo.packageName + } + val packageInfo = PackageInfo().also { + it.packageName = application.applicationInfo.packageName + it.applicationInfo = application.applicationInfo + it.services = arrayOf(service) + } + ShadowPackageManager().installPackage(packageInfo) + + assertTrue { + ManifestValidator.isComponentPresentInManifest( + appCtx, + "com.example.MyService", + ManifestValidator.ComponentType.SERVICE + ) + } + } + + @Test + fun `test isComponentPresentInManifest() should return true if receiver is present in manifest`() { + + val receiver = ActivityInfo().also { + it.name = "com.example.MyReceiver" + it.packageName = application.applicationInfo.packageName + } + val packageInfo = PackageInfo().also { + it.packageName = application.applicationInfo.packageName + it.applicationInfo = application.applicationInfo + it.receivers = arrayOf(receiver) + } + ShadowPackageManager().installPackage(packageInfo) + + assertTrue { + ManifestValidator.isComponentPresentInManifest( + appCtx, + "com.example.MyReceiver", + ManifestValidator.ComponentType.RECEIVER + ) + } + } + + @Test + fun `test isComponentPresentInManifest() should return true if activity is present in manifest`() { + + val activity = ActivityInfo().also { + it.name = "com.example.MyActivity" + it.packageName = application.applicationInfo.packageName + } + val packageInfo = PackageInfo().also { + it.packageName = application.applicationInfo.packageName + it.applicationInfo = application.applicationInfo + it.activities = arrayOf(activity) + } + ShadowPackageManager().installPackage(packageInfo) + + assertTrue { + ManifestValidator.isComponentPresentInManifest( + appCtx, + "com.example.MyActivity", + ManifestValidator.ComponentType.ACTIVITY + ) + } + } + + @Test + fun `test isComponentPresentInManifest() should return false if service is not present in manifest`() { + + val packageInfo = PackageInfo().also { + it.packageName = application.applicationInfo.packageName + it.applicationInfo = application.applicationInfo + it.services = arrayOf() // No services + } + ShadowPackageManager().installPackage(packageInfo) + + assertFalse { + ManifestValidator.isComponentPresentInManifest( + appCtx, + "com.example.MyNonExistentService", + ManifestValidator.ComponentType.SERVICE + ) + } + } + + @Test + fun `test isComponentPresentInManifest() should return false if receiver is not present in manifest`() { + + val packageInfo = PackageInfo().also { + it.packageName = application.applicationInfo.packageName + it.applicationInfo = application.applicationInfo + it.receivers = arrayOf() // No receivers + } + ShadowPackageManager().installPackage(packageInfo) + + assertFalse { + ManifestValidator.isComponentPresentInManifest( + appCtx, + "com.example.MyNonExistentReceiver", + ManifestValidator.ComponentType.RECEIVER + ) + } + } + + + @Test + fun `test isComponentPresentInManifest() should return false if activity is not present in manifest`() { + + val packageInfo = PackageInfo().also { + it.packageName = application.applicationInfo.packageName + it.applicationInfo = application.applicationInfo + it.activities = arrayOf() // No activities + } + ShadowPackageManager().installPackage(packageInfo) + + assertFalse { + ManifestValidator.isComponentPresentInManifest( + appCtx, + "com.example.MyNonExistentActivity", + ManifestValidator.ComponentType.ACTIVITY + ) + } } } \ No newline at end of file diff --git a/clevertap-pushtemplates/src/main/java/com/clevertap/android/pushtemplates/PushTemplateNotificationHandler.java b/clevertap-pushtemplates/src/main/java/com/clevertap/android/pushtemplates/PushTemplateNotificationHandler.java index bcee4d0a9..2a7b7943d 100644 --- a/clevertap-pushtemplates/src/main/java/com/clevertap/android/pushtemplates/PushTemplateNotificationHandler.java +++ b/clevertap-pushtemplates/src/main/java/com/clevertap/android/pushtemplates/PushTemplateNotificationHandler.java @@ -1,12 +1,15 @@ package com.clevertap.android.pushtemplates; import android.content.Context; +import android.content.Intent; import android.os.Bundle; +import androidx.core.content.ContextCompat; import com.clevertap.android.sdk.CleverTapAPI; import com.clevertap.android.sdk.CleverTapInstanceConfig; import com.clevertap.android.sdk.interfaces.ActionButtonClickHandler; -import com.clevertap.android.sdk.pushnotification.INotificationRenderer; import com.clevertap.android.sdk.pushnotification.PushNotificationUtil; +import com.clevertap.android.sdk.validation.ManifestValidator; + import java.util.Objects; public class PushTemplateNotificationHandler implements ActionButtonClickHandler { @@ -18,9 +21,7 @@ public boolean onActionButtonClick(final Context context, final Bundle extras, f CleverTapInstanceConfig config = extras.getParcelable("config"); if (dismissOnClick != null && dismissOnClick.equalsIgnoreCase("true")) { - /** - * For input box remind CTA,pt_dismiss_on_click must be true to raise event - */ + // For input box remind CTA,pt_dismiss_on_click must be true to raise event if (actionID != null && actionID.contains("remind")) { Utils.raiseCleverTapEvent(context, config, extras); } @@ -34,13 +35,17 @@ public boolean onActionButtonClick(final Context context, final Bundle extras, f public boolean onMessageReceived(final Context applicationContext, final Bundle message, final String pushType) { try { PTLog.debug("Inside Push Templates"); - // initial setup - INotificationRenderer templateRenderer = new TemplateRenderer(applicationContext, message); - CleverTapAPI cleverTapAPI = CleverTapAPI - .getGlobalInstance(applicationContext, - PushNotificationUtil.getAccountIdFromNotificationBundle(message)); - Objects.requireNonNull(cleverTapAPI) - .renderPushNotificationOnCallerThread(templateRenderer, applicationContext, message); + TemplateRenderer templateRenderer = new TemplateRenderer(applicationContext, message); + if (ManifestValidator.isComponentPresentInManifest(applicationContext, "com.clevertap.android.pushtemplates.TimerTemplateService", ManifestValidator.ComponentType.SERVICE) + && templateRenderer.getTemplateType() == TemplateType.TIMER) { + PTLog.debug("Starting service for Timer Template"); + Intent serviceIntent = new Intent(applicationContext, TimerTemplateService.class); + serviceIntent.putExtras(message); + ContextCompat.startForegroundService(applicationContext, serviceIntent); + } else { + CleverTapAPI cleverTapAPI = CleverTapAPI.getGlobalInstance(applicationContext, PushNotificationUtil.getAccountIdFromNotificationBundle(message)); + Objects.requireNonNull(cleverTapAPI).renderPushNotificationOnCallerThread(templateRenderer, applicationContext, message); + } } catch (Throwable throwable) { PTLog.verbose("Error parsing FCM payload", throwable); @@ -52,5 +57,4 @@ public boolean onMessageReceived(final Context applicationContext, final Bundle public boolean onNewToken(final Context applicationContext, final String token, final String pushType) { return true; } - } \ No newline at end of file diff --git a/clevertap-pushtemplates/src/main/java/com/clevertap/android/pushtemplates/TemplateRenderer.kt b/clevertap-pushtemplates/src/main/java/com/clevertap/android/pushtemplates/TemplateRenderer.kt index 271f461a1..afac5498a 100644 --- a/clevertap-pushtemplates/src/main/java/com/clevertap/android/pushtemplates/TemplateRenderer.kt +++ b/clevertap-pushtemplates/src/main/java/com/clevertap/android/pushtemplates/TemplateRenderer.kt @@ -13,7 +13,6 @@ import android.os.* import android.os.Build.VERSION import android.os.Build.VERSION_CODES import androidx.annotation.RequiresApi -import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat.Builder import com.clevertap.android.pushtemplates.content.FiveIconBigContentView import com.clevertap.android.pushtemplates.content.FiveIconSmallContentView @@ -29,6 +28,7 @@ import com.clevertap.android.sdk.pushnotification.CTNotificationIntentService import com.clevertap.android.sdk.pushnotification.INotificationRenderer import com.clevertap.android.sdk.pushnotification.PushNotificationHandler import com.clevertap.android.sdk.pushnotification.PushNotificationUtil +import com.clevertap.android.sdk.validation.ManifestValidator import org.json.JSONArray import org.json.JSONException import org.json.JSONObject @@ -109,10 +109,10 @@ class TemplateRenderer : INotificationRenderer, AudibleNotification { } override fun renderNotification( - extras: Bundle, context: Context, nb: NotificationCompat.Builder, + extras: Bundle, context: Context, nb: Builder, config: CleverTapInstanceConfig, notificationId: Int - ): NotificationCompat.Builder? { + ): Builder? { if (pt_id == null) { PTLog.verbose("Template ID not provided. Cannot create the notification") return null @@ -165,7 +165,7 @@ class TemplateRenderer : INotificationRenderer, AudibleNotification { if (ValidatorFactory.getValidator(TemplateType.ZERO_BEZEL, this)?.validate() == true) return ZeroBezelStyle(this).builderFromStyle(context, extras, notificationId, nb) - TemplateType.TIMER -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + TemplateType.TIMER -> if (VERSION.SDK_INT >= VERSION_CODES.N) { if (ValidatorFactory.getValidator(TemplateType.TIMER, this)?.validate() == true) { val timerEnd = getTimerEnd() if (timerEnd != null) { @@ -225,7 +225,11 @@ class TemplateRenderer : INotificationRenderer, AudibleNotification { return timer_end } - @RequiresApi(Build.VERSION_CODES.M) + fun getTemplateType() : TemplateType? { + return templateType + } + + @RequiresApi(VERSION_CODES.M) private fun timerRunner(context: Context, extras: Bundle, notificationId: Int, delay: Int?) { val handler = Handler(Looper.getMainLooper()) @@ -238,6 +242,13 @@ class TemplateRenderer : INotificationRenderer, AudibleNotification { ) && ValidatorFactory.getValidator(TemplateType.BASIC, this)?.validate() == true ) { val applicationContext = context.applicationContext + if (ManifestValidator.isComponentPresentInManifest( + applicationContext, + "com.clevertap.android.pushtemplates.TimerTemplateService", + ManifestValidator.ComponentType.SERVICE)) { + val intent = Intent(context, TimerTemplateService::class.java) + context.stopService(intent) + } val basicTemplateBundle = extras.clone() as Bundle basicTemplateBundle.remove("wzrk_rnv") basicTemplateBundle.putString(Constants.WZRK_PUSH_ID, null) // skip dupe check diff --git a/clevertap-pushtemplates/src/main/java/com/clevertap/android/pushtemplates/TemplateType.kt b/clevertap-pushtemplates/src/main/java/com/clevertap/android/pushtemplates/TemplateType.kt index 45866918c..0a8e3081d 100644 --- a/clevertap-pushtemplates/src/main/java/com/clevertap/android/pushtemplates/TemplateType.kt +++ b/clevertap-pushtemplates/src/main/java/com/clevertap/android/pushtemplates/TemplateType.kt @@ -1,6 +1,6 @@ package com.clevertap.android.pushtemplates -internal enum class TemplateType(private val templateType: String) { +enum class TemplateType(private val templateType: String) { BASIC("pt_basic"), AUTO_CAROUSEL("pt_carousel"), MANUAL_CAROUSEL("pt_manual_carousel"), RATING("pt_rating"), FIVE_ICONS("pt_five_icons"), PRODUCT_DISPLAY("pt_product_display"), diff --git a/clevertap-pushtemplates/src/main/java/com/clevertap/android/pushtemplates/TimerTemplateService.kt b/clevertap-pushtemplates/src/main/java/com/clevertap/android/pushtemplates/TimerTemplateService.kt new file mode 100644 index 000000000..eda9b4b17 --- /dev/null +++ b/clevertap-pushtemplates/src/main/java/com/clevertap/android/pushtemplates/TimerTemplateService.kt @@ -0,0 +1,56 @@ +package com.clevertap.android.pushtemplates + +import android.annotation.SuppressLint +import android.app.Service +import android.content.Intent +import android.os.IBinder +import androidx.core.app.NotificationCompat +import com.clevertap.android.sdk.CleverTapAPI +import com.clevertap.android.sdk.CleverTapInstanceConfig +import com.clevertap.android.sdk.pushnotification.PushNotificationUtil +import com.clevertap.android.sdk.task.CTExecutorFactory + +class TimerTemplateService : Service() { + @SuppressLint("WrongConstant") + override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { + val message = intent.extras ?: return super.onStartCommand(intent, flags, startId) + val templateRenderer = TemplateRenderer(this@TimerTemplateService, message) + PTLog.verbose("Running Timer Template Service") + + val cleverTapAPI = CleverTapAPI.getGlobalInstance( + this@TimerTemplateService, + PushNotificationUtil.getAccountIdFromNotificationBundle(message) + ) + val config: CleverTapInstanceConfig? = cleverTapAPI?.coreState?.config + + config?.let { + val task = CTExecutorFactory.executors(config).postAsyncSafelyTask() + task.execute("getTimerTemplateNotificationBuilder") { + try { + val notificationBuilder = cleverTapAPI.getPushNotificationOnCallerThread( + templateRenderer, this@TimerTemplateService, message + ) + + notificationBuilder?.let { + it.setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE) + val nb = it.build() + PTLog.verbose("Starting foreground service with notification ID: ${templateRenderer.notificationId}") + startForeground(templateRenderer.notificationId, nb) + cleverTapAPI.coreState?.pushProviders?.storePushNotification(message) + } ?: run { + PTLog.verbose("NotificationBuilder is null.") + } + } catch (e: Exception) { + PTLog.verbose("Error while creating notification: ${e.localizedMessage}") + e.printStackTrace() + } + null + } + } + return super.onStartCommand(intent, flags, startId) + } + + override fun onBind(intent: Intent): IBinder? { + return null + } +} diff --git a/sample/src/main/AndroidManifest.xml b/sample/src/main/AndroidManifest.xml index aea65b6f4..d9e8c35a1 100644 --- a/sample/src/main/AndroidManifest.xml +++ b/sample/src/main/AndroidManifest.xml @@ -105,6 +105,11 @@ + +