Java tutorial
// Copyright 2016 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. package org.chromium.chrome.browser.ntp; import android.app.AlarmManager; import android.app.NotificationManager; import android.app.PendingIntent; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.graphics.Bitmap; import android.net.Uri; import android.provider.Browser; import android.support.v4.app.NotificationCompat; import org.chromium.base.ContextUtils; import org.chromium.base.annotations.CalledByNative; import org.chromium.base.annotations.JNINamespace; import org.chromium.base.library_loader.LibraryLoader; import org.chromium.chrome.R; import org.chromium.chrome.browser.IntentHandler; import org.chromium.chrome.browser.ShortcutHelper; import org.chromium.chrome.browser.document.ChromeLauncherActivity; import org.chromium.chrome.browser.ntp.snippets.ContentSuggestionsNotificationAction; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.Set; /** * Provides functionality needed for content suggestion notifications. * * Exposes helper functions to native C++ code. */ @JNINamespace("ntp_snippets") public class ContentSuggestionsNotificationHelper { private static final String NOTIFICATION_TAG = "ContentSuggestionsNotification"; private static final String NOTIFICATION_ID_EXTRA = "notification_id"; private static final String NOTIFICATION_CATEGORY_EXTRA = "category"; private static final String NOTIFICATION_ID_WITHIN_CATEGORY_EXTRA = "id_within_category"; private static final String PREF_CACHED_ACTION_TAP = "ntp.content_suggestions.notification.cached_action_tap"; private static final String PREF_CACHED_ACTION_DISMISSAL = "ntp.content_suggestions.notification.cached_action_dismissal"; private static final String PREF_CACHED_ACTION_HIDE_DEADLINE = "ntp.content_suggestions.notification.cached_action_hide_deadline"; private static final String PREF_CACHED_ACTION_HIDE_EXPIRY = "ntp.content_suggestions.notification.cached_action_hide_expiry"; private static final String PREF_CACHED_ACTION_HIDE_FRONTMOST = "ntp.content_suggestions.notification.cached_action_hide_frontmost"; private static final String PREF_CACHED_ACTION_HIDE_DISABLED = "ntp.content_suggestions.notification.cached_action_hide_disabled"; private static final String PREF_CACHED_ACTION_HIDE_SHUTDOWN = "ntp.content_suggestions.notification.cached_action_hide_shutdown"; private static final String PREF_CACHED_CONSECUTIVE_IGNORED = "ntp.content_suggestions.notification.cached_consecutive_ignored"; // Tracks which URIs there is an active notification for. private static final String PREF_ACTIVE_NOTIFICATIONS = "ntp.content_suggestions.notification.active"; private ContentSuggestionsNotificationHelper() { } // Prevent instantiation /** * Opens the content suggestion when notification is tapped. */ public static final class OpenUrlReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { int category = intent.getIntExtra(NOTIFICATION_CATEGORY_EXTRA, -1); String idWithinCategory = intent.getStringExtra(NOTIFICATION_ID_WITHIN_CATEGORY_EXTRA); openUrl(intent.getData()); recordCachedActionMetric(ContentSuggestionsNotificationAction.CONTENT_SUGGESTIONS_TAP); removeActiveNotification(category, idWithinCategory); } } /** * Records dismissal when notification is swiped away. */ public static final class DeleteReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { int category = intent.getIntExtra(NOTIFICATION_CATEGORY_EXTRA, -1); String idWithinCategory = intent.getStringExtra(NOTIFICATION_ID_WITHIN_CATEGORY_EXTRA); recordCachedActionMetric(ContentSuggestionsNotificationAction.CONTENT_SUGGESTIONS_DISMISSAL); removeActiveNotification(category, idWithinCategory); } } /** * Removes the notification after a timeout period. */ public static final class TimeoutReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { int category = intent.getIntExtra(NOTIFICATION_CATEGORY_EXTRA, -1); String idWithinCategory = intent.getStringExtra(NOTIFICATION_ID_WITHIN_CATEGORY_EXTRA); if (findActiveNotification(category, idWithinCategory) == null) { return; // tapped or swiped } hideNotification(category, idWithinCategory, ContentSuggestionsNotificationAction.CONTENT_SUGGESTIONS_HIDE_DEADLINE); } } private static void openUrl(Uri uri) { Context context = ContextUtils.getApplicationContext(); Intent intent = new Intent().setAction(Intent.ACTION_VIEW).setData(uri) .setClass(context, ChromeLauncherActivity.class).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) .putExtra(Browser.EXTRA_APPLICATION_ID, context.getPackageName()) .putExtra(ShortcutHelper.REUSE_URL_MATCHING_TAB_ELSE_NEW_TAB, true); IntentHandler.addTrustedIntentExtras(intent); context.startActivity(intent); } @CalledByNative private static boolean showNotification(int category, String idWithinCategory, String url, String title, String text, Bitmap image, long timeoutAtMillis) { if (findActiveNotification(category, idWithinCategory) != null) return false; // Post notification. Context context = ContextUtils.getApplicationContext(); NotificationManager manager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); int nextId = nextNotificationId(); Uri uri = Uri.parse(url); Intent contentIntent = new Intent(context, OpenUrlReceiver.class).setData(uri) .putExtra(NOTIFICATION_CATEGORY_EXTRA, category) .putExtra(NOTIFICATION_ID_WITHIN_CATEGORY_EXTRA, idWithinCategory); Intent deleteIntent = new Intent(context, DeleteReceiver.class).setData(uri) .putExtra(NOTIFICATION_CATEGORY_EXTRA, category) .putExtra(NOTIFICATION_ID_WITHIN_CATEGORY_EXTRA, idWithinCategory); NotificationCompat.Builder builder = new NotificationCompat.Builder(context).setAutoCancel(true) .setContentIntent(PendingIntent.getBroadcast(context, 0, contentIntent, 0)) .setDeleteIntent(PendingIntent.getBroadcast(context, 0, deleteIntent, 0)).setContentTitle(title) .setContentText(text).setGroup(NOTIFICATION_TAG).setDefaults(NotificationCompat.DEFAULT_LIGHTS) .setPriority(-1).setLargeIcon(image).setSmallIcon(R.drawable.ic_chrome); manager.notify(NOTIFICATION_TAG, nextId, builder.build()); addActiveNotification(new ActiveNotification(nextId, category, idWithinCategory, uri)); // Set timeout. if (timeoutAtMillis != Long.MAX_VALUE) { AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); Intent timeoutIntent = new Intent(context, TimeoutReceiver.class).setData(Uri.parse(url)) .putExtra(NOTIFICATION_ID_EXTRA, nextId).putExtra(NOTIFICATION_CATEGORY_EXTRA, category) .putExtra(NOTIFICATION_ID_WITHIN_CATEGORY_EXTRA, idWithinCategory); alarmManager.set(AlarmManager.RTC, timeoutAtMillis, PendingIntent.getBroadcast(context, 0, timeoutIntent, PendingIntent.FLAG_UPDATE_CURRENT)); } return true; } @CalledByNative private static void hideNotification(int category, String idWithinCategory, int why) { Context context = ContextUtils.getApplicationContext(); NotificationManager manager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); ActiveNotification activeNotification = findActiveNotification(category, idWithinCategory); if (activeNotification == null) return; manager.cancel(NOTIFICATION_TAG, activeNotification.mId); if (removeActiveNotification(category, idWithinCategory)) { recordCachedActionMetric(why); } } @CalledByNative private static void hideAllNotifications(int why) { Context context = ContextUtils.getApplicationContext(); NotificationManager manager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); for (ActiveNotification activeNotification : getActiveNotifications()) { manager.cancel(NOTIFICATION_TAG, activeNotification.mId); recordCachedActionMetric(why); } } private static class ActiveNotification { final int mId; final int mCategory; final String mIdWithinCategory; final Uri mUri; ActiveNotification(int id, int category, String idWithinCategory, Uri uri) { mId = id; mCategory = category; mIdWithinCategory = idWithinCategory; mUri = uri; } /** Parses the fields out of a chrome://content-suggestions-notification URI */ static ActiveNotification fromUri(Uri notificationUri) { assert notificationUri.getScheme().equals("chrome"); assert notificationUri.getAuthority().equals("content-suggestions-notification"); assert notificationUri.getQueryParameter("id") != null; assert notificationUri.getQueryParameter("category") != null; assert notificationUri.getQueryParameter("idWithinCategory") != null; assert notificationUri.getQueryParameter("uri") != null; return new ActiveNotification(Integer.parseInt(notificationUri.getQueryParameter("id")), Integer.parseInt(notificationUri.getQueryParameter("category")), notificationUri.getQueryParameter("idWithinCategory"), Uri.parse(notificationUri.getQueryParameter("uri"))); } /** Serializes the fields to a chrome://content-suggestions-notification URI */ Uri toUri() { return new Uri.Builder().scheme("chrome").authority("content-suggestions-notification") .appendQueryParameter("id", Integer.toString(mId)) .appendQueryParameter("category", Integer.toString(mCategory)) .appendQueryParameter("idWithinCategory", mIdWithinCategory) .appendQueryParameter("uri", mUri.toString()).build(); } } /** Returns a mutable copy of the named pref. Never returns null. */ private static Set<String> getMutableStringSetPreference(SharedPreferences prefs, String prefName) { Set<String> prefValue = prefs.getStringSet(prefName, null); if (prefValue == null) { return new HashSet<String>(); } return new HashSet<String>(prefValue); } private static void addActiveNotification(ActiveNotification notification) { SharedPreferences prefs = ContextUtils.getAppSharedPreferences(); Set<String> activeNotifications = getMutableStringSetPreference(prefs, PREF_ACTIVE_NOTIFICATIONS); activeNotifications.add(notification.toUri().toString()); prefs.edit().putStringSet(PREF_ACTIVE_NOTIFICATIONS, activeNotifications).apply(); } private static boolean removeActiveNotification(int category, String idWithinCategory) { SharedPreferences prefs = ContextUtils.getAppSharedPreferences(); ActiveNotification notification = findActiveNotification(category, idWithinCategory); if (notification == null) return false; Set<String> activeNotifications = getMutableStringSetPreference(prefs, PREF_ACTIVE_NOTIFICATIONS); boolean result = activeNotifications.remove(notification.toUri().toString()); prefs.edit().putStringSet(PREF_ACTIVE_NOTIFICATIONS, activeNotifications).apply(); return result; } private static Collection<ActiveNotification> getActiveNotifications() { SharedPreferences prefs = ContextUtils.getAppSharedPreferences(); Set<String> activeNotifications = prefs.getStringSet(PREF_ACTIVE_NOTIFICATIONS, null); if (activeNotifications == null) return Collections.emptySet(); Set<ActiveNotification> result = new HashSet<ActiveNotification>(); for (String serialized : activeNotifications) { Uri notificationUri = Uri.parse(serialized); ActiveNotification activeNotification = ActiveNotification.fromUri(notificationUri); if (activeNotification != null) result.add(activeNotification); } return result; } /** Returns an ActiveNotification if a corresponding one is found, otherwise null. */ private static ActiveNotification findActiveNotification(int category, String idWithinCategory) { SharedPreferences prefs = ContextUtils.getAppSharedPreferences(); Set<String> activeNotifications = prefs.getStringSet(PREF_ACTIVE_NOTIFICATIONS, null); if (activeNotifications == null) return null; for (String serialized : activeNotifications) { Uri notificationUri = Uri.parse(serialized); ActiveNotification activeNotification = ActiveNotification.fromUri(notificationUri); if ((activeNotification != null) && (activeNotification.mCategory == category) && (activeNotification.mIdWithinCategory.equals(idWithinCategory))) { return activeNotification; } } return null; } /** Returns a non-negative integer greater than any active notification's notification ID. */ private static int nextNotificationId() { SharedPreferences prefs = ContextUtils.getAppSharedPreferences(); Set<String> activeNotifications = prefs.getStringSet(PREF_ACTIVE_NOTIFICATIONS, null); if (activeNotifications == null) return 0; int nextId = 0; for (String serialized : activeNotifications) { Uri notificationUri = Uri.parse(serialized); ActiveNotification activeNotification = ActiveNotification.fromUri(notificationUri); if ((activeNotification != null) && (activeNotification.mId >= nextId)) { nextId = activeNotification.mId + 1; } } return nextId; } private static String cachedMetricNameForAction( @ContentSuggestionsNotificationAction.ContentSuggestionsNotificationActionEnum int action) { switch (action) { case ContentSuggestionsNotificationAction.CONTENT_SUGGESTIONS_TAP: return PREF_CACHED_ACTION_TAP; case ContentSuggestionsNotificationAction.CONTENT_SUGGESTIONS_DISMISSAL: return PREF_CACHED_ACTION_DISMISSAL; case ContentSuggestionsNotificationAction.CONTENT_SUGGESTIONS_HIDE_DEADLINE: return PREF_CACHED_ACTION_HIDE_DEADLINE; case ContentSuggestionsNotificationAction.CONTENT_SUGGESTIONS_HIDE_EXPIRY: return PREF_CACHED_ACTION_HIDE_EXPIRY; case ContentSuggestionsNotificationAction.CONTENT_SUGGESTIONS_HIDE_FRONTMOST: return PREF_CACHED_ACTION_HIDE_FRONTMOST; case ContentSuggestionsNotificationAction.CONTENT_SUGGESTIONS_HIDE_DISABLED: return PREF_CACHED_ACTION_HIDE_DISABLED; case ContentSuggestionsNotificationAction.CONTENT_SUGGESTIONS_HIDE_SHUTDOWN: return PREF_CACHED_ACTION_HIDE_SHUTDOWN; } return ""; } /** * Records that an action was performed on a notification. * * Also tracks the number of consecutively-ignored notifications, resetting it on a tap or * otherwise incrementing it. * * This method may be called when the native library is not loaded. If it is loaded, the metrics * will immediately be sent to C++. If not, it will cache them for a later call to * flushCachedMetrics(). * * @param action The action to update the pref for. */ private static void recordCachedActionMetric( @ContentSuggestionsNotificationAction.ContentSuggestionsNotificationActionEnum int action) { String prefName = cachedMetricNameForAction(action); assert !prefName.isEmpty(); SharedPreferences prefs = ContextUtils.getAppSharedPreferences(); int currentValue = prefs.getInt(prefName, 0); int consecutiveIgnored = prefs.getInt(PREF_CACHED_CONSECUTIVE_IGNORED, 0); switch (action) { case ContentSuggestionsNotificationAction.CONTENT_SUGGESTIONS_TAP: consecutiveIgnored = 0; break; case ContentSuggestionsNotificationAction.CONTENT_SUGGESTIONS_DISMISSAL: case ContentSuggestionsNotificationAction.CONTENT_SUGGESTIONS_HIDE_DEADLINE: case ContentSuggestionsNotificationAction.CONTENT_SUGGESTIONS_HIDE_EXPIRY: ++consecutiveIgnored; break; case ContentSuggestionsNotificationAction.CONTENT_SUGGESTIONS_HIDE_FRONTMOST: case ContentSuggestionsNotificationAction.CONTENT_SUGGESTIONS_HIDE_DISABLED: case ContentSuggestionsNotificationAction.CONTENT_SUGGESTIONS_HIDE_SHUTDOWN: break; // no change } prefs.edit().putInt(prefName, currentValue + 1).putInt(PREF_CACHED_CONSECUTIVE_IGNORED, consecutiveIgnored) .apply(); if (LibraryLoader.isInitialized()) { flushCachedMetrics(); } } /** * Invokes nativeReceiveFlushedMetrics() with cached metrics and resets them. * * It may be called from either native or Java code, as long as the native libray is loaded. */ @CalledByNative private static void flushCachedMetrics() { assert LibraryLoader.isInitialized(); SharedPreferences prefs = ContextUtils.getAppSharedPreferences(); int tapCount = prefs.getInt(PREF_CACHED_ACTION_TAP, 0); int dismissalCount = prefs.getInt(PREF_CACHED_ACTION_DISMISSAL, 0); int hideDeadlineCount = prefs.getInt(PREF_CACHED_ACTION_HIDE_DEADLINE, 0); int hideExpiryCount = prefs.getInt(PREF_CACHED_ACTION_HIDE_EXPIRY, 0); int hideFrontmostCount = prefs.getInt(PREF_CACHED_ACTION_HIDE_FRONTMOST, 0); int hideDisabledCount = prefs.getInt(PREF_CACHED_ACTION_HIDE_DISABLED, 0); int hideShutdownCount = prefs.getInt(PREF_CACHED_ACTION_HIDE_SHUTDOWN, 0); int consecutiveIgnored = prefs.getInt(PREF_CACHED_CONSECUTIVE_IGNORED, 0); if (tapCount > 0 || dismissalCount > 0 || hideDeadlineCount > 0 || hideExpiryCount > 0 || hideFrontmostCount > 0 || hideDisabledCount > 0 || hideShutdownCount > 0) { nativeReceiveFlushedMetrics(tapCount, dismissalCount, hideDeadlineCount, hideExpiryCount, hideFrontmostCount, hideDisabledCount, hideShutdownCount, consecutiveIgnored); prefs.edit().remove(PREF_CACHED_ACTION_TAP).remove(PREF_CACHED_ACTION_DISMISSAL) .remove(PREF_CACHED_ACTION_HIDE_DEADLINE).remove(PREF_CACHED_ACTION_HIDE_EXPIRY) .remove(PREF_CACHED_ACTION_HIDE_FRONTMOST).remove(PREF_CACHED_ACTION_HIDE_DISABLED) .remove(PREF_CACHED_ACTION_HIDE_SHUTDOWN).remove(PREF_CACHED_CONSECUTIVE_IGNORED).apply(); } } private static native void nativeReceiveFlushedMetrics(int tapCount, int dismissalCount, int hideDeadlineCount, int hideExpiryCount, int hideFrontmostCount, int hideDisabledCount, int hideShutdownCount, int consecutiveIgnored); }