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