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