Home | History | Annotate | Download | only in app
      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