package com.appboy.push; import android.Manifest; import android.annotation.TargetApi; import android.app.AlarmManager; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.PowerManager; import android.os.SystemClock; import android.support.v4.app.NotificationCompat; import android.util.Log; import com.appboy.Appboy; import com.appboy.AppboyAdmReceiver; import com.appboy.AppboyGcmReceiver; import com.appboy.AppboyInternal; import com.appboy.Constants; import com.appboy.IAppboyNotificationFactory; import com.appboy.configuration.AppboyConfigurationProvider; import com.appboy.enums.AppboyViewBounds; import com.appboy.enums.Channel; import com.appboy.support.AppboyImageUtils; import com.appboy.support.AppboyLogger; import com.appboy.support.IntentUtils; import com.appboy.support.PermissionUtils; import com.appboy.support.StringUtils; import com.appboy.ui.AppboyNavigator; import com.appboy.ui.actions.ActionFactory; import com.appboy.ui.actions.UriAction; import com.appboy.ui.support.UriUtils; import org.json.JSONException; import org.json.JSONObject; import java.util.Iterator; public class AppboyNotificationUtils { private static final String TAG = String.format("%s.%s", Constants.APPBOY_LOG_TAG_PREFIX, AppboyNotificationUtils.class.getName()); private static final String SOURCE_KEY = "source"; public static final String APPBOY_NOTIFICATION_OPENED_SUFFIX = ".intent.APPBOY_NOTIFICATION_OPENED"; public static final String APPBOY_NOTIFICATION_RECEIVED_SUFFIX = ".intent.APPBOY_PUSH_RECEIVED"; /** * Handles a push notification click. Called by GCM/ADM receiver when an * Appboy push notification click intent is received. * <p/> * See {@link #logNotificationOpened} and {@link #sendNotificationOpenedBroadcast} * * @param context * @param intent the internal notification clicked intent constructed in * {@link #setContentIntentIfPresent} */ public static void handleNotificationOpened(Context context, Intent intent) { try { logNotificationOpened(context, intent); sendNotificationOpenedBroadcast(context, intent); AppboyConfigurationProvider appConfigurationProvider = new AppboyConfigurationProvider(context); if (appConfigurationProvider.getHandlePushDeepLinksAutomatically()) { routeUserWithNotificationOpenedIntent(context, intent); } } catch (Exception e) { AppboyLogger.e(TAG, "Exception occurred attempting to handle notification.", e); } } /** * Opens any available deep links with an Intent.ACTION_VIEW intent, placing the main activity * on the back stack. If no deep link is available, opens the main activity. * * @param context * @param intent the internal notification clicked intent constructed in * {@link #setContentIntentIfPresent} */ public static void routeUserWithNotificationOpenedIntent(Context context, Intent intent) { // get extras bundle. Bundle extras = intent.getBundleExtra(Constants.APPBOY_PUSH_EXTRAS_KEY); if (extras == null) { extras = new Bundle(); } extras.putString(AppboyGcmReceiver.CAMPAIGN_ID_KEY, intent.getStringExtra(AppboyGcmReceiver.CAMPAIGN_ID_KEY)); extras.putString(SOURCE_KEY, Constants.APPBOY); // If a deep link exists, start an ACTION_VIEW intent pointing at the deep link. // The intent returned from getStartActivityIntent() is placed on the back stack. // Otherwise, start the intent defined in getStartActivityIntent(). String deepLink = intent.getStringExtra(Constants.APPBOY_PUSH_DEEP_LINK_KEY); if (!StringUtils.isNullOrBlank(deepLink)) { Log.d(TAG, String.format("Found a deep link %s.", deepLink)); boolean useWebView = "true".equalsIgnoreCase(intent.getStringExtra(Constants.APPBOY_PUSH_OPEN_URI_IN_WEBVIEW_KEY)); Log.d(TAG, "Use webview set to: " + useWebView); UriAction uriAction = ActionFactory.createUriActionFromUrlString(deepLink, extras, useWebView, Channel.PUSH); AppboyNavigator.getAppboyNavigator().gotoUri(context, uriAction); } else { Log.d(TAG, "Push notification had no deep link. Opening main activity."); context.startActivity(UriUtils.getMainActivityIntent(context, extras)); } } /** * Get the Appboy extras Bundle from the notification extras. Notification extras must be in GCM/ADM format. * * @param notificationExtras Notification extras as provided by GCM/ADM. * @return Returns the Appboy extras Bundle from the notification extras. Amazon ADM recursively flattens all JSON messages, * so for Amazon devices we just return a copy of the original bundle. */ public static Bundle getAppboyExtrasWithoutPreprocessing(Bundle notificationExtras) { if (notificationExtras == null) { return null; } if (!Constants.IS_AMAZON) { return AppboyNotificationUtils.parseJSONStringDictionaryIntoBundle(AppboyNotificationUtils.bundleOptString(notificationExtras, Constants.APPBOY_PUSH_EXTRAS_KEY, "{}")); } else { return new Bundle(notificationExtras); } } /** * Returns the specified String if it is found in the bundle; otherwise it returns the defaultString. */ @TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1) public static String bundleOptString(Bundle bundle, String key, String defaultValue) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1) { return bundle.getString(key, defaultValue); } else { String result = bundle.getString(key); if (result == null) { result = defaultValue; } return result; } } /** * Parses the JSON into a bundle. The JSONObject parsed from the input string must be a flat * dictionary with all string values. */ public static Bundle parseJSONStringDictionaryIntoBundle(String jsonStringDictionary) { try { Bundle bundle = new Bundle(); JSONObject json = new JSONObject(jsonStringDictionary); Iterator keys = json.keys(); while (keys.hasNext()) { String key = (String) keys.next(); bundle.putString(key, json.getString(key)); } return bundle; } catch (JSONException e) { AppboyLogger.e(TAG, "Unable parse JSON into a bundle.", e); return null; } } /** * Checks the incoming GCM/ADM intent to determine whether this is an Appboy push message. * <p/> * All Appboy push messages must contain an extras entry with key set to "_ab" and value set to "true". */ public static boolean isAppboyPushMessage(Intent intent) { Bundle extras = intent.getExtras(); return extras != null && "true".equals(extras.getString(Constants.APPBOY_PUSH_APPBOY_KEY)); } /** * Checks the intent received from GCM to determine whether this is a notification message or a * data push. * <p/> * A notification message is an Appboy push message that displays a notification in the * notification center (and optionally contains extra information that can be used directly * by the app). * <p/> * A data push is an Appboy push message that contains only extra information that can * be used directly by the app. */ public static boolean isNotificationMessage(Intent intent) { Bundle extras = intent.getExtras(); return extras != null && extras.containsKey(Constants.APPBOY_PUSH_TITLE_KEY) && extras.containsKey(Constants.APPBOY_PUSH_CONTENT_KEY); } /** * Creates and sends a broadcast message that can be listened for by the host app. The broadcast * message intent contains all of the data sent as part of the Appboy push message. The broadcast * message action is <host-app-package-name>.intent.APPBOY_PUSH_RECEIVED. */ public static void sendPushMessageReceivedBroadcast(Context context, Bundle notificationExtras) { String pushReceivedAction = context.getPackageName() + APPBOY_NOTIFICATION_RECEIVED_SUFFIX; Intent pushReceivedIntent = new Intent(pushReceivedAction); if (notificationExtras != null) { pushReceivedIntent.putExtras(notificationExtras); } AppboyLogger.d(TAG, "Sending push message received broadcast"); context.sendBroadcast(pushReceivedIntent); } /** * Requests a geofence refresh from Appboy if appropriate based on the payload of the push notification. * * @param context * @param notificationExtras Notification extras as provided by GCM/ADM. * @return True iff a geofence refresh was requested from Appboy. */ public static boolean requestGeofenceRefreshIfAppropriate(Context context, Bundle notificationExtras) { if (notificationExtras.containsKey(Constants.APPBOY_PUSH_SYNC_GEOFENCES_KEY)) { if (Boolean.parseBoolean(notificationExtras.getString(Constants.APPBOY_PUSH_SYNC_GEOFENCES_KEY))) { AppboyInternal.requestGeofenceRefresh(context, true); return true; } else { AppboyLogger.d(TAG, "Geofence sync key was false. Not syncing geofences."); } } else { AppboyLogger.d(TAG, "Geofence sync key not included in push payload. Not syncing geofences."); } return false; } /** * Creates an alarm which will issue a broadcast to cancel the notification specified by the given notificationId after the given duration. */ public static void setNotificationDurationAlarm(Context context, Class<?> thisClass, int notificationId, int durationInMillis) { Intent cancelIntent = new Intent(context, thisClass); cancelIntent.setAction(Constants.APPBOY_CANCEL_NOTIFICATION_ACTION); cancelIntent.putExtra(Constants.APPBOY_PUSH_NOTIFICATION_ID, notificationId); PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, cancelIntent, PendingIntent.FLAG_UPDATE_CURRENT); AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); if (durationInMillis >= Constants.APPBOY_MINIMUM_NOTIFICATION_DURATION_MILLIS) { AppboyLogger.d(TAG, String.format("Setting Notification duration alarm for %d ms", durationInMillis)); alarmManager.set(AlarmManager.ELAPSED_REALTIME, SystemClock.elapsedRealtime() + durationInMillis, pendingIntent); } } /** * Returns an id for the new notification we'll send to the notification center. * Notification id is used by the Android OS to override currently active notifications with identical ids. * If a custom notification id is not defined in the payload, Appboy derives an id value from the message's contents * to prevent duplication in the notification center. */ public static int getNotificationId(Bundle notificationExtras) { if (notificationExtras != null) { if (notificationExtras.containsKey(Constants.APPBOY_PUSH_CUSTOM_NOTIFICATION_ID)) { try { int notificationId = Integer.parseInt(notificationExtras.getString(Constants.APPBOY_PUSH_CUSTOM_NOTIFICATION_ID)); AppboyLogger.d(TAG, "Using notification id provided in the message's extras bundle: " + notificationId); return notificationId; } catch (NumberFormatException e) { AppboyLogger.e(TAG, "Unable to parse notification id provided in the " + "message's extras bundle. Using default notification id instead: " + Constants.APPBOY_DEFAULT_NOTIFICATION_ID, e); return Constants.APPBOY_DEFAULT_NOTIFICATION_ID; } } else { String messageKey = AppboyNotificationUtils.bundleOptString(notificationExtras, Constants.APPBOY_PUSH_TITLE_KEY, "") + AppboyNotificationUtils.bundleOptString(notificationExtras, Constants.APPBOY_PUSH_CONTENT_KEY, ""); int notificationId = messageKey.hashCode(); AppboyLogger.d(TAG, "Message without notification id provided in the extras bundle received. Using a hash of the message: " + notificationId); return notificationId; } } else { AppboyLogger.d(TAG, String.format("Message without extras bundle received. Using default notification id: " + Constants.APPBOY_DEFAULT_NOTIFICATION_ID)); return Constants.APPBOY_DEFAULT_NOTIFICATION_ID; } } /** * This method will retrieve notification priority from notificationExtras bundle if it has been set. * Otherwise returns the default priority. */ @TargetApi(Build.VERSION_CODES.JELLY_BEAN) public static int getNotificationPriority(Bundle notificationExtras) { if (notificationExtras != null && notificationExtras.containsKey(Constants.APPBOY_PUSH_PRIORITY_KEY)) { try { int notificationPriority = Integer.parseInt(notificationExtras.getString(Constants.APPBOY_PUSH_PRIORITY_KEY)); if (isValidNotificationPriority(notificationPriority)) { return notificationPriority; } else { AppboyLogger.e(TAG, String.format("Received invalid notification priority %d", notificationPriority)); } } catch (NumberFormatException e) { AppboyLogger.e(TAG, "Unable to parse custom priority. Returning default priority of " + Notification.PRIORITY_DEFAULT, e); } } return Notification.PRIORITY_DEFAULT; } /** * Checks whether the given integer value is a valid Android notification priority constant. */ @TargetApi(Build.VERSION_CODES.JELLY_BEAN) public static boolean isValidNotificationPriority(int priority) { return (priority >= Notification.PRIORITY_MIN && priority <= Notification.PRIORITY_MAX); } /** * This method will wake the device using a wake lock if the WAKE_LOCK permission is present in the * manifest. If the permission is not present, this does nothing. If the screen is already on, * and the permission is present, this does nothing. If the priority of the incoming notification * is min, this does nothing. */ public static boolean wakeScreenIfHasPermission(Context context, Bundle notificationExtras) { // Check for the wake lock permission. if (!PermissionUtils.hasPermission(context, Manifest.permission.WAKE_LOCK)) { return false; } // Don't wake lock if this is a minimum priority notification. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { if (getNotificationPriority(notificationExtras) == Notification.PRIORITY_MIN) { return false; } } // Get the power manager for the wake lock. PowerManager powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE); PowerManager.WakeLock wakeLock = powerManager.newWakeLock(PowerManager.FULL_WAKE_LOCK | PowerManager.ACQUIRE_CAUSES_WAKEUP, TAG); // Acquire the wake lock for some negligible time, then release it. We just want to wake the screen // and not take up more CPU power than necessary. wakeLock.acquire(); wakeLock.release(); return true; } /** * Returns a custom AppboyNotificationFactory if set, else the default AppboyNotificationFactory */ public static IAppboyNotificationFactory getActiveNotificationFactory() { IAppboyNotificationFactory customAppboyNotificationFactory = Appboy.getCustomAppboyNotificationFactory(); if (customAppboyNotificationFactory == null) { return AppboyNotificationFactory.getInstance(); } else { return customAppboyNotificationFactory; } } /** * Sets notification title if it exists in the notificationExtras. */ public static void setTitleIfPresent(NotificationCompat.Builder notificationBuilder, Bundle notificationExtras) { if (notificationExtras != null) { AppboyLogger.d(TAG, "Setting title for notification"); notificationBuilder.setContentTitle(notificationExtras.getString(Constants.APPBOY_PUSH_TITLE_KEY)); } } /** * Sets notification content if it exists in the notificationExtras. */ public static void setContentIfPresent(NotificationCompat.Builder notificationBuilder, Bundle notificationExtras) { if (notificationExtras != null) { AppboyLogger.d(TAG, "Setting content for notification"); notificationBuilder.setContentText(notificationExtras.getString(Constants.APPBOY_PUSH_CONTENT_KEY)); } } /** * Sets notification ticker to the title if it exists in the notificationExtras. */ public static void setTickerIfPresent(NotificationCompat.Builder notificationBuilder, Bundle notificationExtras) { if (notificationExtras != null) { AppboyLogger.d(TAG, "Setting ticker for notification"); notificationBuilder.setTicker(notificationExtras.getString(Constants.APPBOY_PUSH_TITLE_KEY)); } } /** * Create broadcast intent that will fire when the notification has been opened. The GCM or ADM receiver will be notified, * log a click, then send a broadcast to the client receiver. * * @param context * @param notificationBuilder * @param notificationExtras */ public static void setContentIntentIfPresent(Context context, NotificationCompat.Builder notificationBuilder, Bundle notificationExtras) { try { Intent pushOpenedIntent = new Intent(Constants.APPBOY_PUSH_CLICKED_ACTION).setClass(context, AppboyNotificationUtils.getNotificationReceiverClass()); if (notificationExtras != null) { pushOpenedIntent.putExtras(notificationExtras); } PendingIntent pushOpenedPendingIntent = PendingIntent.getBroadcast(context, IntentUtils.getRequestCode(), pushOpenedIntent, PendingIntent.FLAG_ONE_SHOT); notificationBuilder.setContentIntent(pushOpenedPendingIntent); } catch (Exception e) { AppboyLogger.e(TAG, "Error setting content.", e); } } /** * Sets the icon used in the notification bar itself. * If a drawable defined in appboy.xml is found, we use that. Otherwise, fall back to the application icon. * * @return the resource id of the small icon to be used. */ public static int setSmallIcon(AppboyConfigurationProvider appConfigurationProvider, NotificationCompat.Builder notificationBuilder) { int smallNotificationIconResourceId = appConfigurationProvider.getSmallNotificationIconResourceId(); if (smallNotificationIconResourceId == 0) { AppboyLogger.d(TAG, "Small notification icon resource was not found. Will use the app icon when " + "displaying notifications."); smallNotificationIconResourceId = appConfigurationProvider.getApplicationIconResourceId(); } else { AppboyLogger.d(TAG, "Setting small icon for notification via resource id"); } notificationBuilder.setSmallIcon(smallNotificationIconResourceId); return smallNotificationIconResourceId; } /** * Set large icon for devices on Honeycomb and above. We use the large icon URL if it exists in * the notificationExtras. Otherwise we search for a drawable defined in appboy.xml. If that * doesn't exists, we do nothing. * <p/> * Supported HoneyComb+. * * @return whether a large icon was successfully set. */ public static boolean setLargeIconIfPresentAndSupported(Context context, AppboyConfigurationProvider appConfigurationProvider, NotificationCompat.Builder notificationBuilder, Bundle notificationExtras) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) { AppboyLogger.d(TAG, "Setting large icon for notification not supported on this android version"); return false; } try { if (notificationExtras != null && notificationExtras.containsKey(Constants.APPBOY_PUSH_LARGE_ICON_KEY)) { AppboyLogger.d(TAG, "Setting large icon for notification"); String bitmapUrl = notificationExtras.getString(Constants.APPBOY_PUSH_LARGE_ICON_KEY); Bitmap largeNotificationBitmap = AppboyImageUtils.getBitmap(context, Uri.parse(bitmapUrl), AppboyViewBounds.NOTIFICATION_LARGE_ICON); notificationBuilder.setLargeIcon(largeNotificationBitmap); return true; } AppboyLogger.d(TAG, "Large icon bitmap url not present in extras. Attempting to use resource id instead."); int largeNotificationIconResourceId = appConfigurationProvider .getLargeNotificationIconResourceId(); if (largeNotificationIconResourceId != 0) { Bitmap largeNotificationBitmap = BitmapFactory.decodeResource(context.getResources(), largeNotificationIconResourceId); notificationBuilder.setLargeIcon(largeNotificationBitmap); return true; } else { AppboyLogger.d(TAG, "Large icon resource id not present for notification"); } } catch (Exception e) { AppboyLogger.e(TAG, "Error setting large notification icon", e); } AppboyLogger.d(TAG, "Large icon not set for notification"); return false; } /** * In devices running Honeycomb+ notifications can optionally include a sound to play when the notification is delivered. * <p/> * Supported HoneyComb+. */ public static void setSoundIfPresentAndSupported(NotificationCompat.Builder notificationBuilder, Bundle notificationExtras) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { if (notificationExtras != null && notificationExtras.containsKey(Constants.APPBOY_PUSH_NOTIFICATION_SOUND_KEY)) { // Retrieve sound uri if included in notificationExtras bundle. String soundUri = notificationExtras.getString(Constants.APPBOY_PUSH_NOTIFICATION_SOUND_KEY); if (soundUri != null) { if (soundUri.equals(Constants.APPBOY_PUSH_NOTIFICATION_SOUND_DEFAULT_VALUE)) { AppboyLogger.d(TAG, "Setting default sound for notification."); notificationBuilder.setDefaults(Notification.DEFAULT_SOUND); } else { AppboyLogger.d(TAG, "Setting sound for notification via uri."); notificationBuilder.setSound(Uri.parse(soundUri)); } } } else { AppboyLogger.d(TAG, "Sound key not present in notification extras. Not setting sound for notification."); } } else { AppboyLogger.d(TAG, "Notification sound not supported on this android version. Not setting sound for notification."); } } /** * Sets the subText of the notification if a summary is present in the notification extras. * <p/> * Supported on JellyBean+. */ public static void setSummaryTextIfPresentAndSupported(NotificationCompat.Builder notificationBuilder, Bundle notificationExtras) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { if (notificationExtras != null && notificationExtras.containsKey(Constants.APPBOY_PUSH_SUMMARY_TEXT_KEY)) { // Retrieve summary text if included in notificationExtras bundle. String summaryText = notificationExtras.getString(Constants.APPBOY_PUSH_SUMMARY_TEXT_KEY); if (summaryText != null) { AppboyLogger.d(TAG, "Setting summary text for notification"); notificationBuilder.setSubText(summaryText); } } else { AppboyLogger.d(TAG, "Summary text not present in notification extras. Not setting summary text for notification."); } } } /** * Sets the priority of the notification if a priority is present in the notification extras. * <p/> * Supported JellyBean+. */ public static void setPriorityIfPresentAndSupported(NotificationCompat.Builder notificationBuilder, Bundle notificationExtras) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { if (notificationExtras != null) { AppboyLogger.d(TAG, "Setting priority for notification"); notificationBuilder.setPriority(AppboyNotificationUtils.getNotificationPriority(notificationExtras)); } } } /** * Sets the style of the notification if supported. * <p/> * If there is an image url found in the extras payload and the image can be downloaded, then * use the android BigPictureStyle as the notification. Else, use the BigTextStyle instead. * <p/> * Supported JellyBean+. */ public static void setStyleIfSupported(Context context, NotificationCompat.Builder notificationBuilder, Bundle notificationExtras, Bundle appboyExtras) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { if (notificationExtras != null) { AppboyLogger.d(TAG, "Setting style for notification"); NotificationCompat.Style style = AppboyNotificationStyleFactory.getBigNotificationStyle(context, notificationExtras, appboyExtras); notificationBuilder.setStyle(style); } } } /** * Set accent color for devices on Lollipop and above. We use the push-specific accent color if it exists in the notificationExtras, * otherwise we search for a default set in appboy.xml or don't set the color at all (and the system notification gray * default is used). * <p/> * Supported Lollipop+. */ public static void setAccentColorIfPresentAndSupported(AppboyConfigurationProvider appConfigurationProvider, NotificationCompat.Builder notificationBuilder, Bundle notificationExtras) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { if (notificationExtras != null && notificationExtras.containsKey(Constants.APPBOY_PUSH_ACCENT_KEY)) { // Color is an unsigned integer, so we first parse it as a long. AppboyLogger.d(TAG, "Using accent color for notification from extras bundle"); notificationBuilder.setColor((int) Long.parseLong(notificationExtras.getString(Constants.APPBOY_PUSH_ACCENT_KEY))); } else { AppboyLogger.d(TAG, "Using default accent color for notification"); notificationBuilder.setColor(appConfigurationProvider.getDefaultNotificationAccentColor()); } } } /** * Set category for devices on Lollipop and above. Category is one of the predefined notification categories (see the CATEGORY_* constants in Notification) * that best describes a Notification. May be used by the system for ranking and filtering. * <p/> * Supported Lollipop+. */ public static void setCategoryIfPresentAndSupported(NotificationCompat.Builder notificationBuilder, Bundle notificationExtras) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { if (notificationExtras != null && notificationExtras.containsKey(Constants.APPBOY_PUSH_CATEGORY_KEY)) { AppboyLogger.d(TAG, "Setting category for notification"); String notificationCategory = notificationExtras.getString(Constants.APPBOY_PUSH_CATEGORY_KEY); notificationBuilder.setCategory(notificationCategory); } else { AppboyLogger.d(TAG, "Category not present in notification extras. Not setting category for notification."); } } else { AppboyLogger.d(TAG, "Notification category not supported on this android version. Not setting category for notification."); } } /** * Set visibility for devices on Lollipop and above. * <p/> * Sphere of visibility of this notification, which affects how and when the SystemUI reveals the notification's presence and * contents in untrusted situations (namely, on the secure lockscreen). The default level, VISIBILITY_PRIVATE, behaves exactly * as notifications have always done on Android: The notification's icon and tickerText (if available) are shown in all situations, * but the contents are only available if the device is unlocked for the appropriate user. A more permissive policy can be expressed * by VISIBILITY_PUBLIC; such a notification can be read even in an "insecure" context (that is, above a secure lockscreen). * To modify the public version of this notification—for example, to redact some portions—see setPublicVersion(Notification). * Finally, a notification can be made VISIBILITY_SECRET, which will suppress its icon and ticker until the user has bypassed the lockscreen. * <p/> * Supported Lollipop+. */ public static void setVisibilityIfPresentAndSupported(NotificationCompat.Builder notificationBuilder, Bundle notificationExtras) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { if (notificationExtras != null && notificationExtras.containsKey(Constants.APPBOY_PUSH_VISIBILITY_KEY)) { try { int visibility = Integer.parseInt(notificationExtras.getString(Constants.APPBOY_PUSH_VISIBILITY_KEY)); if (isValidNotificationVisibility(visibility)) { AppboyLogger.d(TAG, "Setting visibility for notification"); notificationBuilder.setVisibility(visibility); } else { AppboyLogger.e(TAG, String.format("Received invalid notification visibility %d", visibility)); } } catch (Exception e) { AppboyLogger.e(TAG, "Failed to parse visibility from notificationExtras", e); } } } else { AppboyLogger.d(TAG, "Notification visibility not supported on this android version. Not setting visibility for notification."); } } /** * Set the public version of the notification for notifications with private visibility. * <p/> * Supported Lollipop+. */ public static void setPublicVersionIfPresentAndSupported(Context context, AppboyConfigurationProvider appboyConfigurationProvider, NotificationCompat.Builder notificationBuilder, Bundle notificationExtras) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { if (notificationExtras != null && notificationExtras.containsKey(Constants.APPBOY_PUSH_PUBLIC_NOTIFICATION_KEY)) { String publicNotificationExtrasString = notificationExtras.getString(Constants.APPBOY_PUSH_PUBLIC_NOTIFICATION_KEY); Bundle publicNotificationExtras = parseJSONStringDictionaryIntoBundle(publicNotificationExtrasString); NotificationCompat.Builder publicNotificationBuilder = new NotificationCompat.Builder(context); setContentIfPresent(publicNotificationBuilder, publicNotificationExtras); setTitleIfPresent(publicNotificationBuilder, publicNotificationExtras); setSummaryTextIfPresentAndSupported(publicNotificationBuilder, publicNotificationExtras); setSmallIcon(appboyConfigurationProvider, publicNotificationBuilder); setAccentColorIfPresentAndSupported(appboyConfigurationProvider, publicNotificationBuilder, publicNotificationExtras); notificationBuilder.setPublicVersion(publicNotificationBuilder.build()); } } } /** * Checks whether the given integer value is a valid Android notification visibility constant. */ @TargetApi(Build.VERSION_CODES.LOLLIPOP) public static boolean isValidNotificationVisibility(int visibility) { return (visibility == Notification.VISIBILITY_SECRET || visibility == Notification.VISIBILITY_PRIVATE || visibility == Notification.VISIBILITY_PUBLIC); } /** * Logs a notification click with Appboy if the extras passed down * indicate that they are from Appboy and contain a campaign Id. * <p/> * An Appboy session must be active to log a push notification. * * @param customContentString extra key value pairs in JSON format. */ public static void logBaiduNotificationClick(Context context, String customContentString) { if (customContentString == null) { AppboyLogger.w(TAG, "customContentString was null. Doing nothing."); return; } try { JSONObject jsonExtras = new JSONObject(customContentString); String source = jsonExtras.optString(SOURCE_KEY, null); String campaignId = jsonExtras.optString(Constants.APPBOY_PUSH_CAMPAIGN_ID_KEY, null); if (source != null && Constants.APPBOY.equals(source) && campaignId != null) { Appboy.getInstance(context).logPushNotificationOpened(campaignId); } } catch (Exception e) { AppboyLogger.e(TAG, String.format("Caught an exception processing customContentString: %s", customContentString), e); } } /** * Handles a request to cancel a push notification in the notification center. Called by GCM/ADM receiver when an * Appboy cancel notification intent is received. * <p/> * Any existing notification in the notification center with the integer Id specified in the * "nid" field of the provided intent's extras is cancelled. * <p/> * If no Id is found, the defaut Appboy notification Id is used. * * @param context * @param intent the cancel notification intent */ public static void handleCancelNotificationAction(Context context, Intent intent) { try { if (intent.hasExtra(Constants.APPBOY_PUSH_NOTIFICATION_ID)) { int notificationId = intent.getIntExtra(Constants.APPBOY_PUSH_NOTIFICATION_ID, Constants.APPBOY_DEFAULT_NOTIFICATION_ID); AppboyLogger.d(TAG, String.format("Cancelling notification action with id: %d", notificationId)); NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); notificationManager.cancel(Constants.APPBOY_PUSH_NOTIFICATION_TAG, notificationId); } } catch (Exception e) { AppboyLogger.e(TAG, "Exception occurred handling cancel notification intent.", e); } } /** * Creates a request to cancel a push notification in the notification center. * <p/> * Sends an intent to the GCM/ADM receiver requesting Appboy to cancel the notification with * the specified notification Id. * <p/> * See {@link #handleCancelNotificationAction} * * @param context * @param notificationId */ public static void cancelNotification(Context context, int notificationId) { try { AppboyLogger.d(TAG, String.format("Cancelling notification action with id: %d", notificationId)); Intent cancelNotificationIntent = new Intent(Constants.APPBOY_CANCEL_NOTIFICATION_ACTION).setClass(context, AppboyNotificationUtils.getNotificationReceiverClass()); cancelNotificationIntent.putExtra(Constants.APPBOY_PUSH_NOTIFICATION_ID, notificationId); context.sendBroadcast(cancelNotificationIntent); } catch (Exception e) { AppboyLogger.e(TAG, "Exception occurred attempting to cancel notification.", e); } } /** * @return the Class of the notification receiver used by this application. */ public static Class<?> getNotificationReceiverClass() { if (Constants.IS_AMAZON) { return AppboyAdmReceiver.class; } else { return AppboyGcmReceiver.class; } } /** * Returns true if the bundle is from a push sent by Appboy for uninstall tracking. * <p/> * Uninstall tracking push messages are content-only (i.e. non-display) push messages. You can use this * method to detect that a push is from an Appboy uninstall tracking push and ensure your broadcast receiver * doesn't take any actions it shouldn't if a content push is from Appboy uninstall tracking (e.g. don't ping your server * for content on Appboy uninstall push). * * @param notificationExtras A notificationExtras bundle that is passed with the push recieved intent when a GCM/ADM message is * received, and that Appboy passes in the intent to registered receivers. */ public static boolean isUninstallTrackingPush(Bundle notificationExtras) { if (notificationExtras != null) { // The ADM case where extras are flattened if (notificationExtras.containsKey(Constants.APPBOY_PUSH_UNINSTALL_TRACKING_KEY)) { return true; } // The GCM case where extras are in a separate bundle Bundle appboyExtras = notificationExtras.getBundle(Constants.APPBOY_PUSH_EXTRAS_KEY); if (appboyExtras != null) { return appboyExtras.containsKey(Constants.APPBOY_PUSH_UNINSTALL_TRACKING_KEY); } } return false; } /** * Returns the specified String resource if it is found; otherwise it returns the defaultString. */ static String getOptionalStringResource(Resources resources, int stringResourceId, String defaultString) { try { return resources.getString(stringResourceId); } catch (Resources.NotFoundException e) { return defaultString; } } /** * Sends a push notification opened broadcast to the client broadcast receiver. * The broadcast message action is <host-app-package-name>.intent.APPBOY_NOTIFICATION_OPENED. * * @param context * @param intent the internal notification clicked intent constructed in * {@link #setContentIntentIfPresent} */ static void sendNotificationOpenedBroadcast(Context context, Intent intent) { AppboyLogger.d(TAG, "Sending notification opened broadcast"); String pushOpenedAction = context.getPackageName() + AppboyNotificationUtils.APPBOY_NOTIFICATION_OPENED_SUFFIX; Intent pushOpenedIntent = new Intent(pushOpenedAction); if (intent.getExtras() != null) { pushOpenedIntent.putExtras(intent.getExtras()); } context.sendBroadcast(pushOpenedIntent); } /** * Logs a push notification open. * * @param context * @param intent the internal notification clicked intent constructed in * {@link #setContentIntentIfPresent} */ private static void logNotificationOpened(Context context, Intent intent) { Appboy.getInstance(context).logPushNotificationOpened(intent); } }