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