1 // Copyright 2018 The Chromium Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 package org.chromium.chrome.browser.notifications; 6 7 import android.app.Notification; 8 import android.app.PendingIntent; 9 import android.content.BroadcastReceiver; 10 import android.content.Context; 11 import android.content.Intent; 12 import android.os.Build; 13 14 import androidx.annotation.IntDef; 15 import androidx.annotation.Nullable; 16 17 import org.chromium.base.ContextUtils; 18 import org.chromium.base.Log; 19 import org.chromium.components.browser_ui.notifications.NotificationMetadata; 20 import org.chromium.components.browser_ui.notifications.PendingIntentProvider; 21 22 import java.lang.annotation.Retention; 23 import java.lang.annotation.RetentionPolicy; 24 25 /** 26 * Class to intercept {@link PendingIntent}s from notifications, including 27 * {@link Notification#contentIntent}, {@link Notification.Action#actionIntent} and 28 * {@link Notification#deleteIntent} with broadcast receivers. 29 */ 30 public class NotificationIntentInterceptor { 31 private static final String TAG = "IntentInterceptor"; 32 private static final String EXTRA_PENDING_INTENT = 33 "notifications.NotificationIntentInterceptor.EXTRA_PENDING_INTENT"; 34 private static final String EXTRA_INTENT_TYPE = 35 "notifications.NotificationIntentInterceptor.EXTRA_INTENT_TYPE"; 36 private static final String EXTRA_NOTIFICATION_TYPE = 37 "notifications.NotificationIntentInterceptor.EXTRA_NOTIFICATION_TYPE"; 38 private static final String EXTRA_ACTION_TYPE = 39 "notifications.NotificationIntentInterceptor.EXTRA_ACTION_TYPE"; 40 private static final String EXTRA_CREATE_TIME = 41 "notifications.NotificationIntentInterceptor.EXTRA_CREATE_TIME"; 42 public static final String INTENT_ACTION = 43 "notifications.NotificationIntentInterceptor.INTENT_ACTION"; 44 public static final long INVALID_CREATE_TIME = -1; 45 46 /** 47 * Enum that defines type of notification intent. 48 */ 49 @IntDef({IntentType.UNKNOWN, IntentType.CONTENT_INTENT, IntentType.ACTION_INTENT, 50 IntentType.DELETE_INTENT}) 51 @Retention(RetentionPolicy.SOURCE) 52 public @interface IntentType { 53 int UNKNOWN = -1; 54 int CONTENT_INTENT = 0; 55 int ACTION_INTENT = 1; 56 int DELETE_INTENT = 2; 57 } 58 59 /** 60 * Receives the event when the user taps on the notification body, notification action, or 61 * dismiss notification. 62 * {@link Notification#contentIntent}, {@link Notification#deleteIntent} 63 * {@link Notification.Action#actionIntent} will be delivered to this broadcast receiver. 64 */ 65 public static final class Receiver extends BroadcastReceiver { 66 @Override onReceive(Context context, Intent intent)67 public void onReceive(Context context, Intent intent) { 68 @IntentType 69 int intentType = intent.getIntExtra(EXTRA_INTENT_TYPE, IntentType.UNKNOWN); 70 @NotificationUmaTracker.SystemNotificationType 71 int notificationType = intent.getIntExtra( 72 EXTRA_NOTIFICATION_TYPE, NotificationUmaTracker.SystemNotificationType.UNKNOWN); 73 74 long createTime = intent.getLongExtra(EXTRA_CREATE_TIME, INVALID_CREATE_TIME); 75 76 switch (intentType) { 77 case IntentType.UNKNOWN: 78 break; 79 case IntentType.CONTENT_INTENT: 80 NotificationUmaTracker.getInstance().onNotificationContentClick( 81 notificationType, createTime); 82 break; 83 case IntentType.DELETE_INTENT: 84 NotificationUmaTracker.getInstance().onNotificationDismiss( 85 notificationType, createTime); 86 break; 87 case IntentType.ACTION_INTENT: 88 int actionType = intent.getIntExtra( 89 EXTRA_ACTION_TYPE, NotificationUmaTracker.ActionType.UNKNOWN); 90 NotificationUmaTracker.getInstance().onNotificationActionClick( 91 actionType, notificationType, createTime); 92 break; 93 } 94 95 forwardPendingIntent(intent); 96 } 97 } 98 NotificationIntentInterceptor()99 private NotificationIntentInterceptor() {} 100 101 /** 102 * Wraps the notification {@link PendingIntent} into another PendingIntent, to intercept clicks 103 * and dismiss events for metrics purpose. 104 * @param intentType The type of the pending intent to intercept. 105 * @param intentId The unique ID of the {@link PendingIntent}, used to distinguish action 106 * intents. 107 * @param metadata The metadata including notification id, tag, type, etc. 108 * @param pendingIntentProvider Provides the {@link PendingIntent} to launch Chrome. 109 * 110 */ createInterceptPendingIntent(@ntentType int intentType, int intentId, NotificationMetadata metadata, @Nullable PendingIntentProvider pendingIntentProvider)111 public static PendingIntent createInterceptPendingIntent(@IntentType int intentType, 112 int intentId, NotificationMetadata metadata, 113 @Nullable PendingIntentProvider pendingIntentProvider) { 114 PendingIntent pendingIntent = null; 115 int flags = 0; 116 if (pendingIntentProvider != null) { 117 pendingIntent = pendingIntentProvider.getPendingIntent(); 118 flags = pendingIntentProvider.getFlags(); 119 } 120 Context applicationContext = ContextUtils.getApplicationContext(); 121 Intent intent = new Intent(applicationContext, Receiver.class); 122 intent.setAction(INTENT_ACTION); 123 intent.putExtra(EXTRA_PENDING_INTENT, pendingIntent); 124 intent.putExtra(EXTRA_INTENT_TYPE, intentType); 125 intent.putExtra(EXTRA_NOTIFICATION_TYPE, metadata.type); 126 intent.putExtra(EXTRA_CREATE_TIME, System.currentTimeMillis()); 127 if (intentType == IntentType.ACTION_INTENT) { 128 intent.putExtra(EXTRA_ACTION_TYPE, intentId); 129 } 130 131 // This flag ensures the broadcast is delivered with foreground priority to speed up the 132 // broadcast delivery. 133 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { 134 intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND); 135 } 136 // Use request code to distinguish different PendingIntents on Android. 137 int requestCode = computeHashCode(metadata, intentType, intentId); 138 return PendingIntent.getBroadcast(applicationContext, requestCode, intent, flags); 139 } 140 141 /** 142 * Get the default delete PendingIntent used to track the notification metrics. 143 * @param metadata The metadata including notification id, tag, type, etc. 144 * @return The {@link PendingIntent} triggered when the user dismiss the notification. 145 */ getDefaultDeletePendingIntent(NotificationMetadata metadata)146 public static PendingIntent getDefaultDeletePendingIntent(NotificationMetadata metadata) { 147 return NotificationIntentInterceptor.createInterceptPendingIntent( 148 NotificationIntentInterceptor.IntentType.DELETE_INTENT, 0 /* intentId */, metadata, 149 null /* pendingIntentProvider */); 150 } 151 152 // Launches the notification's pending intent, which will perform Chrome feature related tasks. forwardPendingIntent(Intent intent)153 private static void forwardPendingIntent(Intent intent) { 154 if (intent == null) { 155 Log.e(TAG, "Intent to forward is null."); 156 return; 157 } 158 159 PendingIntent pendingIntent = 160 (PendingIntent) (intent.getParcelableExtra(EXTRA_PENDING_INTENT)); 161 if (pendingIntent == null) { 162 Log.d(TAG, "The notification's PendingIntent is null."); 163 return; 164 } 165 166 try { 167 pendingIntent.send(); 168 } catch (PendingIntent.CanceledException e) { 169 Log.e(TAG, "The PendingIntent to fire is canceled."); 170 e.printStackTrace(); 171 } 172 } 173 174 /** 175 * Computes an unique hash code to identify the intercept {@link PendingIntent} that wraps the 176 * notification's {@link PendingIntent}. 177 * @param metadata Notification metadata including notification id, tag, etc. 178 * @param intentType The type of the {@link PendingIntent}. 179 * @param intentId The unique ID of the {@link PendingIntent}, used to distinguish action 180 * intents. 181 * @return The hashcode for the intercept {@link PendingIntent}. 182 */ computeHashCode( NotificationMetadata metadata, @IntentType int intentType, int intentId)183 private static int computeHashCode( 184 NotificationMetadata metadata, @IntentType int intentType, int intentId) { 185 assert metadata != null; 186 int hashcode = metadata.type; 187 hashcode = hashcode * 31 + intentType; 188 hashcode = hashcode * 31 + intentId; 189 hashcode = hashcode * 31 + (metadata.tag == null ? 0 : metadata.tag.hashCode()); 190 hashcode = hashcode * 31 + metadata.id; 191 return hashcode; 192 } 193 } 194