blob: f334fc15df1ee738509da85c361b5f1638f44cae [file] [log] [blame]
/*
* 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<Bundle> buildActionExtrasMap(List<Bundle> actionExtrasList) {
SparseArray<Bundle> 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<Bundle>();
}
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<Bundle> 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.<PendingIntent>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<String> allowedDataTypesAsList = data.getStringArrayList(KEY_ALLOWED_DATA_TYPES);
Set<String> 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<String> allowedDataTypes = remoteInput.getAllowedDataTypes();
if (allowedDataTypes != null && !allowedDataTypes.isEmpty()) {
ArrayList<String> 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() {
}
}