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     /** @hide */
97     @IntDef(prefix = {"EDIT_CHOICES_BEFORE_SENDING_"},
98             value = {EDIT_CHOICES_BEFORE_SENDING_AUTO, EDIT_CHOICES_BEFORE_SENDING_DISABLED,
99                     EDIT_CHOICES_BEFORE_SENDING_ENABLED})
100     @Retention(RetentionPolicy.SOURCE)
101     public @interface EditChoicesBeforeSending {}
102 
103     /** The platform will determine whether choices will be edited before being sent to the app. */
104     public static final int EDIT_CHOICES_BEFORE_SENDING_AUTO = 0;
105 
106     /** Tapping on a choice should send the input immediately, without letting the user edit it. */
107     public static final int EDIT_CHOICES_BEFORE_SENDING_DISABLED = 1;
108 
109     /** Tapping on a choice should let the user edit the input before it is sent to the app. */
110     public static final int EDIT_CHOICES_BEFORE_SENDING_ENABLED = 2;
111 
112     // Flags bitwise-ored to mFlags
113     private static final int FLAG_ALLOW_FREE_FORM_INPUT = 0x1;
114 
115     // Default value for flags integer
116     private static final int DEFAULT_FLAGS = FLAG_ALLOW_FREE_FORM_INPUT;
117 
118     private final String mResultKey;
119     private final CharSequence mLabel;
120     private final CharSequence[] mChoices;
121     private final int mFlags;
122     @EditChoicesBeforeSending private final int mEditChoicesBeforeSending;
123     private final Bundle mExtras;
124     private final ArraySet<String> mAllowedDataTypes;
125 
RemoteInput(String resultKey, CharSequence label, CharSequence[] choices, int flags, int editChoicesBeforeSending, Bundle extras, ArraySet<String> allowedDataTypes)126     private RemoteInput(String resultKey, CharSequence label, CharSequence[] choices,
127             int flags, int editChoicesBeforeSending, Bundle extras,
128             ArraySet<String> allowedDataTypes) {
129         this.mResultKey = resultKey;
130         this.mLabel = label;
131         this.mChoices = choices;
132         this.mFlags = flags;
133         this.mEditChoicesBeforeSending = editChoicesBeforeSending;
134         this.mExtras = extras;
135         this.mAllowedDataTypes = allowedDataTypes;
136         if (getEditChoicesBeforeSending() == EDIT_CHOICES_BEFORE_SENDING_ENABLED
137                 && !getAllowFreeFormInput()) {
138             throw new IllegalArgumentException(
139                     "setEditChoicesBeforeSending requires setAllowFreeFormInput");
140         }
141     }
142 
143     /**
144      * Get the key that the result of this input will be set in from the Bundle returned by
145      * {@link #getResultsFromIntent} when the {@link android.app.PendingIntent} is sent.
146      */
getResultKey()147     public String getResultKey() {
148         return mResultKey;
149     }
150 
151     /**
152      * Get the label to display to users when collecting this input.
153      */
getLabel()154     public CharSequence getLabel() {
155         return mLabel;
156     }
157 
158     /**
159      * Get possible input choices. This can be {@code null} if there are no choices to present.
160      */
getChoices()161     public CharSequence[] getChoices() {
162         return mChoices;
163     }
164 
165     /**
166      * Get possible non-textual inputs that are accepted.
167      * This can be {@code null} if the input does not accept non-textual values.
168      * See {@link Builder#setAllowDataType}.
169      */
getAllowedDataTypes()170     public Set<String> getAllowedDataTypes() {
171         return mAllowedDataTypes;
172     }
173 
174     /**
175      * Returns true if the input only accepts data, meaning {@link #getAllowFreeFormInput}
176      * is false, {@link #getChoices} is null or empty, and {@link #getAllowedDataTypes} is
177      * non-null and not empty.
178      */
isDataOnly()179     public boolean isDataOnly() {
180         return !getAllowFreeFormInput()
181                 && (getChoices() == null || getChoices().length == 0)
182                 && !getAllowedDataTypes().isEmpty();
183     }
184 
185     /**
186      * Get whether or not users can provide an arbitrary value for
187      * input. If you set this to {@code false}, users must select one of the
188      * choices in {@link #getChoices}. An {@link IllegalArgumentException} is thrown
189      * if you set this to false and {@link #getChoices} returns {@code null} or empty.
190      */
getAllowFreeFormInput()191     public boolean getAllowFreeFormInput() {
192         return (mFlags & FLAG_ALLOW_FREE_FORM_INPUT) != 0;
193     }
194 
195     /**
196      * Gets whether tapping on a choice should let the user edit the input before it is sent to the
197      * app.
198      */
199     @EditChoicesBeforeSending
getEditChoicesBeforeSending()200     public int getEditChoicesBeforeSending() {
201         return mEditChoicesBeforeSending;
202     }
203 
204     /**
205      * Get additional metadata carried around with this remote input.
206      */
getExtras()207     public Bundle getExtras() {
208         return mExtras;
209     }
210 
211     /**
212      * Builder class for {@link RemoteInput} objects.
213      */
214     public static final class Builder {
215         private final String mResultKey;
216         private final ArraySet<String> mAllowedDataTypes = new ArraySet<>();
217         private final Bundle mExtras = new Bundle();
218         private CharSequence mLabel;
219         private CharSequence[] mChoices;
220         private int mFlags = DEFAULT_FLAGS;
221         @EditChoicesBeforeSending
222         private int mEditChoicesBeforeSending = EDIT_CHOICES_BEFORE_SENDING_AUTO;
223 
224         /**
225          * Create a builder object for {@link RemoteInput} objects.
226          *
227          * @param resultKey the Bundle key that refers to this input when collected from the user
228          */
Builder(@onNull String resultKey)229         public Builder(@NonNull String resultKey) {
230             if (resultKey == null) {
231                 throw new IllegalArgumentException("Result key can't be null");
232             }
233             mResultKey = resultKey;
234         }
235 
236         /**
237          * Set a label to be displayed to the user when collecting this input.
238          *
239          * @param label The label to show to users when they input a response
240          * @return this object for method chaining
241          */
242         @NonNull
setLabel(@ullable CharSequence label)243         public Builder setLabel(@Nullable CharSequence label) {
244             mLabel = Notification.safeCharSequence(label);
245             return this;
246         }
247 
248         /**
249          * Specifies choices available to the user to satisfy this input.
250          *
251          * <p>Note: Starting in Android P, these choices will always be shown on phones if the app's
252          * target SDK is >= P. However, these choices may also be rendered on other types of devices
253          * regardless of target SDK.
254          *
255          * @param choices an array of pre-defined choices for users input.
256          *        You must provide a non-null and non-empty array if
257          *        you disabled free form input using {@link #setAllowFreeFormInput}
258          * @return this object for method chaining
259          */
260         @NonNull
setChoices(@ullable CharSequence[] choices)261         public Builder setChoices(@Nullable CharSequence[] choices) {
262             if (choices == null) {
263                 mChoices = null;
264             } else {
265                 mChoices = new CharSequence[choices.length];
266                 for (int i = 0; i < choices.length; i++) {
267                     mChoices[i] = Notification.safeCharSequence(choices[i]);
268                 }
269             }
270             return this;
271         }
272 
273         /**
274          * Specifies whether the user can provide arbitrary values. This allows an input
275          * to accept non-textual values. Examples of usage are an input that wants audio
276          * or an image.
277          *
278          * @param mimeType A mime type that results are allowed to come in.
279          *         Be aware that text results (see {@link #setAllowFreeFormInput}
280          *         are allowed by default. If you do not want text results you will have to
281          *         pass false to {@code setAllowFreeFormInput}
282          * @param doAllow Whether the mime type should be allowed or not
283          * @return this object for method chaining
284          */
285         @NonNull
setAllowDataType(@onNull String mimeType, boolean doAllow)286         public Builder setAllowDataType(@NonNull String mimeType, boolean doAllow) {
287             if (doAllow) {
288                 mAllowedDataTypes.add(mimeType);
289             } else {
290                 mAllowedDataTypes.remove(mimeType);
291             }
292             return this;
293         }
294 
295         /**
296          * Specifies whether the user can provide arbitrary text values.
297          *
298          * @param allowFreeFormTextInput The default is {@code true}.
299          *         If you specify {@code false}, you must either provide a non-null
300          *         and non-empty array to {@link #setChoices}, or enable a data result
301          *         in {@code setAllowDataType}. Otherwise an
302          *         {@link IllegalArgumentException} is thrown
303          * @return this object for method chaining
304          */
305         @NonNull
setAllowFreeFormInput(boolean allowFreeFormTextInput)306         public Builder setAllowFreeFormInput(boolean allowFreeFormTextInput) {
307             setFlag(FLAG_ALLOW_FREE_FORM_INPUT, allowFreeFormTextInput);
308             return this;
309         }
310 
311         /**
312          * Specifies whether tapping on a choice should let the user edit the input before it is
313          * sent to the app. The default is {@link #EDIT_CHOICES_BEFORE_SENDING_AUTO}.
314          *
315          * It cannot be used if {@link #setAllowFreeFormInput} has been set to false.
316          */
317         @NonNull
setEditChoicesBeforeSending( @ditChoicesBeforeSending int editChoicesBeforeSending)318         public Builder setEditChoicesBeforeSending(
319                 @EditChoicesBeforeSending int editChoicesBeforeSending) {
320             mEditChoicesBeforeSending = editChoicesBeforeSending;
321             return this;
322         }
323 
324         /**
325          * Merge additional metadata into this builder.
326          *
327          * <p>Values within the Bundle will replace existing extras values in this Builder.
328          *
329          * @see RemoteInput#getExtras
330          */
331         @NonNull
addExtras(@onNull Bundle extras)332         public Builder addExtras(@NonNull Bundle extras) {
333             if (extras != null) {
334                 mExtras.putAll(extras);
335             }
336             return this;
337         }
338 
339         /**
340          * Get the metadata Bundle used by this Builder.
341          *
342          * <p>The returned Bundle is shared with this Builder.
343          */
344         @NonNull
getExtras()345         public Bundle getExtras() {
346             return mExtras;
347         }
348 
setFlag(int mask, boolean value)349         private void setFlag(int mask, boolean value) {
350             if (value) {
351                 mFlags |= mask;
352             } else {
353                 mFlags &= ~mask;
354             }
355         }
356 
357         /**
358          * Combine all of the options that have been set and return a new {@link RemoteInput}
359          * object.
360          */
361         @NonNull
build()362         public RemoteInput build() {
363             return new RemoteInput(mResultKey, mLabel, mChoices, mFlags, mEditChoicesBeforeSending,
364                     mExtras, mAllowedDataTypes);
365         }
366     }
367 
RemoteInput(Parcel in)368     private RemoteInput(Parcel in) {
369         mResultKey = in.readString();
370         mLabel = in.readCharSequence();
371         mChoices = in.readCharSequenceArray();
372         mFlags = in.readInt();
373         mEditChoicesBeforeSending = in.readInt();
374         mExtras = in.readBundle();
375         mAllowedDataTypes = (ArraySet<String>) in.readArraySet(null);
376     }
377 
378     /**
379      * Similar as {@link #getResultsFromIntent} but retrieves data results for a
380      * specific RemoteInput result. To retrieve a value use:
381      * <pre>
382      * {@code
383      * Map<String, Uri> results =
384      *     RemoteInput.getDataResultsFromIntent(intent, REMOTE_INPUT_KEY);
385      * if (results != null) {
386      *   Uri data = results.get(MIME_TYPE_OF_INTEREST);
387      * }
388      * }
389      * </pre>
390      * @param intent The intent object that fired in response to an action or content intent
391      *               which also had one or more remote input requested.
392      * @param remoteInputResultKey The result key for the RemoteInput you want results for.
393      */
getDataResultsFromIntent( Intent intent, String remoteInputResultKey)394     public static Map<String, Uri> getDataResultsFromIntent(
395             Intent intent, String remoteInputResultKey) {
396         Intent clipDataIntent = getClipDataIntentFromIntent(intent);
397         if (clipDataIntent == null) {
398             return null;
399         }
400         Map<String, Uri> results = new HashMap<>();
401         Bundle extras = clipDataIntent.getExtras();
402         for (String key : extras.keySet()) {
403           if (key.startsWith(EXTRA_DATA_TYPE_RESULTS_DATA)) {
404               String mimeType = key.substring(EXTRA_DATA_TYPE_RESULTS_DATA.length());
405               if (mimeType == null || mimeType.isEmpty()) {
406                   continue;
407               }
408               Bundle bundle = clipDataIntent.getBundleExtra(key);
409               String uriStr = bundle.getString(remoteInputResultKey);
410               if (uriStr == null || uriStr.isEmpty()) {
411                   continue;
412               }
413               results.put(mimeType, Uri.parse(uriStr));
414           }
415         }
416         return results.isEmpty() ? null : results;
417     }
418 
419     /**
420      * Get the remote input text results bundle from an intent. The returned Bundle will
421      * contain a key/value for every result key populated with text by remote input collector.
422      * Use the {@link Bundle#getCharSequence(String)} method to retrieve a value. For non-text
423      * results use {@link #getDataResultsFromIntent}.
424      * @param intent The intent object that fired in response to an action or content intent
425      *               which also had one or more remote input requested.
426      */
getResultsFromIntent(Intent intent)427     public static Bundle getResultsFromIntent(Intent intent) {
428         Intent clipDataIntent = getClipDataIntentFromIntent(intent);
429         if (clipDataIntent == null) {
430             return null;
431         }
432         return clipDataIntent.getExtras().getParcelable(EXTRA_RESULTS_DATA);
433     }
434 
435     /**
436      * Populate an intent object with the text results gathered from remote input. This method
437      * should only be called by remote input collection services when sending results to a
438      * pending intent.
439      * @param remoteInputs The remote inputs for which results are being provided
440      * @param intent The intent to add remote inputs to. The {@link ClipData}
441      *               field of the intent will be modified to contain the results.
442      * @param results A bundle holding the remote input results. This bundle should
443      *                be populated with keys matching the result keys specified in
444      *                {@code remoteInputs} with values being the CharSequence results per key.
445      */
addResultsToIntent(RemoteInput[] remoteInputs, Intent intent, Bundle results)446     public static void addResultsToIntent(RemoteInput[] remoteInputs, Intent intent,
447             Bundle results) {
448         Intent clipDataIntent = getClipDataIntentFromIntent(intent);
449         if (clipDataIntent == null) {
450             clipDataIntent = new Intent();  // First time we've added a result.
451         }
452         Bundle resultsBundle = clipDataIntent.getBundleExtra(EXTRA_RESULTS_DATA);
453         if (resultsBundle == null) {
454             resultsBundle = new Bundle();
455         }
456         for (RemoteInput remoteInput : remoteInputs) {
457             Object result = results.get(remoteInput.getResultKey());
458             if (result instanceof CharSequence) {
459                 resultsBundle.putCharSequence(remoteInput.getResultKey(), (CharSequence) result);
460             }
461         }
462         clipDataIntent.putExtra(EXTRA_RESULTS_DATA, resultsBundle);
463         intent.setClipData(ClipData.newIntent(RESULTS_CLIP_LABEL, clipDataIntent));
464     }
465 
466     /**
467      * Same as {@link #addResultsToIntent} but for setting data results. This is used
468      * for inputs that accept non-textual results (see {@link Builder#setAllowDataType}).
469      * Only one result can be provided for every mime type accepted by the RemoteInput.
470      * If multiple inputs of the same mime type are expected then multiple RemoteInputs
471      * should be used.
472      *
473      * @param remoteInput The remote input for which results are being provided
474      * @param intent The intent to add remote input results to. The {@link ClipData}
475      *               field of the intent will be modified to contain the results.
476      * @param results A map of mime type to the Uri result for that mime type.
477      */
addDataResultToIntent(RemoteInput remoteInput, Intent intent, Map<String, Uri> results)478     public static void addDataResultToIntent(RemoteInput remoteInput, Intent intent,
479             Map<String, Uri> results) {
480         Intent clipDataIntent = getClipDataIntentFromIntent(intent);
481         if (clipDataIntent == null) {
482             clipDataIntent = new Intent();  // First time we've added a result.
483         }
484         for (Map.Entry<String, Uri> entry : results.entrySet()) {
485             String mimeType = entry.getKey();
486             Uri uri = entry.getValue();
487             if (mimeType == null) {
488                 continue;
489             }
490             Bundle resultsBundle =
491                     clipDataIntent.getBundleExtra(getExtraResultsKeyForData(mimeType));
492             if (resultsBundle == null) {
493                 resultsBundle = new Bundle();
494             }
495             resultsBundle.putString(remoteInput.getResultKey(), uri.toString());
496 
497             clipDataIntent.putExtra(getExtraResultsKeyForData(mimeType), resultsBundle);
498         }
499         intent.setClipData(ClipData.newIntent(RESULTS_CLIP_LABEL, clipDataIntent));
500     }
501 
502     /**
503      * Set the source of the RemoteInput results. This method should only be called by remote
504      * input collection services (e.g.
505      * {@link android.service.notification.NotificationListenerService})
506      * when sending results to a pending intent.
507      *
508      * @see #SOURCE_FREE_FORM_INPUT
509      * @see #SOURCE_CHOICE
510      *
511      * @param intent The intent to add remote input source to. The {@link ClipData}
512      *               field of the intent will be modified to contain the source.
513      * @param source The source of the results.
514      */
setResultsSource(Intent intent, @Source int source)515     public static void setResultsSource(Intent intent, @Source int source) {
516         Intent clipDataIntent = getClipDataIntentFromIntent(intent);
517         if (clipDataIntent == null) {
518             clipDataIntent = new Intent();  // First time we've added a result.
519         }
520         clipDataIntent.putExtra(EXTRA_RESULTS_SOURCE, source);
521         intent.setClipData(ClipData.newIntent(RESULTS_CLIP_LABEL, clipDataIntent));
522     }
523 
524     /**
525      * Get the source of the RemoteInput results.
526      *
527      * @see #SOURCE_FREE_FORM_INPUT
528      * @see #SOURCE_CHOICE
529      *
530      * @param intent The intent object that fired in response to an action or content intent
531      *               which also had one or more remote input requested.
532      * @return The source of the results. If no source was set, {@link #SOURCE_FREE_FORM_INPUT} will
533      * be returned.
534      */
535     @Source
getResultsSource(Intent intent)536     public static int getResultsSource(Intent intent) {
537         Intent clipDataIntent = getClipDataIntentFromIntent(intent);
538         if (clipDataIntent == null) {
539             return SOURCE_FREE_FORM_INPUT;
540         }
541         return clipDataIntent.getExtras().getInt(EXTRA_RESULTS_SOURCE, SOURCE_FREE_FORM_INPUT);
542     }
543 
getExtraResultsKeyForData(String mimeType)544     private static String getExtraResultsKeyForData(String mimeType) {
545         return EXTRA_DATA_TYPE_RESULTS_DATA + mimeType;
546     }
547 
548     @Override
describeContents()549     public int describeContents() {
550         return 0;
551     }
552 
553     @Override
writeToParcel(Parcel out, int flags)554     public void writeToParcel(Parcel out, int flags) {
555         out.writeString(mResultKey);
556         out.writeCharSequence(mLabel);
557         out.writeCharSequenceArray(mChoices);
558         out.writeInt(mFlags);
559         out.writeInt(mEditChoicesBeforeSending);
560         out.writeBundle(mExtras);
561         out.writeArraySet(mAllowedDataTypes);
562     }
563 
564     public static final @android.annotation.NonNull Creator<RemoteInput> CREATOR = new Creator<RemoteInput>() {
565         @Override
566         public RemoteInput createFromParcel(Parcel in) {
567             return new RemoteInput(in);
568         }
569 
570         @Override
571         public RemoteInput[] newArray(int size) {
572             return new RemoteInput[size];
573         }
574     };
575 
getClipDataIntentFromIntent(Intent intent)576     private static Intent getClipDataIntentFromIntent(Intent intent) {
577         ClipData clipData = intent.getClipData();
578         if (clipData == null) {
579             return null;
580         }
581         ClipDescription clipDescription = clipData.getDescription();
582         if (!clipDescription.hasMimeType(ClipDescription.MIMETYPE_TEXT_INTENT)) {
583             return null;
584         }
585         if (!clipDescription.getLabel().equals(RESULTS_CLIP_LABEL)) {
586             return null;
587         }
588         return clipData.getItemAt(0).getIntent();
589     }
590 }
591