/* * Copyright (C) 2012 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package androidx.core.app; import android.app.Notification; import android.app.PendingIntent; import android.os.Bundle; import android.os.Parcelable; import android.util.Log; import android.util.SparseArray; import androidx.annotation.RequiresApi; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Set; @RequiresApi(16) class NotificationCompatJellybean { public static final String TAG = "NotificationCompat"; // Extras keys used for Jellybean SDK and above. static final String EXTRA_DATA_ONLY_REMOTE_INPUTS = "android.support.dataRemoteInputs"; static final String EXTRA_ALLOW_GENERATED_REPLIES = "android.support.allowGeneratedReplies"; // Bundle keys for storing action fields in a bundle private static final String KEY_ICON = "icon"; private static final String KEY_TITLE = "title"; private static final String KEY_ACTION_INTENT = "actionIntent"; private static final String KEY_EXTRAS = "extras"; private static final String KEY_REMOTE_INPUTS = "remoteInputs"; private static final String KEY_DATA_ONLY_REMOTE_INPUTS = "dataOnlyRemoteInputs"; private static final String KEY_RESULT_KEY = "resultKey"; private static final String KEY_LABEL = "label"; private static final String KEY_CHOICES = "choices"; private static final String KEY_ALLOW_FREE_FORM_INPUT = "allowFreeFormInput"; private static final String KEY_ALLOWED_DATA_TYPES = "allowedDataTypes"; private static final String KEY_SEMANTIC_ACTION = "semanticAction"; private static final String KEY_SHOWS_USER_INTERFACE = "showsUserInterface"; private static final Object sExtrasLock = new Object(); private static Field sExtrasField; private static boolean sExtrasFieldAccessFailed; private static final Object sActionsLock = new Object(); private static Class sActionClass; private static Field sActionsField; private static Field sActionIconField; private static Field sActionTitleField; private static Field sActionIntentField; private static boolean sActionsAccessFailed; /** Return an SparseArray for action extras or null if none was needed. */ public static SparseArray buildActionExtrasMap(List actionExtrasList) { SparseArray actionExtrasMap = null; for (int i = 0, count = actionExtrasList.size(); i < count; i++) { Bundle actionExtras = actionExtrasList.get(i); if (actionExtras != null) { if (actionExtrasMap == null) { actionExtrasMap = new SparseArray(); } actionExtrasMap.put(i, actionExtras); } } return actionExtrasMap; } /** * Get the extras Bundle from a notification using reflection. Extras were present in * Jellybean notifications, but the field was private until KitKat. */ public static Bundle getExtras(Notification notif) { synchronized (sExtrasLock) { if (sExtrasFieldAccessFailed) { return null; } try { if (sExtrasField == null) { Field extrasField = Notification.class.getDeclaredField("extras"); if (!Bundle.class.isAssignableFrom(extrasField.getType())) { Log.e(TAG, "Notification.extras field is not of type Bundle"); sExtrasFieldAccessFailed = true; return null; } extrasField.setAccessible(true); sExtrasField = extrasField; } Bundle extras = (Bundle) sExtrasField.get(notif); if (extras == null) { extras = new Bundle(); sExtrasField.set(notif, extras); } return extras; } catch (IllegalAccessException e) { Log.e(TAG, "Unable to access notification extras", e); } catch (NoSuchFieldException e) { Log.e(TAG, "Unable to access notification extras", e); } sExtrasFieldAccessFailed = true; return null; } } public static NotificationCompat.Action readAction(int icon, CharSequence title, PendingIntent actionIntent, Bundle extras) { RemoteInput[] remoteInputs = null; RemoteInput[] dataOnlyRemoteInputs = null; boolean allowGeneratedReplies = false; if (extras != null) { remoteInputs = fromBundleArray( getBundleArrayFromBundle(extras, NotificationCompatExtras.EXTRA_REMOTE_INPUTS)); dataOnlyRemoteInputs = fromBundleArray( getBundleArrayFromBundle(extras, EXTRA_DATA_ONLY_REMOTE_INPUTS)); allowGeneratedReplies = extras.getBoolean(EXTRA_ALLOW_GENERATED_REPLIES); } return new NotificationCompat.Action(icon, title, actionIntent, extras, remoteInputs, dataOnlyRemoteInputs, allowGeneratedReplies, NotificationCompat.Action.SEMANTIC_ACTION_NONE, true); } public static Bundle writeActionAndGetExtras( Notification.Builder builder, NotificationCompat.Action action) { builder.addAction(action.getIcon(), action.getTitle(), action.getActionIntent()); Bundle actionExtras = new Bundle(action.getExtras()); if (action.getRemoteInputs() != null) { actionExtras.putParcelableArray(NotificationCompatExtras.EXTRA_REMOTE_INPUTS, toBundleArray(action.getRemoteInputs())); } if (action.getDataOnlyRemoteInputs() != null) { actionExtras.putParcelableArray(EXTRA_DATA_ONLY_REMOTE_INPUTS, toBundleArray(action.getDataOnlyRemoteInputs())); } actionExtras.putBoolean(EXTRA_ALLOW_GENERATED_REPLIES, action.getAllowGeneratedReplies()); return actionExtras; } public static int getActionCount(Notification notif) { synchronized (sActionsLock) { Object[] actionObjects = getActionObjectsLocked(notif); return actionObjects != null ? actionObjects.length : 0; } } public static NotificationCompat.Action getAction(Notification notif, int actionIndex) { synchronized (sActionsLock) { try { Object[] actionObjects = getActionObjectsLocked(notif); if (actionObjects != null) { Object actionObject = actionObjects[actionIndex]; Bundle actionExtras = null; Bundle extras = getExtras(notif); if (extras != null) { SparseArray actionExtrasMap = extras.getSparseParcelableArray( NotificationCompatExtras.EXTRA_ACTION_EXTRAS); if (actionExtrasMap != null) { actionExtras = actionExtrasMap.get(actionIndex); } } return readAction(sActionIconField.getInt(actionObject), (CharSequence) sActionTitleField.get(actionObject), (PendingIntent) sActionIntentField.get(actionObject), actionExtras); } } catch (IllegalAccessException e) { Log.e(TAG, "Unable to access notification actions", e); sActionsAccessFailed = true; } } return null; } private static Object[] getActionObjectsLocked(Notification notif) { synchronized (sActionsLock) { if (!ensureActionReflectionReadyLocked()) { return null; } try { return (Object[]) sActionsField.get(notif); } catch (IllegalAccessException e) { Log.e(TAG, "Unable to access notification actions", e); sActionsAccessFailed = true; return null; } } } @SuppressWarnings("LiteralClassName") private static boolean ensureActionReflectionReadyLocked() { if (sActionsAccessFailed) { return false; } try { if (sActionsField == null) { sActionClass = Class.forName("android.app.Notification$Action"); sActionIconField = sActionClass.getDeclaredField("icon"); sActionTitleField = sActionClass.getDeclaredField("title"); sActionIntentField = sActionClass.getDeclaredField("actionIntent"); sActionsField = Notification.class.getDeclaredField("actions"); sActionsField.setAccessible(true); } } catch (ClassNotFoundException e) { Log.e(TAG, "Unable to access notification actions", e); sActionsAccessFailed = true; } catch (NoSuchFieldException e) { Log.e(TAG, "Unable to access notification actions", e); sActionsAccessFailed = true; } return !sActionsAccessFailed; } static NotificationCompat.Action getActionFromBundle(Bundle bundle) { Bundle extras = bundle.getBundle(KEY_EXTRAS); boolean allowGeneratedReplies = false; if (extras != null) { allowGeneratedReplies = extras.getBoolean(EXTRA_ALLOW_GENERATED_REPLIES, false); } return new NotificationCompat.Action( bundle.getInt(KEY_ICON), bundle.getCharSequence(KEY_TITLE), bundle.getParcelable(KEY_ACTION_INTENT), bundle.getBundle(KEY_EXTRAS), fromBundleArray(getBundleArrayFromBundle(bundle, KEY_REMOTE_INPUTS)), fromBundleArray(getBundleArrayFromBundle(bundle, KEY_DATA_ONLY_REMOTE_INPUTS)), allowGeneratedReplies, bundle.getInt(KEY_SEMANTIC_ACTION), bundle.getBoolean(KEY_SHOWS_USER_INTERFACE)); } static Bundle getBundleForAction(NotificationCompat.Action action) { Bundle bundle = new Bundle(); bundle.putInt(KEY_ICON, action.getIcon()); bundle.putCharSequence(KEY_TITLE, action.getTitle()); bundle.putParcelable(KEY_ACTION_INTENT, action.getActionIntent()); Bundle actionExtras; if (action.getExtras() != null) { actionExtras = new Bundle(action.getExtras()); } else { actionExtras = new Bundle(); } actionExtras.putBoolean(NotificationCompatJellybean.EXTRA_ALLOW_GENERATED_REPLIES, action.getAllowGeneratedReplies()); bundle.putBundle(KEY_EXTRAS, actionExtras); bundle.putParcelableArray(KEY_REMOTE_INPUTS, toBundleArray(action.getRemoteInputs())); bundle.putBoolean(KEY_SHOWS_USER_INTERFACE, action.getShowsUserInterface()); bundle.putInt(KEY_SEMANTIC_ACTION, action.getSemanticAction()); return bundle; } private static RemoteInput fromBundle(Bundle data) { ArrayList allowedDataTypesAsList = data.getStringArrayList(KEY_ALLOWED_DATA_TYPES); Set allowedDataTypes = new HashSet<>(); if (allowedDataTypesAsList != null) { for (String type : allowedDataTypesAsList) { allowedDataTypes.add(type); } } return new RemoteInput(data.getString(KEY_RESULT_KEY), data.getCharSequence(KEY_LABEL), data.getCharSequenceArray(KEY_CHOICES), data.getBoolean(KEY_ALLOW_FREE_FORM_INPUT), data.getBundle(KEY_EXTRAS), allowedDataTypes); } private static Bundle toBundle(RemoteInput remoteInput) { Bundle data = new Bundle(); data.putString(KEY_RESULT_KEY, remoteInput.getResultKey()); data.putCharSequence(KEY_LABEL, remoteInput.getLabel()); data.putCharSequenceArray(KEY_CHOICES, remoteInput.getChoices()); data.putBoolean(KEY_ALLOW_FREE_FORM_INPUT, remoteInput.getAllowFreeFormInput()); data.putBundle(KEY_EXTRAS, remoteInput.getExtras()); Set allowedDataTypes = remoteInput.getAllowedDataTypes(); if (allowedDataTypes != null && !allowedDataTypes.isEmpty()) { ArrayList allowedDataTypesAsList = new ArrayList<>(allowedDataTypes.size()); for (String type : allowedDataTypes) { allowedDataTypesAsList.add(type); } data.putStringArrayList(KEY_ALLOWED_DATA_TYPES, allowedDataTypesAsList); } return data; } private static RemoteInput[] fromBundleArray(Bundle[] bundles) { if (bundles == null) { return null; } RemoteInput[] remoteInputs = new RemoteInput[bundles.length]; for (int i = 0; i < bundles.length; i++) { remoteInputs[i] = fromBundle(bundles[i]); } return remoteInputs; } private static Bundle[] toBundleArray(RemoteInput[] remoteInputs) { if (remoteInputs == null) { return null; } Bundle[] bundles = new Bundle[remoteInputs.length]; for (int i = 0; i < remoteInputs.length; i++) { bundles[i] = toBundle(remoteInputs[i]); } return bundles; } /** * Get an array of Bundle objects from a parcelable array field in a bundle. * Update the bundle to have a typed array so fetches in the future don't need * to do an array copy. */ private static Bundle[] getBundleArrayFromBundle(Bundle bundle, String key) { Parcelable[] array = bundle.getParcelableArray(key); if (array instanceof Bundle[] || array == null) { return (Bundle[]) array; } Bundle[] typedArray = Arrays.copyOf(array, array.length, Bundle[].class); bundle.putParcelableArray(key, typedArray); return typedArray; } private NotificationCompatJellybean() { } }