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