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