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.os.Bundle; 22 import android.os.Parcelable; 23 import android.support.annotation.RequiresApi; 24 import android.util.Log; 25 import android.util.SparseArray; 26 27 import java.lang.reflect.Field; 28 import java.util.ArrayList; 29 import java.util.Arrays; 30 import java.util.HashSet; 31 import java.util.List; 32 import java.util.Set; 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_DATA_ONLY_REMOTE_INPUTS = "android.support.dataRemoteInputs"; 40 static final String EXTRA_ALLOW_GENERATED_REPLIES = "android.support.allowGeneratedReplies"; 41 42 // Bundle keys for storing action fields in a bundle 43 private static final String KEY_ICON = "icon"; 44 private static final String KEY_TITLE = "title"; 45 private static final String KEY_ACTION_INTENT = "actionIntent"; 46 private static final String KEY_EXTRAS = "extras"; 47 private static final String KEY_REMOTE_INPUTS = "remoteInputs"; 48 private static final String KEY_DATA_ONLY_REMOTE_INPUTS = "dataOnlyRemoteInputs"; 49 private static final String KEY_RESULT_KEY = "resultKey"; 50 private static final String KEY_LABEL = "label"; 51 private static final String KEY_CHOICES = "choices"; 52 private static final String KEY_ALLOW_FREE_FORM_INPUT = "allowFreeFormInput"; 53 private static final String KEY_ALLOWED_DATA_TYPES = "allowedDataTypes"; 54 55 private static final Object sExtrasLock = new Object(); 56 private static Field sExtrasField; 57 private static boolean sExtrasFieldAccessFailed; 58 59 private static final Object sActionsLock = new Object(); 60 private static Class<?> sActionClass; 61 private static Field sActionsField; 62 private static Field sActionIconField; 63 private static Field sActionTitleField; 64 private static Field sActionIntentField; 65 private static boolean sActionsAccessFailed; 66 67 /** Return an SparseArray for action extras or null if none was needed. */ buildActionExtrasMap(List<Bundle> actionExtrasList)68 public static SparseArray<Bundle> buildActionExtrasMap(List<Bundle> actionExtrasList) { 69 SparseArray<Bundle> actionExtrasMap = null; 70 for (int i = 0, count = actionExtrasList.size(); i < count; i++) { 71 Bundle actionExtras = actionExtrasList.get(i); 72 if (actionExtras != null) { 73 if (actionExtrasMap == null) { 74 actionExtrasMap = new SparseArray<Bundle>(); 75 } 76 actionExtrasMap.put(i, actionExtras); 77 } 78 } 79 return actionExtrasMap; 80 } 81 82 /** 83 * Get the extras Bundle from a notification using reflection. Extras were present in 84 * Jellybean notifications, but the field was private until KitKat. 85 */ getExtras(Notification notif)86 public static Bundle getExtras(Notification notif) { 87 synchronized (sExtrasLock) { 88 if (sExtrasFieldAccessFailed) { 89 return null; 90 } 91 try { 92 if (sExtrasField == null) { 93 Field extrasField = Notification.class.getDeclaredField("extras"); 94 if (!Bundle.class.isAssignableFrom(extrasField.getType())) { 95 Log.e(TAG, "Notification.extras field is not of type Bundle"); 96 sExtrasFieldAccessFailed = true; 97 return null; 98 } 99 extrasField.setAccessible(true); 100 sExtrasField = extrasField; 101 } 102 Bundle extras = (Bundle) sExtrasField.get(notif); 103 if (extras == null) { 104 extras = new Bundle(); 105 sExtrasField.set(notif, extras); 106 } 107 return extras; 108 } catch (IllegalAccessException e) { 109 Log.e(TAG, "Unable to access notification extras", e); 110 } catch (NoSuchFieldException e) { 111 Log.e(TAG, "Unable to access notification extras", e); 112 } 113 sExtrasFieldAccessFailed = true; 114 return null; 115 } 116 } 117 readAction(int icon, CharSequence title, PendingIntent actionIntent, Bundle extras)118 public static NotificationCompat.Action readAction(int icon, CharSequence title, 119 PendingIntent actionIntent, Bundle extras) { 120 RemoteInput[] remoteInputs = null; 121 RemoteInput[] dataOnlyRemoteInputs = null; 122 boolean allowGeneratedReplies = false; 123 if (extras != null) { 124 remoteInputs = fromBundleArray( 125 getBundleArrayFromBundle(extras, 126 NotificationCompatExtras.EXTRA_REMOTE_INPUTS)); 127 dataOnlyRemoteInputs = fromBundleArray( 128 getBundleArrayFromBundle(extras, EXTRA_DATA_ONLY_REMOTE_INPUTS)); 129 allowGeneratedReplies = extras.getBoolean(EXTRA_ALLOW_GENERATED_REPLIES); 130 } 131 return new NotificationCompat.Action(icon, title, actionIntent, extras, remoteInputs, 132 dataOnlyRemoteInputs, allowGeneratedReplies); 133 } 134 writeActionAndGetExtras( Notification.Builder builder, NotificationCompat.Action action)135 public static Bundle writeActionAndGetExtras( 136 Notification.Builder builder, NotificationCompat.Action action) { 137 builder.addAction(action.getIcon(), action.getTitle(), action.getActionIntent()); 138 Bundle actionExtras = new Bundle(action.getExtras()); 139 if (action.getRemoteInputs() != null) { 140 actionExtras.putParcelableArray(NotificationCompatExtras.EXTRA_REMOTE_INPUTS, 141 toBundleArray(action.getRemoteInputs())); 142 } 143 if (action.getDataOnlyRemoteInputs() != null) { 144 actionExtras.putParcelableArray(EXTRA_DATA_ONLY_REMOTE_INPUTS, 145 toBundleArray(action.getDataOnlyRemoteInputs())); 146 } 147 actionExtras.putBoolean(EXTRA_ALLOW_GENERATED_REPLIES, 148 action.getAllowGeneratedReplies()); 149 return actionExtras; 150 } 151 getActionCount(Notification notif)152 public static int getActionCount(Notification notif) { 153 synchronized (sActionsLock) { 154 Object[] actionObjects = getActionObjectsLocked(notif); 155 return actionObjects != null ? actionObjects.length : 0; 156 } 157 } 158 getAction(Notification notif, int actionIndex)159 public static NotificationCompat.Action getAction(Notification notif, int actionIndex) { 160 synchronized (sActionsLock) { 161 try { 162 Object[] actionObjects = getActionObjectsLocked(notif); 163 if (actionObjects != null) { 164 Object actionObject = actionObjects[actionIndex]; 165 Bundle actionExtras = null; 166 Bundle extras = getExtras(notif); 167 if (extras != null) { 168 SparseArray<Bundle> actionExtrasMap = extras.getSparseParcelableArray( 169 NotificationCompatExtras.EXTRA_ACTION_EXTRAS); 170 if (actionExtrasMap != null) { 171 actionExtras = actionExtrasMap.get(actionIndex); 172 } 173 } 174 return readAction(sActionIconField.getInt(actionObject), 175 (CharSequence) sActionTitleField.get(actionObject), 176 (PendingIntent) sActionIntentField.get(actionObject), 177 actionExtras); 178 } 179 } catch (IllegalAccessException e) { 180 Log.e(TAG, "Unable to access notification actions", e); 181 sActionsAccessFailed = true; 182 } 183 } 184 return null; 185 } 186 getActionObjectsLocked(Notification notif)187 private static Object[] getActionObjectsLocked(Notification notif) { 188 synchronized (sActionsLock) { 189 if (!ensureActionReflectionReadyLocked()) { 190 return null; 191 } 192 try { 193 return (Object[]) sActionsField.get(notif); 194 } catch (IllegalAccessException e) { 195 Log.e(TAG, "Unable to access notification actions", e); 196 sActionsAccessFailed = true; 197 return null; 198 } 199 } 200 } 201 202 @SuppressWarnings("LiteralClassName") ensureActionReflectionReadyLocked()203 private static boolean ensureActionReflectionReadyLocked() { 204 if (sActionsAccessFailed) { 205 return false; 206 } 207 try { 208 if (sActionsField == null) { 209 sActionClass = Class.forName("android.app.Notification$Action"); 210 sActionIconField = sActionClass.getDeclaredField("icon"); 211 sActionTitleField = sActionClass.getDeclaredField("title"); 212 sActionIntentField = sActionClass.getDeclaredField("actionIntent"); 213 sActionsField = Notification.class.getDeclaredField("actions"); 214 sActionsField.setAccessible(true); 215 } 216 } catch (ClassNotFoundException e) { 217 Log.e(TAG, "Unable to access notification actions", e); 218 sActionsAccessFailed = true; 219 } catch (NoSuchFieldException e) { 220 Log.e(TAG, "Unable to access notification actions", e); 221 sActionsAccessFailed = true; 222 } 223 return !sActionsAccessFailed; 224 } 225 getActionFromBundle(Bundle bundle)226 static NotificationCompat.Action getActionFromBundle(Bundle bundle) { 227 Bundle extras = bundle.getBundle(KEY_EXTRAS); 228 boolean allowGeneratedReplies = false; 229 if (extras != null) { 230 allowGeneratedReplies = extras.getBoolean(EXTRA_ALLOW_GENERATED_REPLIES, false); 231 } 232 return new NotificationCompat.Action( 233 bundle.getInt(KEY_ICON), 234 bundle.getCharSequence(KEY_TITLE), 235 bundle.<PendingIntent>getParcelable(KEY_ACTION_INTENT), 236 bundle.getBundle(KEY_EXTRAS), 237 fromBundleArray(getBundleArrayFromBundle(bundle, KEY_REMOTE_INPUTS)), 238 fromBundleArray(getBundleArrayFromBundle(bundle, KEY_DATA_ONLY_REMOTE_INPUTS)), 239 allowGeneratedReplies); 240 } 241 getBundleForAction(NotificationCompat.Action action)242 static Bundle getBundleForAction(NotificationCompat.Action action) { 243 Bundle bundle = new Bundle(); 244 bundle.putInt(KEY_ICON, action.getIcon()); 245 bundle.putCharSequence(KEY_TITLE, action.getTitle()); 246 bundle.putParcelable(KEY_ACTION_INTENT, action.getActionIntent()); 247 Bundle actionExtras; 248 if (action.getExtras() != null) { 249 actionExtras = new Bundle(action.getExtras()); 250 } else { 251 actionExtras = new Bundle(); 252 } 253 actionExtras.putBoolean(NotificationCompatJellybean.EXTRA_ALLOW_GENERATED_REPLIES, 254 action.getAllowGeneratedReplies()); 255 bundle.putBundle(KEY_EXTRAS, actionExtras); 256 bundle.putParcelableArray(KEY_REMOTE_INPUTS, toBundleArray(action.getRemoteInputs())); 257 return bundle; 258 } 259 260 fromBundle(Bundle data)261 private static RemoteInput fromBundle(Bundle data) { 262 ArrayList<String> allowedDataTypesAsList = data.getStringArrayList(KEY_ALLOWED_DATA_TYPES); 263 Set<String> allowedDataTypes = new HashSet<>(); 264 if (allowedDataTypesAsList != null) { 265 for (String type : allowedDataTypesAsList) { 266 allowedDataTypes.add(type); 267 } 268 } 269 return new RemoteInput(data.getString(KEY_RESULT_KEY), 270 data.getCharSequence(KEY_LABEL), 271 data.getCharSequenceArray(KEY_CHOICES), 272 data.getBoolean(KEY_ALLOW_FREE_FORM_INPUT), 273 data.getBundle(KEY_EXTRAS), 274 allowedDataTypes); 275 } 276 toBundle(RemoteInput remoteInput)277 private static Bundle toBundle(RemoteInput remoteInput) { 278 Bundle data = new Bundle(); 279 data.putString(KEY_RESULT_KEY, remoteInput.getResultKey()); 280 data.putCharSequence(KEY_LABEL, remoteInput.getLabel()); 281 data.putCharSequenceArray(KEY_CHOICES, remoteInput.getChoices()); 282 data.putBoolean(KEY_ALLOW_FREE_FORM_INPUT, remoteInput.getAllowFreeFormInput()); 283 data.putBundle(KEY_EXTRAS, remoteInput.getExtras()); 284 285 Set<String> allowedDataTypes = remoteInput.getAllowedDataTypes(); 286 if (allowedDataTypes != null && !allowedDataTypes.isEmpty()) { 287 ArrayList<String> allowedDataTypesAsList = new ArrayList<>(allowedDataTypes.size()); 288 for (String type : allowedDataTypes) { 289 allowedDataTypesAsList.add(type); 290 } 291 data.putStringArrayList(KEY_ALLOWED_DATA_TYPES, allowedDataTypesAsList); 292 } 293 return data; 294 } 295 fromBundleArray(Bundle[] bundles)296 private static RemoteInput[] fromBundleArray(Bundle[] bundles) { 297 if (bundles == null) { 298 return null; 299 } 300 RemoteInput[] remoteInputs = new RemoteInput[bundles.length]; 301 for (int i = 0; i < bundles.length; i++) { 302 remoteInputs[i] = fromBundle(bundles[i]); 303 } 304 return remoteInputs; 305 } 306 toBundleArray(RemoteInput[] remoteInputs)307 private static Bundle[] toBundleArray(RemoteInput[] remoteInputs) { 308 if (remoteInputs == null) { 309 return null; 310 } 311 Bundle[] bundles = new Bundle[remoteInputs.length]; 312 for (int i = 0; i < remoteInputs.length; i++) { 313 bundles[i] = toBundle(remoteInputs[i]); 314 } 315 return bundles; 316 } 317 318 /** 319 * Get an array of Bundle objects from a parcelable array field in a bundle. 320 * Update the bundle to have a typed array so fetches in the future don't need 321 * to do an array copy. 322 */ getBundleArrayFromBundle(Bundle bundle, String key)323 private static Bundle[] getBundleArrayFromBundle(Bundle bundle, String key) { 324 Parcelable[] array = bundle.getParcelableArray(key); 325 if (array instanceof Bundle[] || array == null) { 326 return (Bundle[]) array; 327 } 328 Bundle[] typedArray = Arrays.copyOf(array, array.length, 329 Bundle[].class); 330 bundle.putParcelableArray(key, typedArray); 331 return typedArray; 332 } 333 } 334