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            if (mContentView != null) {
157                notif.contentView = mContentView;
158            }
159            if (mBigContentView != null) {
160                notif.bigContentView = mBigContentView;
161            }
162            return notif;
163        }
164    }
165
166    public static void addBigTextStyle(NotificationBuilderWithBuilderAccessor b,
167            CharSequence bigContentTitle, boolean useSummary,
168            CharSequence summaryText, CharSequence bigText) {
169        Notification.BigTextStyle style = new Notification.BigTextStyle(b.getBuilder())
170            .setBigContentTitle(bigContentTitle)
171            .bigText(bigText);
172        if (useSummary) {
173            style.setSummaryText(summaryText);
174        }
175    }
176
177    public static void addBigPictureStyle(NotificationBuilderWithBuilderAccessor b,
178            CharSequence bigContentTitle, boolean useSummary,
179            CharSequence summaryText, Bitmap bigPicture, Bitmap bigLargeIcon,
180            boolean bigLargeIconSet) {
181        Notification.BigPictureStyle style = new Notification.BigPictureStyle(b.getBuilder())
182            .setBigContentTitle(bigContentTitle)
183            .bigPicture(bigPicture);
184        if (bigLargeIconSet) {
185            style.bigLargeIcon(bigLargeIcon);
186        }
187        if (useSummary) {
188            style.setSummaryText(summaryText);
189        }
190    }
191
192    public static void addInboxStyle(NotificationBuilderWithBuilderAccessor b,
193            CharSequence bigContentTitle, boolean useSummary,
194            CharSequence summaryText, ArrayList<CharSequence> texts) {
195        Notification.InboxStyle style = new Notification.InboxStyle(b.getBuilder())
196            .setBigContentTitle(bigContentTitle);
197        if (useSummary) {
198            style.setSummaryText(summaryText);
199        }
200        for (CharSequence text: texts) {
201            style.addLine(text);
202        }
203    }
204
205    /** Return an SparseArray for action extras or null if none was needed. */
206    public static SparseArray<Bundle> buildActionExtrasMap(List<Bundle> actionExtrasList) {
207        SparseArray<Bundle> actionExtrasMap = null;
208        for (int i = 0, count = actionExtrasList.size(); i < count; i++) {
209            Bundle actionExtras = actionExtrasList.get(i);
210            if (actionExtras != null) {
211                if (actionExtrasMap == null) {
212                    actionExtrasMap = new SparseArray<Bundle>();
213                }
214                actionExtrasMap.put(i, actionExtras);
215            }
216        }
217        return actionExtrasMap;
218    }
219
220    /**
221     * Get the extras Bundle from a notification using reflection. Extras were present in
222     * Jellybean notifications, but the field was private until KitKat.
223     */
224    public static Bundle getExtras(Notification notif) {
225        synchronized (sExtrasLock) {
226            if (sExtrasFieldAccessFailed) {
227                return null;
228            }
229            try {
230                if (sExtrasField == null) {
231                    Field extrasField = Notification.class.getDeclaredField("extras");
232                    if (!Bundle.class.isAssignableFrom(extrasField.getType())) {
233                        Log.e(TAG, "Notification.extras field is not of type Bundle");
234                        sExtrasFieldAccessFailed = true;
235                        return null;
236                    }
237                    extrasField.setAccessible(true);
238                    sExtrasField = extrasField;
239                }
240                Bundle extras = (Bundle) sExtrasField.get(notif);
241                if (extras == null) {
242                    extras = new Bundle();
243                    sExtrasField.set(notif, extras);
244                }
245                return extras;
246            } catch (IllegalAccessException e) {
247                Log.e(TAG, "Unable to access notification extras", e);
248            } catch (NoSuchFieldException e) {
249                Log.e(TAG, "Unable to access notification extras", e);
250            }
251            sExtrasFieldAccessFailed = true;
252            return null;
253        }
254    }
255
256    public static NotificationCompatBase.Action readAction(
257            NotificationCompatBase.Action.Factory factory,
258            RemoteInputCompatBase.RemoteInput.Factory remoteInputFactory, int icon,
259            CharSequence title, PendingIntent actionIntent, Bundle extras) {
260        RemoteInputCompatBase.RemoteInput[] remoteInputs = null;
261        boolean allowGeneratedReplies = false;
262        if (extras != null) {
263            remoteInputs = RemoteInputCompatJellybean.fromBundleArray(
264                    BundleUtil.getBundleArrayFromBundle(extras, EXTRA_REMOTE_INPUTS),
265                    remoteInputFactory);
266            allowGeneratedReplies = extras.getBoolean(EXTRA_ALLOW_GENERATED_REPLIES);
267        }
268        return factory.build(icon, title, actionIntent, extras, remoteInputs,
269                allowGeneratedReplies);
270    }
271
272    public static Bundle writeActionAndGetExtras(
273            Notification.Builder builder, NotificationCompatBase.Action action) {
274        builder.addAction(action.getIcon(), action.getTitle(), action.getActionIntent());
275        Bundle actionExtras = new Bundle(action.getExtras());
276        if (action.getRemoteInputs() != null) {
277            actionExtras.putParcelableArray(EXTRA_REMOTE_INPUTS,
278                    RemoteInputCompatJellybean.toBundleArray(action.getRemoteInputs()));
279        }
280        actionExtras.putBoolean(EXTRA_ALLOW_GENERATED_REPLIES,
281                action.getAllowGeneratedReplies());
282        return actionExtras;
283    }
284
285    public static int getActionCount(Notification notif) {
286        synchronized (sActionsLock) {
287            Object[] actionObjects = getActionObjectsLocked(notif);
288            return actionObjects != null ? actionObjects.length : 0;
289        }
290    }
291
292    public static NotificationCompatBase.Action getAction(Notification notif, int actionIndex,
293            NotificationCompatBase.Action.Factory factory,
294            RemoteInputCompatBase.RemoteInput.Factory remoteInputFactory) {
295        synchronized (sActionsLock) {
296            try {
297                Object actionObject = getActionObjectsLocked(notif)[actionIndex];
298                Bundle actionExtras = null;
299                Bundle extras = getExtras(notif);
300                if (extras != null) {
301                    SparseArray<Bundle> actionExtrasMap = extras.getSparseParcelableArray(
302                            EXTRA_ACTION_EXTRAS);
303                    if (actionExtrasMap != null) {
304                        actionExtras = actionExtrasMap.get(actionIndex);
305                    }
306                }
307                return readAction(factory, remoteInputFactory,
308                        sActionIconField.getInt(actionObject),
309                        (CharSequence) sActionTitleField.get(actionObject),
310                        (PendingIntent) sActionIntentField.get(actionObject),
311                        actionExtras);
312            } catch (IllegalAccessException e) {
313                Log.e(TAG, "Unable to access notification actions", e);
314                sActionsAccessFailed = true;
315            }
316        }
317        return null;
318    }
319
320    private static Object[] getActionObjectsLocked(Notification notif) {
321        synchronized (sActionsLock) {
322            if (!ensureActionReflectionReadyLocked()) {
323                return null;
324            }
325            try {
326                return (Object[]) sActionsField.get(notif);
327            } catch (IllegalAccessException e) {
328                Log.e(TAG, "Unable to access notification actions", e);
329                sActionsAccessFailed = true;
330                return null;
331            }
332        }
333    }
334
335    private static boolean ensureActionReflectionReadyLocked() {
336        if (sActionsAccessFailed) {
337            return false;
338        }
339        try {
340            if (sActionsField == null) {
341                sActionClass = Class.forName("android.app.Notification$Action");
342                sActionIconField = sActionClass.getDeclaredField("icon");
343                sActionTitleField = sActionClass.getDeclaredField("title");
344                sActionIntentField = sActionClass.getDeclaredField("actionIntent");
345                sActionsField = Notification.class.getDeclaredField("actions");
346                sActionsField.setAccessible(true);
347            }
348        } catch (ClassNotFoundException e) {
349            Log.e(TAG, "Unable to access notification actions", e);
350            sActionsAccessFailed = true;
351        } catch (NoSuchFieldException e) {
352            Log.e(TAG, "Unable to access notification actions", e);
353            sActionsAccessFailed = true;
354        }
355        return !sActionsAccessFailed;
356    }
357
358    public static NotificationCompatBase.Action[] getActionsFromParcelableArrayList(
359            ArrayList<Parcelable> parcelables,
360            NotificationCompatBase.Action.Factory actionFactory,
361            RemoteInputCompatBase.RemoteInput.Factory remoteInputFactory) {
362        if (parcelables == null) {
363            return null;
364        }
365        NotificationCompatBase.Action[] actions = actionFactory.newArray(parcelables.size());
366        for (int i = 0; i < actions.length; i++) {
367            actions[i] = getActionFromBundle((Bundle) parcelables.get(i),
368                    actionFactory, remoteInputFactory);
369        }
370        return actions;
371    }
372
373    private static NotificationCompatBase.Action getActionFromBundle(Bundle bundle,
374            NotificationCompatBase.Action.Factory actionFactory,
375            RemoteInputCompatBase.RemoteInput.Factory remoteInputFactory) {
376        return actionFactory.build(
377                bundle.getInt(KEY_ICON),
378                bundle.getCharSequence(KEY_TITLE),
379                bundle.<PendingIntent>getParcelable(KEY_ACTION_INTENT),
380                bundle.getBundle(KEY_EXTRAS),
381                RemoteInputCompatJellybean.fromBundleArray(
382                        BundleUtil.getBundleArrayFromBundle(bundle, KEY_REMOTE_INPUTS),
383                        remoteInputFactory), bundle.getBoolean(KEY_ALLOW_GENERATED_REPLIES));
384    }
385
386    public static ArrayList<Parcelable> getParcelableArrayListForActions(
387            NotificationCompatBase.Action[] actions) {
388        if (actions == null) {
389            return null;
390        }
391        ArrayList<Parcelable> parcelables = new ArrayList<Parcelable>(actions.length);
392        for (NotificationCompatBase.Action action : actions) {
393            parcelables.add(getBundleForAction(action));
394        }
395        return parcelables;
396    }
397
398    private static Bundle getBundleForAction(NotificationCompatBase.Action action) {
399        Bundle bundle = new Bundle();
400        bundle.putInt(KEY_ICON, action.getIcon());
401        bundle.putCharSequence(KEY_TITLE, action.getTitle());
402        bundle.putParcelable(KEY_ACTION_INTENT, action.getActionIntent());
403        bundle.putBundle(KEY_EXTRAS, action.getExtras());
404        bundle.putParcelableArray(KEY_REMOTE_INPUTS, RemoteInputCompatJellybean.toBundleArray(
405                action.getRemoteInputs()));
406        return bundle;
407    }
408
409    public static boolean getLocalOnly(Notification notif) {
410        return getExtras(notif).getBoolean(EXTRA_LOCAL_ONLY);
411    }
412
413    public static String getGroup(Notification n) {
414        return getExtras(n).getString(EXTRA_GROUP_KEY);
415    }
416
417    public static boolean isGroupSummary(Notification n) {
418        return getExtras(n).getBoolean(EXTRA_GROUP_SUMMARY);
419    }
420
421    public static String getSortKey(Notification n) {
422        return getExtras(n).getString(EXTRA_SORT_KEY);
423    }
424}
425