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.support.annotation.RequiresApi; 26 import android.util.Log; 27 import android.util.SparseArray; 28 import android.widget.RemoteViews; 29 30 import java.lang.reflect.Field; 31 import java.util.ArrayList; 32 import java.util.List; 33 34 @RequiresApi(16) 35 class 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 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, RemoteViews contentView, RemoteViews bigContentView)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 addAction(NotificationCompatBase.Action action)133 public void addAction(NotificationCompatBase.Action action) { 134 mActionExtrasList.add(writeActionAndGetExtras(b, action)); 135 } 136 137 @Override getBuilder()138 public Notification.Builder getBuilder() { 139 return b; 140 } 141 142 @Override build()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 addBigTextStyle(NotificationBuilderWithBuilderAccessor b, CharSequence bigContentTitle, boolean useSummary, CharSequence summaryText, CharSequence bigText)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 addBigPictureStyle(NotificationBuilderWithBuilderAccessor b, CharSequence bigContentTitle, boolean useSummary, CharSequence summaryText, Bitmap bigPicture, Bitmap bigLargeIcon, boolean bigLargeIconSet)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 addInboxStyle(NotificationBuilderWithBuilderAccessor b, CharSequence bigContentTitle, boolean useSummary, CharSequence summaryText, ArrayList<CharSequence> texts)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. */ buildActionExtrasMap(List<Bundle> actionExtrasList)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 */ getExtras(Notification notif)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 readAction( NotificationCompatBase.Action.Factory factory, RemoteInputCompatBase.RemoteInput.Factory remoteInputFactory, int icon, CharSequence title, PendingIntent actionIntent, Bundle extras)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 writeActionAndGetExtras( Notification.Builder builder, NotificationCompatBase.Action action)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 getActionCount(Notification notif)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 getAction(Notification notif, int actionIndex, NotificationCompatBase.Action.Factory factory, RemoteInputCompatBase.RemoteInput.Factory remoteInputFactory)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 getActionObjectsLocked(Notification notif)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") ensureActionReflectionReadyLocked()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 getActionsFromParcelableArrayList( ArrayList<Parcelable> parcelables, NotificationCompatBase.Action.Factory actionFactory, RemoteInputCompatBase.RemoteInput.Factory remoteInputFactory)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 getActionFromBundle(Bundle bundle, NotificationCompatBase.Action.Factory actionFactory, RemoteInputCompatBase.RemoteInput.Factory remoteInputFactory)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 getParcelableArrayListForActions( NotificationCompatBase.Action[] actions)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 getBundleForAction(NotificationCompatBase.Action action)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