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