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