1 /*
2  * Copyright (C) 2012 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.support.v4.app;
18 
19 import android.app.Notification;
20 import android.app.PendingIntent;
21 import android.content.Context;
22 import android.graphics.Bitmap;
23 import android.os.Bundle;
24 import android.os.Parcelable;
25 import android.support.annotation.RequiresApi;
26 import android.util.Log;
27 import android.util.SparseArray;
28 import android.widget.RemoteViews;
29 
30 import java.lang.reflect.Field;
31 import java.util.ArrayList;
32 import java.util.List;
33 
34 @RequiresApi(16)
35 class NotificationCompatJellybean {
36     public static final String TAG = "NotificationCompat";
37 
38     // Extras keys used for Jellybean SDK and above.
39     static final String EXTRA_LOCAL_ONLY = "android.support.localOnly";
40     static final String EXTRA_ACTION_EXTRAS = "android.support.actionExtras";
41     static final String EXTRA_REMOTE_INPUTS = "android.support.remoteInputs";
42     static final String EXTRA_DATA_ONLY_REMOTE_INPUTS = "android.support.dataRemoteInputs";
43     static final String EXTRA_GROUP_KEY = "android.support.groupKey";
44     static final String EXTRA_GROUP_SUMMARY = "android.support.isGroupSummary";
45     static final String EXTRA_SORT_KEY = "android.support.sortKey";
46     static final String EXTRA_USE_SIDE_CHANNEL = "android.support.useSideChannel";
47     static final String EXTRA_ALLOW_GENERATED_REPLIES = "android.support.allowGeneratedReplies";
48 
49     // Bundle keys for storing action fields in a bundle
50     private static final String KEY_ICON = "icon";
51     private static final String KEY_TITLE = "title";
52     private static final String KEY_ACTION_INTENT = "actionIntent";
53     private static final String KEY_EXTRAS = "extras";
54     private static final String KEY_REMOTE_INPUTS = "remoteInputs";
55     private static final String KEY_DATA_ONLY_REMOTE_INPUTS = "dataOnlyRemoteInputs";
56 
57     private static final Object sExtrasLock = new Object();
58     private static Field sExtrasField;
59     private static boolean sExtrasFieldAccessFailed;
60 
61     private static final Object sActionsLock = new Object();
62     private static Class<?> sActionClass;
63     private static Field sActionsField;
64     private static Field sActionIconField;
65     private static Field sActionTitleField;
66     private static Field sActionIntentField;
67     private static boolean sActionsAccessFailed;
68 
69     public static class Builder implements NotificationBuilderWithBuilderAccessor,
70             NotificationBuilderWithActions {
71         private Notification.Builder b;
72         private final Bundle mExtras;
73         private List<Bundle> mActionExtrasList = new ArrayList<Bundle>();
74         private RemoteViews mContentView;
75         private RemoteViews mBigContentView;
76 
Builder(Context context, Notification n, CharSequence contentTitle, CharSequence contentText, CharSequence contentInfo, RemoteViews tickerView, int number, PendingIntent contentIntent, PendingIntent fullScreenIntent, Bitmap largeIcon, int progressMax, int progress, boolean progressIndeterminate, boolean useChronometer, int priority, CharSequence subText, boolean localOnly, Bundle extras, String groupKey, boolean groupSummary, String sortKey, RemoteViews contentView, RemoteViews bigContentView)77         public Builder(Context context, Notification n,
78                 CharSequence contentTitle, CharSequence contentText, CharSequence contentInfo,
79                 RemoteViews tickerView, int number,
80                 PendingIntent contentIntent, PendingIntent fullScreenIntent, Bitmap largeIcon,
81                 int progressMax, int progress, boolean progressIndeterminate,
82                 boolean useChronometer, int priority, CharSequence subText, boolean localOnly,
83                 Bundle extras, String groupKey, boolean groupSummary, String sortKey,
84                 RemoteViews contentView, RemoteViews bigContentView) {
85             b = new Notification.Builder(context)
86                 .setWhen(n.when)
87                 .setSmallIcon(n.icon, n.iconLevel)
88                 .setContent(n.contentView)
89                 .setTicker(n.tickerText, tickerView)
90                 .setSound(n.sound, n.audioStreamType)
91                 .setVibrate(n.vibrate)
92                 .setLights(n.ledARGB, n.ledOnMS, n.ledOffMS)
93                 .setOngoing((n.flags & Notification.FLAG_ONGOING_EVENT) != 0)
94                 .setOnlyAlertOnce((n.flags & Notification.FLAG_ONLY_ALERT_ONCE) != 0)
95                 .setAutoCancel((n.flags & Notification.FLAG_AUTO_CANCEL) != 0)
96                 .setDefaults(n.defaults)
97                 .setContentTitle(contentTitle)
98                 .setContentText(contentText)
99                 .setSubText(subText)
100                 .setContentInfo(contentInfo)
101                 .setContentIntent(contentIntent)
102                 .setDeleteIntent(n.deleteIntent)
103                 .setFullScreenIntent(fullScreenIntent,
104                         (n.flags & Notification.FLAG_HIGH_PRIORITY) != 0)
105                 .setLargeIcon(largeIcon)
106                 .setNumber(number)
107                 .setUsesChronometer(useChronometer)
108                 .setPriority(priority)
109                 .setProgress(progressMax, progress, progressIndeterminate);
110             mExtras = new Bundle();
111             if (extras != null) {
112                 mExtras.putAll(extras);
113             }
114             if (localOnly) {
115                 mExtras.putBoolean(EXTRA_LOCAL_ONLY, true);
116             }
117             if (groupKey != null) {
118                 mExtras.putString(EXTRA_GROUP_KEY, groupKey);
119                 if (groupSummary) {
120                     mExtras.putBoolean(EXTRA_GROUP_SUMMARY, true);
121                 } else {
122                     mExtras.putBoolean(EXTRA_USE_SIDE_CHANNEL, true);
123                 }
124             }
125             if (sortKey != null) {
126                 mExtras.putString(EXTRA_SORT_KEY, sortKey);
127             }
128             mContentView = contentView;
129             mBigContentView = bigContentView;
130         }
131 
132         @Override
addAction(NotificationCompatBase.Action action)133         public void addAction(NotificationCompatBase.Action action) {
134             mActionExtrasList.add(writeActionAndGetExtras(b, action));
135         }
136 
137         @Override
getBuilder()138         public Notification.Builder getBuilder() {
139             return b;
140         }
141 
142         @Override
build()143         public Notification build() {
144             Notification notif = b.build();
145             // Merge in developer provided extras, but let the values already set
146             // for keys take precedence.
147             Bundle extras = getExtras(notif);
148             Bundle mergeBundle = new Bundle(mExtras);
149             for (String key : mExtras.keySet()) {
150                 if (extras.containsKey(key)) {
151                     mergeBundle.remove(key);
152                 }
153             }
154             extras.putAll(mergeBundle);
155             SparseArray<Bundle> actionExtrasMap = buildActionExtrasMap(mActionExtrasList);
156             if (actionExtrasMap != null) {
157                 // Add the action extras sparse array if any action was added with extras.
158                 getExtras(notif).putSparseParcelableArray(EXTRA_ACTION_EXTRAS, actionExtrasMap);
159             }
160             if (mContentView != null) {
161                 notif.contentView = mContentView;
162             }
163             if (mBigContentView != null) {
164                 notif.bigContentView = mBigContentView;
165             }
166             return notif;
167         }
168     }
169 
addBigTextStyle(NotificationBuilderWithBuilderAccessor b, CharSequence bigContentTitle, boolean useSummary, CharSequence summaryText, CharSequence bigText)170     public static void addBigTextStyle(NotificationBuilderWithBuilderAccessor b,
171             CharSequence bigContentTitle, boolean useSummary,
172             CharSequence summaryText, CharSequence bigText) {
173         Notification.BigTextStyle style = new Notification.BigTextStyle(b.getBuilder())
174             .setBigContentTitle(bigContentTitle)
175             .bigText(bigText);
176         if (useSummary) {
177             style.setSummaryText(summaryText);
178         }
179     }
180 
addBigPictureStyle(NotificationBuilderWithBuilderAccessor b, CharSequence bigContentTitle, boolean useSummary, CharSequence summaryText, Bitmap bigPicture, Bitmap bigLargeIcon, boolean bigLargeIconSet)181     public static void addBigPictureStyle(NotificationBuilderWithBuilderAccessor b,
182             CharSequence bigContentTitle, boolean useSummary,
183             CharSequence summaryText, Bitmap bigPicture, Bitmap bigLargeIcon,
184             boolean bigLargeIconSet) {
185         Notification.BigPictureStyle style = new Notification.BigPictureStyle(b.getBuilder())
186             .setBigContentTitle(bigContentTitle)
187             .bigPicture(bigPicture);
188         if (bigLargeIconSet) {
189             style.bigLargeIcon(bigLargeIcon);
190         }
191         if (useSummary) {
192             style.setSummaryText(summaryText);
193         }
194     }
195 
addInboxStyle(NotificationBuilderWithBuilderAccessor b, CharSequence bigContentTitle, boolean useSummary, CharSequence summaryText, ArrayList<CharSequence> texts)196     public static void addInboxStyle(NotificationBuilderWithBuilderAccessor b,
197             CharSequence bigContentTitle, boolean useSummary,
198             CharSequence summaryText, ArrayList<CharSequence> texts) {
199         Notification.InboxStyle style = new Notification.InboxStyle(b.getBuilder())
200             .setBigContentTitle(bigContentTitle);
201         if (useSummary) {
202             style.setSummaryText(summaryText);
203         }
204         for (CharSequence text: texts) {
205             style.addLine(text);
206         }
207     }
208 
209     /** Return an SparseArray for action extras or null if none was needed. */
buildActionExtrasMap(List<Bundle> actionExtrasList)210     public static SparseArray<Bundle> buildActionExtrasMap(List<Bundle> actionExtrasList) {
211         SparseArray<Bundle> actionExtrasMap = null;
212         for (int i = 0, count = actionExtrasList.size(); i < count; i++) {
213             Bundle actionExtras = actionExtrasList.get(i);
214             if (actionExtras != null) {
215                 if (actionExtrasMap == null) {
216                     actionExtrasMap = new SparseArray<Bundle>();
217                 }
218                 actionExtrasMap.put(i, actionExtras);
219             }
220         }
221         return actionExtrasMap;
222     }
223 
224     /**
225      * Get the extras Bundle from a notification using reflection. Extras were present in
226      * Jellybean notifications, but the field was private until KitKat.
227      */
getExtras(Notification notif)228     public static Bundle getExtras(Notification notif) {
229         synchronized (sExtrasLock) {
230             if (sExtrasFieldAccessFailed) {
231                 return null;
232             }
233             try {
234                 if (sExtrasField == null) {
235                     Field extrasField = Notification.class.getDeclaredField("extras");
236                     if (!Bundle.class.isAssignableFrom(extrasField.getType())) {
237                         Log.e(TAG, "Notification.extras field is not of type Bundle");
238                         sExtrasFieldAccessFailed = true;
239                         return null;
240                     }
241                     extrasField.setAccessible(true);
242                     sExtrasField = extrasField;
243                 }
244                 Bundle extras = (Bundle) sExtrasField.get(notif);
245                 if (extras == null) {
246                     extras = new Bundle();
247                     sExtrasField.set(notif, extras);
248                 }
249                 return extras;
250             } catch (IllegalAccessException e) {
251                 Log.e(TAG, "Unable to access notification extras", e);
252             } catch (NoSuchFieldException e) {
253                 Log.e(TAG, "Unable to access notification extras", e);
254             }
255             sExtrasFieldAccessFailed = true;
256             return null;
257         }
258     }
259 
readAction( NotificationCompatBase.Action.Factory factory, RemoteInputCompatBase.RemoteInput.Factory remoteInputFactory, int icon, CharSequence title, PendingIntent actionIntent, Bundle extras)260     public static NotificationCompatBase.Action readAction(
261             NotificationCompatBase.Action.Factory factory,
262             RemoteInputCompatBase.RemoteInput.Factory remoteInputFactory, int icon,
263             CharSequence title, PendingIntent actionIntent, Bundle extras) {
264         RemoteInputCompatBase.RemoteInput[] remoteInputs = null;
265         RemoteInputCompatBase.RemoteInput[] dataOnlyRemoteInputs = null;
266         boolean allowGeneratedReplies = false;
267         if (extras != null) {
268             remoteInputs = RemoteInputCompatJellybean.fromBundleArray(
269                     BundleUtil.getBundleArrayFromBundle(extras, EXTRA_REMOTE_INPUTS),
270                     remoteInputFactory);
271             dataOnlyRemoteInputs = RemoteInputCompatJellybean.fromBundleArray(
272                     BundleUtil.getBundleArrayFromBundle(extras, EXTRA_DATA_ONLY_REMOTE_INPUTS),
273                     remoteInputFactory);
274             allowGeneratedReplies = extras.getBoolean(EXTRA_ALLOW_GENERATED_REPLIES);
275         }
276         return factory.build(icon, title, actionIntent, extras, remoteInputs,
277                 dataOnlyRemoteInputs, allowGeneratedReplies);
278     }
279 
writeActionAndGetExtras( Notification.Builder builder, NotificationCompatBase.Action action)280     public static Bundle writeActionAndGetExtras(
281             Notification.Builder builder, NotificationCompatBase.Action action) {
282         builder.addAction(action.getIcon(), action.getTitle(), action.getActionIntent());
283         Bundle actionExtras = new Bundle(action.getExtras());
284         if (action.getRemoteInputs() != null) {
285             actionExtras.putParcelableArray(EXTRA_REMOTE_INPUTS,
286                     RemoteInputCompatJellybean.toBundleArray(action.getRemoteInputs()));
287         }
288         if (action.getDataOnlyRemoteInputs() != null) {
289             actionExtras.putParcelableArray(EXTRA_DATA_ONLY_REMOTE_INPUTS,
290                     RemoteInputCompatJellybean.toBundleArray(action.getDataOnlyRemoteInputs()));
291         }
292         actionExtras.putBoolean(EXTRA_ALLOW_GENERATED_REPLIES,
293                 action.getAllowGeneratedReplies());
294         return actionExtras;
295     }
296 
getActionCount(Notification notif)297     public static int getActionCount(Notification notif) {
298         synchronized (sActionsLock) {
299             Object[] actionObjects = getActionObjectsLocked(notif);
300             return actionObjects != null ? actionObjects.length : 0;
301         }
302     }
303 
getAction(Notification notif, int actionIndex, NotificationCompatBase.Action.Factory factory, RemoteInputCompatBase.RemoteInput.Factory remoteInputFactory)304     public static NotificationCompatBase.Action getAction(Notification notif, int actionIndex,
305             NotificationCompatBase.Action.Factory factory,
306             RemoteInputCompatBase.RemoteInput.Factory remoteInputFactory) {
307         synchronized (sActionsLock) {
308             try {
309                 Object[] actionObjects = getActionObjectsLocked(notif);
310                 if (actionObjects != null) {
311                     Object actionObject = actionObjects[actionIndex];
312                     Bundle actionExtras = null;
313                     Bundle extras = getExtras(notif);
314                     if (extras != null) {
315                         SparseArray<Bundle> actionExtrasMap = extras.getSparseParcelableArray(
316                                 EXTRA_ACTION_EXTRAS);
317                         if (actionExtrasMap != null) {
318                             actionExtras = actionExtrasMap.get(actionIndex);
319                         }
320                     }
321                     return readAction(factory, remoteInputFactory,
322                             sActionIconField.getInt(actionObject),
323                             (CharSequence) sActionTitleField.get(actionObject),
324                             (PendingIntent) sActionIntentField.get(actionObject),
325                             actionExtras);
326                 }
327             } catch (IllegalAccessException e) {
328                 Log.e(TAG, "Unable to access notification actions", e);
329                 sActionsAccessFailed = true;
330             }
331         }
332         return null;
333     }
334 
getActionObjectsLocked(Notification notif)335     private static Object[] getActionObjectsLocked(Notification notif) {
336         synchronized (sActionsLock) {
337             if (!ensureActionReflectionReadyLocked()) {
338                 return null;
339             }
340             try {
341                 return (Object[]) sActionsField.get(notif);
342             } catch (IllegalAccessException e) {
343                 Log.e(TAG, "Unable to access notification actions", e);
344                 sActionsAccessFailed = true;
345                 return null;
346             }
347         }
348     }
349 
350     @SuppressWarnings("LiteralClassName")
ensureActionReflectionReadyLocked()351     private static boolean ensureActionReflectionReadyLocked() {
352         if (sActionsAccessFailed) {
353             return false;
354         }
355         try {
356             if (sActionsField == null) {
357                 sActionClass = Class.forName("android.app.Notification$Action");
358                 sActionIconField = sActionClass.getDeclaredField("icon");
359                 sActionTitleField = sActionClass.getDeclaredField("title");
360                 sActionIntentField = sActionClass.getDeclaredField("actionIntent");
361                 sActionsField = Notification.class.getDeclaredField("actions");
362                 sActionsField.setAccessible(true);
363             }
364         } catch (ClassNotFoundException e) {
365             Log.e(TAG, "Unable to access notification actions", e);
366             sActionsAccessFailed = true;
367         } catch (NoSuchFieldException e) {
368             Log.e(TAG, "Unable to access notification actions", e);
369             sActionsAccessFailed = true;
370         }
371         return !sActionsAccessFailed;
372     }
373 
getActionsFromParcelableArrayList( ArrayList<Parcelable> parcelables, NotificationCompatBase.Action.Factory actionFactory, RemoteInputCompatBase.RemoteInput.Factory remoteInputFactory)374     public static NotificationCompatBase.Action[] getActionsFromParcelableArrayList(
375             ArrayList<Parcelable> parcelables,
376             NotificationCompatBase.Action.Factory actionFactory,
377             RemoteInputCompatBase.RemoteInput.Factory remoteInputFactory) {
378         if (parcelables == null) {
379             return null;
380         }
381         NotificationCompatBase.Action[] actions = actionFactory.newArray(parcelables.size());
382         for (int i = 0; i < actions.length; i++) {
383             actions[i] = getActionFromBundle((Bundle) parcelables.get(i),
384                     actionFactory, remoteInputFactory);
385         }
386         return actions;
387     }
388 
getActionFromBundle(Bundle bundle, NotificationCompatBase.Action.Factory actionFactory, RemoteInputCompatBase.RemoteInput.Factory remoteInputFactory)389     private static NotificationCompatBase.Action getActionFromBundle(Bundle bundle,
390             NotificationCompatBase.Action.Factory actionFactory,
391             RemoteInputCompatBase.RemoteInput.Factory remoteInputFactory) {
392         Bundle extras = bundle.getBundle(KEY_EXTRAS);
393         boolean allowGeneratedReplies = false;
394         if (extras != null) {
395             allowGeneratedReplies = extras.getBoolean(EXTRA_ALLOW_GENERATED_REPLIES, false);
396         }
397         return actionFactory.build(
398                 bundle.getInt(KEY_ICON),
399                 bundle.getCharSequence(KEY_TITLE),
400                 bundle.<PendingIntent>getParcelable(KEY_ACTION_INTENT),
401                 bundle.getBundle(KEY_EXTRAS),
402                 RemoteInputCompatJellybean.fromBundleArray(
403                         BundleUtil.getBundleArrayFromBundle(bundle, KEY_REMOTE_INPUTS),
404                         remoteInputFactory),
405                 RemoteInputCompatJellybean.fromBundleArray(
406                         BundleUtil.getBundleArrayFromBundle(bundle, KEY_DATA_ONLY_REMOTE_INPUTS),
407                         remoteInputFactory),
408                 allowGeneratedReplies);
409     }
410 
getParcelableArrayListForActions( NotificationCompatBase.Action[] actions)411     public static ArrayList<Parcelable> getParcelableArrayListForActions(
412             NotificationCompatBase.Action[] actions) {
413         if (actions == null) {
414             return null;
415         }
416         ArrayList<Parcelable> parcelables = new ArrayList<Parcelable>(actions.length);
417         for (NotificationCompatBase.Action action : actions) {
418             parcelables.add(getBundleForAction(action));
419         }
420         return parcelables;
421     }
422 
getBundleForAction(NotificationCompatBase.Action action)423     private static Bundle getBundleForAction(NotificationCompatBase.Action action) {
424         Bundle bundle = new Bundle();
425         bundle.putInt(KEY_ICON, action.getIcon());
426         bundle.putCharSequence(KEY_TITLE, action.getTitle());
427         bundle.putParcelable(KEY_ACTION_INTENT, action.getActionIntent());
428         Bundle actionExtras;
429         if (action.getExtras() != null) {
430             actionExtras = new Bundle(action.getExtras());
431         } else {
432             actionExtras = new Bundle();
433         }
434         actionExtras.putBoolean(NotificationCompatJellybean.EXTRA_ALLOW_GENERATED_REPLIES,
435                 action.getAllowGeneratedReplies());
436         bundle.putBundle(KEY_EXTRAS, actionExtras);
437         bundle.putParcelableArray(KEY_REMOTE_INPUTS, RemoteInputCompatJellybean.toBundleArray(
438                 action.getRemoteInputs()));
439         return bundle;
440     }
441 }
442