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 
RemoteInput(String resultKey, CharSequence label, CharSequence[] choices, int flags, Bundle extras, ArraySet<String> allowedDataTypes)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      */
getResultKey()123     public String getResultKey() {
124         return mResultKey;
125     }
126 
127     /**
128      * Get the label to display to users when collecting this input.
129      */
getLabel()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      */
getChoices()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      */
getAllowedDataTypes()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      */
isDataOnly()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      */
getAllowFreeFormInput()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      */
getExtras()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          */
Builder(@onNull String resultKey)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
setLabel(@ullable CharSequence label)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
setChoices(@ullable CharSequence[] choices)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
setAllowDataType(@onNull String mimeType, boolean doAllow)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
setAllowFreeFormInput(boolean allowFreeFormTextInput)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
addExtras(@onNull Bundle extras)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
getExtras()297         public Bundle getExtras() {
298             return mExtras;
299         }
300 
setFlag(int mask, boolean value)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
build()314         public RemoteInput build() {
315             return new RemoteInput(
316                     mResultKey, mLabel, mChoices, mFlags, mExtras, mAllowedDataTypes);
317         }
318     }
319 
RemoteInput(Parcel in)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      */
getDataResultsFromIntent( Intent intent, String remoteInputResultKey)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      */
getResultsFromIntent(Intent intent)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      */
addResultsToIntent(RemoteInput[] remoteInputs, Intent intent, Bundle results)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      */
addDataResultToIntent(RemoteInput remoteInput, Intent intent, Map<String, Uri> results)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      */
setResultsSource(Intent intent, @Source int source)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
getResultsSource(Intent intent)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 
getExtraResultsKeyForData(String mimeType)495     private static String getExtraResultsKeyForData(String mimeType) {
496         return EXTRA_DATA_TYPE_RESULTS_DATA + mimeType;
497     }
498 
499     @Override
describeContents()500     public int describeContents() {
501         return 0;
502     }
503 
504     @Override
writeToParcel(Parcel out, int flags)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 
getClipDataIntentFromIntent(Intent intent)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