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