Home | History | Annotate | Download | only in app
      1 /*
      2  * Copyright (C) 2014 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.content.ClipData;
     20 import android.content.ClipDescription;
     21 import android.content.Intent;
     22 import android.net.Uri;
     23 import android.os.Build;
     24 import android.os.Bundle;
     25 import android.util.Log;
     26 
     27 import androidx.annotation.RequiresApi;
     28 
     29 import java.util.HashMap;
     30 import java.util.HashSet;
     31 import java.util.Map;
     32 import java.util.Set;
     33 
     34 /**
     35  * Helper for using the {@link android.app.RemoteInput}.
     36  */
     37 public final class RemoteInput {
     38     private static final String TAG = "RemoteInput";
     39 
     40     /** Label used to denote the clip data type used for remote input transport */
     41     public static final String RESULTS_CLIP_LABEL = "android.remoteinput.results";
     42 
     43     /** Extra added to a clip data intent object to hold the text results bundle. */
     44     public static final String EXTRA_RESULTS_DATA = "android.remoteinput.resultsData";
     45 
     46     /** Extra added to a clip data intent object to hold the data results bundle. */
     47     private static final String EXTRA_DATA_TYPE_RESULTS_DATA =
     48             "android.remoteinput.dataTypeResultsData";
     49 
     50     private final String mResultKey;
     51     private final CharSequence mLabel;
     52     private final CharSequence[] mChoices;
     53     private final boolean mAllowFreeFormTextInput;
     54     private final Bundle mExtras;
     55     private final Set<String> mAllowedDataTypes;
     56 
     57     RemoteInput(String resultKey, CharSequence label, CharSequence[] choices,
     58             boolean allowFreeFormTextInput, Bundle extras, Set<String> allowedDataTypes) {
     59         this.mResultKey = resultKey;
     60         this.mLabel = label;
     61         this.mChoices = choices;
     62         this.mAllowFreeFormTextInput = allowFreeFormTextInput;
     63         this.mExtras = extras;
     64         this.mAllowedDataTypes = allowedDataTypes;
     65     }
     66 
     67     /**
     68      * Get the key that the result of this input will be set in from the Bundle returned by
     69      * {@link #getResultsFromIntent} when the {@link android.app.PendingIntent} is sent.
     70      */
     71     public String getResultKey() {
     72         return mResultKey;
     73     }
     74 
     75     /**
     76      * Get the label to display to users when collecting this input.
     77      */
     78     public CharSequence getLabel() {
     79         return mLabel;
     80     }
     81 
     82     /**
     83      * Get possible input choices. This can be {@code null} if there are no choices to present.
     84      */
     85     public CharSequence[] getChoices() {
     86         return mChoices;
     87     }
     88 
     89     public Set<String> getAllowedDataTypes() {
     90         return mAllowedDataTypes;
     91     }
     92 
     93     /**
     94      * Returns true if the input only accepts data, meaning {@link #getAllowFreeFormInput}
     95      * is false, {@link #getChoices} is null or empty, and {@link #getAllowedDataTypes is
     96      * non-null and not empty.
     97      */
     98     public boolean isDataOnly() {
     99         return !getAllowFreeFormInput()
    100                 && (getChoices() == null || getChoices().length == 0)
    101                 && getAllowedDataTypes() != null
    102                 && !getAllowedDataTypes().isEmpty();
    103     }
    104 
    105     /**
    106      * Get whether or not users can provide an arbitrary value for
    107      * input. If you set this to {@code false}, users must select one of the
    108      * choices in {@link #getChoices}. An {@link IllegalArgumentException} is thrown
    109      * if you set this to false and {@link #getChoices} returns {@code null} or empty.
    110      */
    111     public boolean getAllowFreeFormInput() {
    112         return mAllowFreeFormTextInput;
    113     }
    114 
    115     /**
    116      * Get additional metadata carried around with this remote input.
    117      */
    118     public Bundle getExtras() {
    119         return mExtras;
    120     }
    121 
    122     /**
    123      * Builder class for {@link androidx.core.app.RemoteInput} objects.
    124      */
    125     public static final class Builder {
    126         private final String mResultKey;
    127         private CharSequence mLabel;
    128         private CharSequence[] mChoices;
    129         private boolean mAllowFreeFormTextInput = true;
    130         private Bundle mExtras = new Bundle();
    131         private final Set<String> mAllowedDataTypes = new HashSet<>();
    132 
    133         /**
    134          * Create a builder object for {@link androidx.core.app.RemoteInput} objects.
    135          * @param resultKey the Bundle key that refers to this input when collected from the user
    136          */
    137         public Builder(String resultKey) {
    138             if (resultKey == null) {
    139                 throw new IllegalArgumentException("Result key can't be null");
    140             }
    141             mResultKey = resultKey;
    142         }
    143 
    144         /**
    145          * Set a label to be displayed to the user when collecting this input.
    146          * @param label The label to show to users when they input a response.
    147          * @return this object for method chaining
    148          */
    149         public Builder setLabel(CharSequence label) {
    150             mLabel = label;
    151             return this;
    152         }
    153 
    154         /**
    155          * Specifies choices available to the user to satisfy this input.
    156          * @param choices an array of pre-defined choices for users input.
    157          *        You must provide a non-null and non-empty array if
    158          *        you disabled free form input using {@link #setAllowFreeFormInput}.
    159          * @return this object for method chaining
    160          */
    161         public Builder setChoices(CharSequence[] choices) {
    162             mChoices = choices;
    163             return this;
    164         }
    165 
    166         /**
    167          * Specifies whether the user can provide arbitrary values.
    168          *
    169          * @param mimeType A mime type that results are allowed to come in.
    170          *         Be aware that text results (see {@link #setAllowFreeFormInput}
    171          *         are allowed by default. If you do not want text results you will have to
    172          *         pass false to {@code setAllowFreeFormInput}.
    173          * @param doAllow Whether the mime type should be allowed or not.
    174          * @return this object for method chaining
    175          */
    176         public Builder setAllowDataType(String mimeType, boolean doAllow) {
    177             if (doAllow) {
    178                 mAllowedDataTypes.add(mimeType);
    179             } else {
    180                 mAllowedDataTypes.remove(mimeType);
    181             }
    182             return this;
    183         }
    184 
    185         /**
    186          * Specifies whether the user can provide arbitrary text values.
    187          *
    188          * @param allowFreeFormTextInput The default is {@code true}.
    189          *         If you specify {@code false}, you must either provide a non-null
    190          *         and non-empty array to {@link #setChoices}, or enable a data result
    191          *         in {@code setAllowDataType}. Otherwise an
    192          *         {@link IllegalArgumentException} is thrown.
    193          * @return this object for method chaining
    194          */
    195         public Builder setAllowFreeFormInput(boolean allowFreeFormTextInput) {
    196             mAllowFreeFormTextInput = allowFreeFormTextInput;
    197             return this;
    198         }
    199 
    200         /**
    201          * Merge additional metadata into this builder.
    202          *
    203          * <p>Values within the Bundle will replace existing extras values in this Builder.
    204          *
    205          * @see RemoteInput#getExtras
    206          */
    207         public Builder addExtras(Bundle extras) {
    208             if (extras != null) {
    209                 mExtras.putAll(extras);
    210             }
    211             return this;
    212         }
    213 
    214         /**
    215          * Get the metadata Bundle used by this Builder.
    216          *
    217          * <p>The returned Bundle is shared with this Builder.
    218          */
    219         public Bundle getExtras() {
    220             return mExtras;
    221         }
    222 
    223         /**
    224          * Combine all of the options that have been set and return a new
    225          * {@link androidx.core.app.RemoteInput} object.
    226          */
    227         public RemoteInput build() {
    228             return new RemoteInput(
    229                     mResultKey,
    230                     mLabel,
    231                     mChoices,
    232                     mAllowFreeFormTextInput,
    233                     mExtras,
    234                     mAllowedDataTypes);
    235         }
    236     }
    237 
    238     /**
    239      * Similar as {@link #getResultsFromIntent} but retrieves data results for a
    240      * specific RemoteInput result. To retrieve a value use:
    241      * <pre>
    242      * {@code
    243      * Map<String, Uri> results =
    244      *     RemoteInput.getDataResultsFromIntent(intent, REMOTE_INPUT_KEY);
    245      * if (results != null) {
    246      *   Uri data = results.get(MIME_TYPE_OF_INTEREST);
    247      * }
    248      * }
    249      * </pre>
    250      * @param intent The intent object that fired in response to an action or content intent
    251      *               which also had one or more remote input requested.
    252      * @param remoteInputResultKey The result key for the RemoteInput you want results for.
    253      */
    254     public static Map<String, Uri> getDataResultsFromIntent(
    255             Intent intent, String remoteInputResultKey) {
    256         if (Build.VERSION.SDK_INT >= 26) {
    257             return android.app.RemoteInput.getDataResultsFromIntent(intent, remoteInputResultKey);
    258         } else if (Build.VERSION.SDK_INT >= 16) {
    259             Intent clipDataIntent = getClipDataIntentFromIntent(intent);
    260             if (clipDataIntent == null) {
    261                 return null;
    262             }
    263             Map<String, Uri> results = new HashMap<>();
    264             Bundle extras = clipDataIntent.getExtras();
    265             for (String key : extras.keySet()) {
    266                 if (key.startsWith(EXTRA_DATA_TYPE_RESULTS_DATA)) {
    267                     String mimeType = key.substring(EXTRA_DATA_TYPE_RESULTS_DATA.length());
    268                     if (mimeType.isEmpty()) {
    269                         continue;
    270                     }
    271                     Bundle bundle = clipDataIntent.getBundleExtra(key);
    272                     String uriStr = bundle.getString(remoteInputResultKey);
    273                     if (uriStr == null || uriStr.isEmpty()) {
    274                         continue;
    275                     }
    276                     results.put(mimeType, Uri.parse(uriStr));
    277                 }
    278             }
    279             return results.isEmpty() ? null : results;
    280         } else {
    281             Log.w(TAG, "RemoteInput is only supported from API Level 16");
    282             return null;
    283         }
    284     }
    285 
    286     /**
    287      * Get the remote input text results bundle from an intent. The returned Bundle will
    288      * contain a key/value for every result key populated by remote input collector.
    289      * Use the {@link Bundle#getCharSequence(String)} method to retrieve a value. For data results
    290      * use {@link #getDataResultsFromIntent}.
    291      * @param intent The intent object that fired in response to an action or content intent
    292      *               which also had one or more remote input requested.
    293      */
    294     public static Bundle getResultsFromIntent(Intent intent) {
    295         if (Build.VERSION.SDK_INT >= 20) {
    296             return android.app.RemoteInput.getResultsFromIntent(intent);
    297         } else if (Build.VERSION.SDK_INT >= 16) {
    298             Intent clipDataIntent = getClipDataIntentFromIntent(intent);
    299             if (clipDataIntent == null) {
    300                 return null;
    301             }
    302             return clipDataIntent.getExtras().getParcelable(RemoteInput.EXTRA_RESULTS_DATA);
    303         } else {
    304             Log.w(TAG, "RemoteInput is only supported from API Level 16");
    305             return null;
    306         }
    307     }
    308 
    309     /**
    310      * Populate an intent object with the results gathered from remote input. This method
    311      * should only be called by remote input collection services when sending results to a
    312      * pending intent.
    313      * @param remoteInputs The remote inputs for which results are being provided
    314      * @param intent The intent to add remote inputs to. The {@link android.content.ClipData}
    315      *               field of the intent will be modified to contain the results.
    316      * @param results A bundle holding the remote input results. This bundle should
    317      *                be populated with keys matching the result keys specified in
    318      *                {@code remoteInputs} with values being the result per key.
    319      */
    320     public static void addResultsToIntent(RemoteInput[] remoteInputs, Intent intent,
    321             Bundle results) {
    322         if (Build.VERSION.SDK_INT >= 26) {
    323             android.app.RemoteInput.addResultsToIntent(fromCompat(remoteInputs), intent, results);
    324         } else if (Build.VERSION.SDK_INT >= 20) {
    325             // Implementations of RemoteInput#addResultsToIntent prior to SDK 26 don't actually add
    326             // results, they wipe out old results and insert the new one. Work around that by
    327             // preserving old results.
    328             Bundle existingTextResults =
    329                     androidx.core.app.RemoteInput.getResultsFromIntent(intent);
    330             if (existingTextResults == null) {
    331                 existingTextResults = results;
    332             } else {
    333                 existingTextResults.putAll(results);
    334             }
    335             for (RemoteInput input : remoteInputs) {
    336                 // Data results are also wiped out. So grab them and add them back in.
    337                 Map<String, Uri> existingDataResults =
    338                         androidx.core.app.RemoteInput.getDataResultsFromIntent(
    339                                 intent, input.getResultKey());
    340                 RemoteInput[] arr = new RemoteInput[1];
    341                 arr[0] = input;
    342                 android.app.RemoteInput.addResultsToIntent(
    343                         fromCompat(arr), intent, existingTextResults);
    344                 if (existingDataResults != null) {
    345                     RemoteInput.addDataResultToIntent(input, intent, existingDataResults);
    346                 }
    347             }
    348         } else if (Build.VERSION.SDK_INT >= 16) {
    349             Intent clipDataIntent = getClipDataIntentFromIntent(intent);
    350             if (clipDataIntent == null) {
    351                 clipDataIntent = new Intent();  // First time we've added a result.
    352             }
    353             Bundle resultsBundle = clipDataIntent.getBundleExtra(RemoteInput.EXTRA_RESULTS_DATA);
    354             if (resultsBundle == null) {
    355                 resultsBundle = new Bundle();
    356             }
    357             for (RemoteInput remoteInput : remoteInputs) {
    358                 Object result = results.get(remoteInput.getResultKey());
    359                 if (result instanceof CharSequence) {
    360                     resultsBundle.putCharSequence(
    361                             remoteInput.getResultKey(), (CharSequence) result);
    362                 }
    363             }
    364             clipDataIntent.putExtra(RemoteInput.EXTRA_RESULTS_DATA, resultsBundle);
    365             intent.setClipData(ClipData.newIntent(RemoteInput.RESULTS_CLIP_LABEL, clipDataIntent));
    366         } else {
    367             Log.w(TAG, "RemoteInput is only supported from API Level 16");
    368         }
    369     }
    370 
    371     /**
    372      * Same as {@link #addResultsToIntent} but for setting data results.
    373      * @param remoteInput The remote input for which results are being provided
    374      * @param intent The intent to add remote input results to. The
    375      *               {@link android.content.ClipData} field of the intent will be
    376      *               modified to contain the results.
    377      * @param results A map of mime type to the Uri result for that mime type.
    378      */
    379     public static void addDataResultToIntent(RemoteInput remoteInput, Intent intent,
    380             Map<String, Uri> results) {
    381         if (Build.VERSION.SDK_INT >= 26) {
    382             android.app.RemoteInput.addDataResultToIntent(fromCompat(remoteInput), intent, results);
    383         } else if (Build.VERSION.SDK_INT >= 16) {
    384             Intent clipDataIntent = getClipDataIntentFromIntent(intent);
    385             if (clipDataIntent == null) {
    386                 clipDataIntent = new Intent();  // First time we've added a result.
    387             }
    388             for (Map.Entry<String, Uri> entry : results.entrySet()) {
    389                 String mimeType = entry.getKey();
    390                 Uri uri = entry.getValue();
    391                 if (mimeType == null) {
    392                     continue;
    393                 }
    394                 Bundle resultsBundle =
    395                         clipDataIntent.getBundleExtra(getExtraResultsKeyForData(mimeType));
    396                 if (resultsBundle == null) {
    397                     resultsBundle = new Bundle();
    398                 }
    399                 resultsBundle.putString(remoteInput.getResultKey(), uri.toString());
    400                 clipDataIntent.putExtra(getExtraResultsKeyForData(mimeType), resultsBundle);
    401             }
    402             intent.setClipData(ClipData.newIntent(RemoteInput.RESULTS_CLIP_LABEL, clipDataIntent));
    403         } else {
    404             Log.w(TAG, "RemoteInput is only supported from API Level 16");
    405         }
    406     }
    407 
    408     private static String getExtraResultsKeyForData(String mimeType) {
    409         return EXTRA_DATA_TYPE_RESULTS_DATA + mimeType;
    410     }
    411 
    412     @RequiresApi(20)
    413     static android.app.RemoteInput[] fromCompat(RemoteInput[] srcArray) {
    414         if (srcArray == null) {
    415             return null;
    416         }
    417         android.app.RemoteInput[] result = new android.app.RemoteInput[srcArray.length];
    418         for (int i = 0; i < srcArray.length; i++) {
    419             result[i] = fromCompat(srcArray[i]);
    420         }
    421         return result;
    422     }
    423 
    424     @RequiresApi(20)
    425     static android.app.RemoteInput fromCompat(RemoteInput src) {
    426         return new android.app.RemoteInput.Builder(src.getResultKey())
    427                 .setLabel(src.getLabel())
    428                 .setChoices(src.getChoices())
    429                 .setAllowFreeFormInput(src.getAllowFreeFormInput())
    430                 .addExtras(src.getExtras())
    431                 .build();
    432     }
    433 
    434     @RequiresApi(16)
    435     private static Intent getClipDataIntentFromIntent(Intent intent) {
    436         ClipData clipData = intent.getClipData();
    437         if (clipData == null) {
    438             return null;
    439         }
    440         ClipDescription clipDescription = clipData.getDescription();
    441         if (!clipDescription.hasMimeType(ClipDescription.MIMETYPE_TEXT_INTENT)) {
    442             return null;
    443         }
    444         if (!clipDescription.getLabel().equals(RemoteInput.RESULTS_CLIP_LABEL)) {
    445             return null;
    446         }
    447         return clipData.getItemAt(0).getIntent();
    448     }
    449 }
    450