org.chromium.chrome.browser.ntp.ContentSuggestionsNotificationHelper.java Source code

Java tutorial

Introduction

Here is the source code for org.chromium.chrome.browser.ntp.ContentSuggestionsNotificationHelper.java

Source

// 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);
}