RemoteInput.java revision 4b3a54710792d24233b77bcd6cca120b9ffe0b32
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
17package android.app;
18
19import android.annotation.IntDef;
20import android.annotation.NonNull;
21import android.annotation.Nullable;
22import android.content.ClipData;
23import android.content.ClipDescription;
24import android.content.Intent;
25import android.net.Uri;
26import android.os.Bundle;
27import android.os.Parcel;
28import android.os.Parcelable;
29import android.util.ArraySet;
30
31import java.lang.annotation.Retention;
32import java.lang.annotation.RetentionPolicy;
33import java.util.HashMap;
34import java.util.Map;
35import 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 */
71public 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