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 android.app;
     18 
     19 import android.annotation.IntDef;
     20 import android.annotation.NonNull;
     21 import android.annotation.Nullable;
     22 import android.content.ClipData;
     23 import android.content.ClipDescription;
     24 import android.content.Intent;
     25 import android.net.Uri;
     26 import android.os.Bundle;
     27 import android.os.Parcel;
     28 import android.os.Parcelable;
     29 import android.util.ArraySet;
     30 
     31 import java.lang.annotation.Retention;
     32 import java.lang.annotation.RetentionPolicy;
     33 import java.util.HashMap;
     34 import java.util.Map;
     35 import java.util.Set;
     36 
     37 /**
     38  * A {@code RemoteInput} object specifies input to be collected from a user to be passed along with
     39  * an intent inside a {@link android.app.PendingIntent} that is sent.
     40  * Always use {@link RemoteInput.Builder} to create instances of this class.
     41  * <p class="note"> See
     42  * <a href="{@docRoot}guide/topics/ui/notifiers/notifications.html#direct">Replying
     43  * to notifications</a> for more information on how to use this class.
     44  *
     45  * <p>The following example adds a {@code RemoteInput} to a {@link Notification.Action},
     46  * sets the result key as {@code quick_reply}, and sets the label as {@code Quick reply}.
     47  * Users are prompted to input a response when they trigger the action. The results are sent along
     48  * with the intent and can be retrieved with the result key (provided to the {@link Builder}
     49  * constructor) from the Bundle returned by {@link #getResultsFromIntent}.
     50  *
     51  * <pre class="prettyprint">
     52  * public static final String KEY_QUICK_REPLY_TEXT = "quick_reply";
     53  * Notification.Action action = new Notification.Action.Builder(
     54  *         R.drawable.reply, &quot;Reply&quot;, actionIntent)
     55  *         <b>.addRemoteInput(new RemoteInput.Builder(KEY_QUICK_REPLY_TEXT)
     56  *                 .setLabel("Quick reply").build()</b>)
     57  *         .build();</pre>
     58  *
     59  * <p>When the {@link android.app.PendingIntent} is fired, the intent inside will contain the
     60  * input results if collected. To access these results, use the {@link #getResultsFromIntent}
     61  * function. The result values will present under the result key passed to the {@link Builder}
     62  * constructor.
     63  *
     64  * <pre class="prettyprint">
     65  * public static final String KEY_QUICK_REPLY_TEXT = "quick_reply";
     66  * Bundle results = RemoteInput.getResultsFromIntent(intent);
     67  * if (results != null) {
     68  *     CharSequence quickReplyResult = results.getCharSequence(KEY_QUICK_REPLY_TEXT);
     69  * }</pre>
     70  */
     71 public final class RemoteInput implements Parcelable {
     72     /** Label used to denote the clip data type used for remote input transport */
     73     public static final String RESULTS_CLIP_LABEL = "android.remoteinput.results";
     74 
     75     /** Extra added to a clip data intent object to hold the text results bundle. */
     76     public static final String EXTRA_RESULTS_DATA = "android.remoteinput.resultsData";
     77 
     78     /** Extra added to a clip data intent object to hold the data results bundle. */
     79     private static final String EXTRA_DATA_TYPE_RESULTS_DATA =
     80             "android.remoteinput.dataTypeResultsData";
     81 
     82     /** Extra added to a clip data intent object identifying the {@link Source} of the results. */
     83     private static final String EXTRA_RESULTS_SOURCE = "android.remoteinput.resultsSource";
     84 
     85     /** @hide */
     86     @IntDef(prefix = {"SOURCE_"}, value = {SOURCE_FREE_FORM_INPUT, SOURCE_CHOICE})
     87     @Retention(RetentionPolicy.SOURCE)
     88     public @interface Source {}
     89 
     90     /** The user manually entered the data. */
     91     public static final int SOURCE_FREE_FORM_INPUT = 0;
     92 
     93     /** The user selected one of the choices from {@link #getChoices}. */
     94     public static final int SOURCE_CHOICE = 1;
     95 
     96     // Flags bitwise-ored to mFlags
     97     private static final int FLAG_ALLOW_FREE_FORM_INPUT = 0x1;
     98 
     99     // Default value for flags integer
    100     private static final int DEFAULT_FLAGS = FLAG_ALLOW_FREE_FORM_INPUT;
    101 
    102     private final String mResultKey;
    103     private final CharSequence mLabel;
    104     private final CharSequence[] mChoices;
    105     private final int mFlags;
    106     private final Bundle mExtras;
    107     private final ArraySet<String> mAllowedDataTypes;
    108 
    109     private RemoteInput(String resultKey, CharSequence label, CharSequence[] choices,
    110             int flags, Bundle extras, ArraySet<String> allowedDataTypes) {
    111         this.mResultKey = resultKey;
    112         this.mLabel = label;
    113         this.mChoices = choices;
    114         this.mFlags = flags;
    115         this.mExtras = extras;
    116         this.mAllowedDataTypes = allowedDataTypes;
    117     }
    118 
    119     /**
    120      * Get the key that the result of this input will be set in from the Bundle returned by
    121      * {@link #getResultsFromIntent} when the {@link android.app.PendingIntent} is sent.
    122      */
    123     public String getResultKey() {
    124         return mResultKey;
    125     }
    126 
    127     /**
    128      * Get the label to display to users when collecting this input.
    129      */
    130     public CharSequence getLabel() {
    131         return mLabel;
    132     }
    133 
    134     /**
    135      * Get possible input choices. This can be {@code null} if there are no choices to present.
    136      */
    137     public CharSequence[] getChoices() {
    138         return mChoices;
    139     }
    140 
    141     /**
    142      * Get possible non-textual inputs that are accepted.
    143      * This can be {@code null} if the input does not accept non-textual values.
    144      * See {@link Builder#setAllowDataType}.
    145      */
    146     public Set<String> getAllowedDataTypes() {
    147         return mAllowedDataTypes;
    148     }
    149 
    150     /**
    151      * Returns true if the input only accepts data, meaning {@link #getAllowFreeFormInput}
    152      * is false, {@link #getChoices} is null or empty, and {@link #getAllowedDataTypes is
    153      * non-null and not empty.
    154      */
    155     public boolean isDataOnly() {
    156         return !getAllowFreeFormInput()
    157                 && (getChoices() == null || getChoices().length == 0)
    158                 && !getAllowedDataTypes().isEmpty();
    159     }
    160 
    161     /**
    162      * Get whether or not users can provide an arbitrary value for
    163      * input. If you set this to {@code false}, users must select one of the
    164      * choices in {@link #getChoices}. An {@link IllegalArgumentException} is thrown
    165      * if you set this to false and {@link #getChoices} returns {@code null} or empty.
    166      */
    167     public boolean getAllowFreeFormInput() {
    168         return (mFlags & FLAG_ALLOW_FREE_FORM_INPUT) != 0;
    169     }
    170 
    171     /**
    172      * Get additional metadata carried around with this remote input.
    173      */
    174     public Bundle getExtras() {
    175         return mExtras;
    176     }
    177 
    178     /**
    179      * Builder class for {@link RemoteInput} objects.
    180      */
    181     public static final class Builder {
    182         private final String mResultKey;
    183         private final ArraySet<String> mAllowedDataTypes = new ArraySet<>();
    184         private final Bundle mExtras = new Bundle();
    185         private CharSequence mLabel;
    186         private CharSequence[] mChoices;
    187         private int mFlags = DEFAULT_FLAGS;
    188 
    189         /**
    190          * Create a builder object for {@link RemoteInput} objects.
    191          *
    192          * @param resultKey the Bundle key that refers to this input when collected from the user
    193          */
    194         public Builder(@NonNull String resultKey) {
    195             if (resultKey == null) {
    196                 throw new IllegalArgumentException("Result key can't be null");
    197             }
    198             mResultKey = resultKey;
    199         }
    200 
    201         /**
    202          * Set a label to be displayed to the user when collecting this input.
    203          *
    204          * @param label The label to show to users when they input a response
    205          * @return this object for method chaining
    206          */
    207         @NonNull
    208         public Builder setLabel(@Nullable CharSequence label) {
    209             mLabel = Notification.safeCharSequence(label);
    210             return this;
    211         }
    212 
    213         /**
    214          * Specifies choices available to the user to satisfy this input.
    215          *
    216          * <p>Note: Starting in Android P, these choices will always be shown on phones if the app's
    217          * target SDK is >= P. However, these choices may also be rendered on other types of devices
    218          * regardless of target SDK.
    219          *
    220          * @param choices an array of pre-defined choices for users input.
    221          *        You must provide a non-null and non-empty array if
    222          *        you disabled free form input using {@link #setAllowFreeFormInput}
    223          * @return this object for method chaining
    224          */
    225         @NonNull
    226         public Builder setChoices(@Nullable CharSequence[] choices) {
    227             if (choices == null) {
    228                 mChoices = null;
    229             } else {
    230                 mChoices = new CharSequence[choices.length];
    231                 for (int i = 0; i < choices.length; i++) {
    232                     mChoices[i] = Notification.safeCharSequence(choices[i]);
    233                 }
    234             }
    235             return this;
    236         }
    237 
    238         /**
    239          * Specifies whether the user can provide arbitrary values. This allows an input
    240          * to accept non-textual values. Examples of usage are an input that wants audio
    241          * or an image.
    242          *
    243          * @param mimeType A mime type that results are allowed to come in.
    244          *         Be aware that text results (see {@link #setAllowFreeFormInput}
    245          *         are allowed by default. If you do not want text results you will have to
    246          *         pass false to {@code setAllowFreeFormInput}
    247          * @param doAllow Whether the mime type should be allowed or not
    248          * @return this object for method chaining
    249          */
    250         @NonNull
    251         public Builder setAllowDataType(@NonNull String mimeType, boolean doAllow) {
    252             if (doAllow) {
    253                 mAllowedDataTypes.add(mimeType);
    254             } else {
    255                 mAllowedDataTypes.remove(mimeType);
    256             }
    257             return this;
    258         }
    259 
    260         /**
    261          * Specifies whether the user can provide arbitrary text values.
    262          *
    263          * @param allowFreeFormTextInput The default is {@code true}.
    264          *         If you specify {@code false}, you must either provide a non-null
    265          *         and non-empty array to {@link #setChoices}, or enable a data result
    266          *         in {@code setAllowDataType}. Otherwise an
    267          *         {@link IllegalArgumentException} is thrown
    268          * @return this object for method chaining
    269          */
    270         @NonNull
    271         public Builder setAllowFreeFormInput(boolean allowFreeFormTextInput) {
    272             setFlag(mFlags, allowFreeFormTextInput);
    273             return this;
    274         }
    275 
    276         /**
    277          * Merge additional metadata into this builder.
    278          *
    279          * <p>Values within the Bundle will replace existing extras values in this Builder.
    280          *
    281          * @see RemoteInput#getExtras
    282          */
    283         @NonNull
    284         public Builder addExtras(@NonNull Bundle extras) {
    285             if (extras != null) {
    286                 mExtras.putAll(extras);
    287             }
    288             return this;
    289         }
    290 
    291         /**
    292          * Get the metadata Bundle used by this Builder.
    293          *
    294          * <p>The returned Bundle is shared with this Builder.
    295          */
    296         @NonNull
    297         public Bundle getExtras() {
    298             return mExtras;
    299         }
    300 
    301         private void setFlag(int mask, boolean value) {
    302             if (value) {
    303                 mFlags |= mask;
    304             } else {
    305                 mFlags &= ~mask;
    306             }
    307         }
    308 
    309         /**
    310          * Combine all of the options that have been set and return a new {@link RemoteInput}
    311          * object.
    312          */
    313         @NonNull
    314         public RemoteInput build() {
    315             return new RemoteInput(
    316                     mResultKey, mLabel, mChoices, mFlags, mExtras, mAllowedDataTypes);
    317         }
    318     }
    319 
    320     private RemoteInput(Parcel in) {
    321         mResultKey = in.readString();
    322         mLabel = in.readCharSequence();
    323         mChoices = in.readCharSequenceArray();
    324         mFlags = in.readInt();
    325         mExtras = in.readBundle();
    326         mAllowedDataTypes = (ArraySet<String>) in.readArraySet(null);
    327     }
    328 
    329     /**
    330      * Similar as {@link #getResultsFromIntent} but retrieves data results for a
    331      * specific RemoteInput result. To retrieve a value use:
    332      * <pre>
    333      * {@code
    334      * Map<String, Uri> results =
    335      *     RemoteInput.getDataResultsFromIntent(intent, REMOTE_INPUT_KEY);
    336      * if (results != null) {
    337      *   Uri data = results.get(MIME_TYPE_OF_INTEREST);
    338      * }
    339      * }
    340      * </pre>
    341      * @param intent The intent object that fired in response to an action or content intent
    342      *               which also had one or more remote input requested.
    343      * @param remoteInputResultKey The result key for the RemoteInput you want results for.
    344      */
    345     public static Map<String, Uri> getDataResultsFromIntent(
    346             Intent intent, String remoteInputResultKey) {
    347         Intent clipDataIntent = getClipDataIntentFromIntent(intent);
    348         if (clipDataIntent == null) {
    349             return null;
    350         }
    351         Map<String, Uri> results = new HashMap<>();
    352         Bundle extras = clipDataIntent.getExtras();
    353         for (String key : extras.keySet()) {
    354           if (key.startsWith(EXTRA_DATA_TYPE_RESULTS_DATA)) {
    355               String mimeType = key.substring(EXTRA_DATA_TYPE_RESULTS_DATA.length());
    356               if (mimeType == null || mimeType.isEmpty()) {
    357                   continue;
    358               }
    359               Bundle bundle = clipDataIntent.getBundleExtra(key);
    360               String uriStr = bundle.getString(remoteInputResultKey);
    361               if (uriStr == null || uriStr.isEmpty()) {
    362                   continue;
    363               }
    364               results.put(mimeType, Uri.parse(uriStr));
    365           }
    366         }
    367         return results.isEmpty() ? null : results;
    368     }
    369 
    370     /**
    371      * Get the remote input text results bundle from an intent. The returned Bundle will
    372      * contain a key/value for every result key populated with text by remote input collector.
    373      * Use the {@link Bundle#getCharSequence(String)} method to retrieve a value. For non-text
    374      * results use {@link #getDataResultsFromIntent}.
    375      * @param intent The intent object that fired in response to an action or content intent
    376      *               which also had one or more remote input requested.
    377      */
    378     public static Bundle getResultsFromIntent(Intent intent) {
    379         Intent clipDataIntent = getClipDataIntentFromIntent(intent);
    380         if (clipDataIntent == null) {
    381             return null;
    382         }
    383         return clipDataIntent.getExtras().getParcelable(EXTRA_RESULTS_DATA);
    384     }
    385 
    386     /**
    387      * Populate an intent object with the text results gathered from remote input. This method
    388      * should only be called by remote input collection services when sending results to a
    389      * pending intent.
    390      * @param remoteInputs The remote inputs for which results are being provided
    391      * @param intent The intent to add remote inputs to. The {@link ClipData}
    392      *               field of the intent will be modified to contain the results.
    393      * @param results A bundle holding the remote input results. This bundle should
    394      *                be populated with keys matching the result keys specified in
    395      *                {@code remoteInputs} with values being the CharSequence results per key.
    396      */
    397     public static void addResultsToIntent(RemoteInput[] remoteInputs, Intent intent,
    398             Bundle results) {
    399         Intent clipDataIntent = getClipDataIntentFromIntent(intent);
    400         if (clipDataIntent == null) {
    401             clipDataIntent = new Intent();  // First time we've added a result.
    402         }
    403         Bundle resultsBundle = clipDataIntent.getBundleExtra(EXTRA_RESULTS_DATA);
    404         if (resultsBundle == null) {
    405             resultsBundle = new Bundle();
    406         }
    407         for (RemoteInput remoteInput : remoteInputs) {
    408             Object result = results.get(remoteInput.getResultKey());
    409             if (result instanceof CharSequence) {
    410                 resultsBundle.putCharSequence(remoteInput.getResultKey(), (CharSequence) result);
    411             }
    412         }
    413         clipDataIntent.putExtra(EXTRA_RESULTS_DATA, resultsBundle);
    414         intent.setClipData(ClipData.newIntent(RESULTS_CLIP_LABEL, clipDataIntent));
    415     }
    416 
    417     /**
    418      * Same as {@link #addResultsToIntent} but for setting data results. This is used
    419      * for inputs that accept non-textual results (see {@link Builder#setAllowDataType}).
    420      * Only one result can be provided for every mime type accepted by the RemoteInput.
    421      * If multiple inputs of the same mime type are expected then multiple RemoteInputs
    422      * should be used.
    423      *
    424      * @param remoteInput The remote input for which results are being provided
    425      * @param intent The intent to add remote input results to. The {@link ClipData}
    426      *               field of the intent will be modified to contain the results.
    427      * @param results A map of mime type to the Uri result for that mime type.
    428      */
    429     public static void addDataResultToIntent(RemoteInput remoteInput, Intent intent,
    430             Map<String, Uri> results) {
    431         Intent clipDataIntent = getClipDataIntentFromIntent(intent);
    432         if (clipDataIntent == null) {
    433             clipDataIntent = new Intent();  // First time we've added a result.
    434         }
    435         for (Map.Entry<String, Uri> entry : results.entrySet()) {
    436             String mimeType = entry.getKey();
    437             Uri uri = entry.getValue();
    438             if (mimeType == null) {
    439                 continue;
    440             }
    441             Bundle resultsBundle =
    442                     clipDataIntent.getBundleExtra(getExtraResultsKeyForData(mimeType));
    443             if (resultsBundle == null) {
    444                 resultsBundle = new Bundle();
    445             }
    446             resultsBundle.putString(remoteInput.getResultKey(), uri.toString());
    447 
    448             clipDataIntent.putExtra(getExtraResultsKeyForData(mimeType), resultsBundle);
    449         }
    450         intent.setClipData(ClipData.newIntent(RESULTS_CLIP_LABEL, clipDataIntent));
    451     }
    452 
    453     /**
    454      * Set the source of the RemoteInput results. This method should only be called by remote
    455      * input collection services (e.g.
    456      * {@link android.service.notification.NotificationListenerService})
    457      * when sending results to a pending intent.
    458      *
    459      * @see #SOURCE_FREE_FORM_INPUT
    460      * @see #SOURCE_CHOICE
    461      *
    462      * @param intent The intent to add remote input source to. The {@link ClipData}
    463      *               field of the intent will be modified to contain the source.
    464      * @param source The source of the results.
    465      */
    466     public static void setResultsSource(Intent intent, @Source int source) {
    467         Intent clipDataIntent = getClipDataIntentFromIntent(intent);
    468         if (clipDataIntent == null) {
    469             clipDataIntent = new Intent();  // First time we've added a result.
    470         }
    471         clipDataIntent.putExtra(EXTRA_RESULTS_SOURCE, source);
    472         intent.setClipData(ClipData.newIntent(RESULTS_CLIP_LABEL, clipDataIntent));
    473     }
    474 
    475     /**
    476      * Get the source of the RemoteInput results.
    477      *
    478      * @see #SOURCE_FREE_FORM_INPUT
    479      * @see #SOURCE_CHOICE
    480      *
    481      * @param intent The intent object that fired in response to an action or content intent
    482      *               which also had one or more remote input requested.
    483      * @return The source of the results. If no source was set, {@link #SOURCE_FREE_FORM_INPUT} will
    484      * be returned.
    485      */
    486     @Source
    487     public static int getResultsSource(Intent intent) {
    488         Intent clipDataIntent = getClipDataIntentFromIntent(intent);
    489         if (clipDataIntent == null) {
    490             return SOURCE_FREE_FORM_INPUT;
    491         }
    492         return clipDataIntent.getExtras().getInt(EXTRA_RESULTS_SOURCE, SOURCE_FREE_FORM_INPUT);
    493     }
    494 
    495     private static String getExtraResultsKeyForData(String mimeType) {
    496         return EXTRA_DATA_TYPE_RESULTS_DATA + mimeType;
    497     }
    498 
    499     @Override
    500     public int describeContents() {
    501         return 0;
    502     }
    503 
    504     @Override
    505     public void writeToParcel(Parcel out, int flags) {
    506         out.writeString(mResultKey);
    507         out.writeCharSequence(mLabel);
    508         out.writeCharSequenceArray(mChoices);
    509         out.writeInt(mFlags);
    510         out.writeBundle(mExtras);
    511         out.writeArraySet(mAllowedDataTypes);
    512     }
    513 
    514     public static final Creator<RemoteInput> CREATOR = new Creator<RemoteInput>() {
    515         @Override
    516         public RemoteInput createFromParcel(Parcel in) {
    517             return new RemoteInput(in);
    518         }
    519 
    520         @Override
    521         public RemoteInput[] newArray(int size) {
    522             return new RemoteInput[size];
    523         }
    524     };
    525 
    526     private static Intent getClipDataIntentFromIntent(Intent intent) {
    527         ClipData clipData = intent.getClipData();
    528         if (clipData == null) {
    529             return null;
    530         }
    531         ClipDescription clipDescription = clipData.getDescription();
    532         if (!clipDescription.hasMimeType(ClipDescription.MIMETYPE_TEXT_INTENT)) {
    533             return null;
    534         }
    535         if (!clipDescription.getLabel().equals(RESULTS_CLIP_LABEL)) {
    536             return null;
    537         }
    538         return clipData.getItemAt(0).getIntent();
    539     }
    540 }
    541