/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*- * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ package org.mozilla.gecko.notifications; import android.app.Notification; import android.app.PendingIntent; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.graphics.Bitmap; import android.net.Uri; import android.support.v4.app.NotificationCompat; import android.support.v4.app.NotificationManagerCompat; import android.util.Log; import java.util.HashMap; import org.mozilla.gecko.AppConstants; import org.mozilla.gecko.GeckoApp; import org.mozilla.gecko.GeckoAppShell; import org.mozilla.gecko.GeckoService; import org.mozilla.gecko.NotificationListener; import org.mozilla.gecko.R; import org.mozilla.gecko.gfx.BitmapUtils; /** * Client for posting notifications. */ public final class NotificationClient implements NotificationListener { private static final String LOGTAG = "GeckoNotificationClient"; /* package */ static final String CLICK_ACTION = AppConstants.ANDROID_PACKAGE_NAME + ".NOTIFICATION_CLICK"; /* package */ static final String CLOSE_ACTION = AppConstants.ANDROID_PACKAGE_NAME + ".NOTIFICATION_CLOSE"; /* package */ static final String PERSISTENT_INTENT_EXTRA = "persistentIntent"; private final Context mContext; private final NotificationManagerCompat mNotificationManager; private final HashMap mNotifications = new HashMap<>(); /** * Notification associated with this service's foreground state. * * {@link android.app.Service#startForeground(int, android.app.Notification)} * associates the foreground with exactly one notification from the service. * To keep Fennec alive during downloads (and to make sure it can be killed * once downloads are complete), we make sure that the foreground is always * associated with an active progress notification if and only if at least * one download is in progress. */ private String mForegroundNotification; public NotificationClient(Context context) { mContext = context.getApplicationContext(); mNotificationManager = NotificationManagerCompat.from(mContext); } @Override // NotificationListener public void showNotification(String name, String cookie, String title, String text, String host, String imageUrl) { showNotification(name, cookie, title, text, host, imageUrl, /* data */ null); } @Override // NotificationListener public void showPersistentNotification(String name, String cookie, String title, String text, String host, String imageUrl, String data) { showNotification(name, cookie, title, text, host, imageUrl, data != null ? data : ""); } private void showNotification(String name, String cookie, String title, String text, String host, String imageUrl, String persistentData) { // Put the strings into the intent as an URI // "alert:?name=&cookie=" String packageName = AppConstants.ANDROID_PACKAGE_NAME; String className = AppConstants.MOZ_ANDROID_BROWSER_INTENT_CLASS; if (GeckoAppShell.getGeckoInterface() != null) { final ComponentName comp = GeckoAppShell.getGeckoInterface() .getActivity().getComponentName(); packageName = comp.getPackageName(); className = comp.getClassName(); } final Uri dataUri = (new Uri.Builder()) .scheme("moz-notification") .authority(packageName) .path(className) .appendQueryParameter("name", name) .appendQueryParameter("cookie", cookie) .build(); final Intent clickIntent = new Intent(CLICK_ACTION); clickIntent.setClass(mContext, NotificationReceiver.class); clickIntent.setData(dataUri); if (persistentData != null) { final Intent persistentIntent = GeckoService.getIntentToCreateServices( mContext, "persistent-notification-click", persistentData); clickIntent.putExtra(PERSISTENT_INTENT_EXTRA, persistentIntent); } final PendingIntent clickPendingIntent = PendingIntent.getBroadcast( mContext, 0, clickIntent, PendingIntent.FLAG_UPDATE_CURRENT); final Intent closeIntent = new Intent(CLOSE_ACTION); closeIntent.setClass(mContext, NotificationReceiver.class); closeIntent.setData(dataUri); if (persistentData != null) { final Intent persistentIntent = GeckoService.getIntentToCreateServices( mContext, "persistent-notification-close", persistentData); closeIntent.putExtra(PERSISTENT_INTENT_EXTRA, persistentIntent); } final PendingIntent closePendingIntent = PendingIntent.getBroadcast( mContext, 0, closeIntent, PendingIntent.FLAG_UPDATE_CURRENT); add(name, imageUrl, host, title, text, clickPendingIntent, closePendingIntent); GeckoAppShell.onNotificationShow(name, cookie); } @Override // NotificationListener public void closeNotification(String name) { remove(name); } /** * Adds a notification; used for web notifications. * * @param name the unique name of the notification * @param imageUrl URL of the image to use * @param alertTitle title of the notification * @param alertText text of the notification * @param contentIntent Intent used when the notification is clicked * @param deleteIntent Intent used when the notification is closed */ private void add(final String name, final String imageUrl, final String host, final String alertTitle, final String alertText, final PendingIntent contentIntent, final PendingIntent deleteIntent) { final NotificationCompat.Builder builder = new NotificationCompat.Builder(mContext) .setContentTitle(alertTitle) .setContentText(alertText) .setSmallIcon(R.drawable.ic_status_logo) .setContentIntent(contentIntent) .setDeleteIntent(deleteIntent) .setAutoCancel(true) .setStyle(new NotificationCompat.BigTextStyle() .bigText(alertText) .setSummaryText(host)); // Fetch icon. if (!imageUrl.isEmpty()) { final Bitmap image = BitmapUtils.decodeUrl(imageUrl); builder.setLargeIcon(image); } builder.setWhen(System.currentTimeMillis()); final Notification notification = builder.build(); synchronized (this) { mNotifications.put(name, notification); } mNotificationManager.notify(name, 0, notification); } /** * Adds a notification; used for Fennec app notifications. * * @param name the unique name of the notification * @param notification the Notification to add */ public synchronized void add(final String name, final Notification notification) { final boolean ongoing = isOngoing(notification); if (ongoing != isOngoing(mNotifications.get(name))) { // In order to change notification from ongoing to non-ongoing, or vice versa, // we have to remove the previous notification, because ongoing notifications // use a different id value than non-ongoing notifications. onNotificationClose(name); } mNotifications.put(name, notification); if (!ongoing) { mNotificationManager.notify(name, 0, notification); return; } // Ongoing if (mForegroundNotification == null) { setForegroundNotificationLocked(name, notification); } else if (mForegroundNotification.equals(name)) { // Shortcut to update the current foreground notification, instead of // going through the service. mNotificationManager.notify(R.id.foregroundNotification, notification); } } /** * Updates a notification. * * @param name Name of existing notification * @param progress progress of item being updated * @param progressMax max progress of item being updated * @param alertText text of the notification */ public void update(final String name, final long progress, final long progressMax, final String alertText) { Notification notification; synchronized (this) { notification = mNotifications.get(name); } if (notification == null) { return; } notification = new NotificationCompat.Builder(mContext) .setContentText(alertText) .setSmallIcon(notification.icon) .setWhen(notification.when) .setContentIntent(notification.contentIntent) .setProgress((int) progressMax, (int) progress, false) .build(); add(name, notification); } /* package */ synchronized Notification onNotificationClose(final String name) { mNotificationManager.cancel(name, 0); final Notification notification = mNotifications.remove(name); if (notification != null) { updateForegroundNotificationLocked(name); } return notification; } /** * Removes a notification. * * @param name Name of existing notification */ public synchronized void remove(final String name) { final Notification notification = onNotificationClose(name); if (notification == null || notification.deleteIntent == null) { return; } // Canceling the notification doesn't trigger the delete intent, so we // have to trigger it manually. try { notification.deleteIntent.send(); } catch (final PendingIntent.CanceledException e) { // Ignore. } } /** * Determines whether the service is done. * * The service is considered finished when all notifications have been * removed. * * @return whether all notifications have been removed */ public synchronized boolean isDone() { return mNotifications.isEmpty(); } /** * Determines whether a notification should hold a foreground service to keep Gecko alive * * @param name the name of the notification to check * @return whether the notification is ongoing */ public synchronized boolean isOngoing(final String name) { return isOngoing(mNotifications.get(name)); } /** * Determines whether a notification should hold a foreground service to keep Gecko alive * * @param notification the notification to check * @return whether the notification is ongoing */ public boolean isOngoing(final Notification notification) { if (notification != null && (notification.flags & Notification.FLAG_ONGOING_EVENT) != 0) { return true; } return false; } private void setForegroundNotificationLocked(final String name, final Notification notification) { mForegroundNotification = name; final Intent intent = new Intent(mContext, NotificationService.class); intent.putExtra(NotificationService.EXTRA_NOTIFICATION, notification); mContext.startService(intent); } private void updateForegroundNotificationLocked(final String oldName) { if (mForegroundNotification == null || !mForegroundNotification.equals(oldName)) { return; } // If we're removing the notification associated with the // foreground, we need to pick another active notification to act // as the foreground notification. for (final String name : mNotifications.keySet()) { final Notification notification = mNotifications.get(name); if (isOngoing(notification)) { setForegroundNotificationLocked(name, notification); return; } } setForegroundNotificationLocked(null, null); } }