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