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
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
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
123        public void addAction(NotificationCompatBase.Action action) {
124            mActionExtrasList.add(writeActionAndGetExtras(b, action));
125        }
126
127        @Override
128        public Notification.Builder getBuilder() {
129            return b;
130        }
131
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
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
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
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. */
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     */
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
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
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
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
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
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
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
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
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
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
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
391    public static boolean getLocalOnly(Notification notif) {
392        return getExtras(notif).getBoolean(EXTRA_LOCAL_ONLY);
393    }
394
395    public static String getGroup(Notification n) {
396        return getExtras(n).getString(EXTRA_GROUP_KEY);
397    }
398
399    public static boolean isGroupSummary(Notification n) {
400        return getExtras(n).getBoolean(EXTRA_GROUP_SUMMARY);
401    }
402
403    public static String getSortKey(Notification n) {
404        return getExtras(n).getString(EXTRA_SORT_KEY);
405    }
406}
407