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