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
17package android.support.v4.app;
18
19import android.app.Notification;
20import android.app.PendingIntent;
21import android.content.Context;
22import android.graphics.Bitmap;
23import android.os.Bundle;
24import android.os.Parcelable;
25import android.support.annotation.RequiresApi;
26import android.util.Log;
27import android.util.SparseArray;
28import android.widget.RemoteViews;
29
30import java.lang.reflect.Field;
31import java.util.ArrayList;
32import java.util.List;
33
34@RequiresApi(16)
35class 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
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
133        public void addAction(NotificationCompatBase.Action action) {
134            mActionExtrasList.add(writeActionAndGetExtras(b, action));
135        }
136
137        @Override
138        public Notification.Builder getBuilder() {
139            return b;
140        }
141
142        @Override
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
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
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
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. */
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     */
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
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
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
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
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
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")
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
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
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
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
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