blob: 36c524b57fe416b4cefc2c09c67f8aca6fbbb1da [file] [log] [blame]
Alan Viverette3da604b2020-06-10 18:34:39 +00001/*
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 /** @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
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 */
147 public String getResultKey() {
148 return mResultKey;
149 }
150
151 /**
152 * Get the label to display to users when collecting this input.
153 */
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 */
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 */
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 */
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 */
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
200 public int getEditChoicesBeforeSending() {
201 return mEditChoicesBeforeSending;
202 }
203
204 /**
205 * Get additional metadata carried around with this remote input.
206 */
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 */
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
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
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
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
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
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
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
345 public Bundle getExtras() {
346 return mExtras;
347 }
348
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
362 public RemoteInput build() {
363 return new RemoteInput(mResultKey, mLabel, mChoices, mFlags, mEditChoicesBeforeSending,
364 mExtras, mAllowedDataTypes);
365 }
366 }
367
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 */
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 */
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 */
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 */
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 */
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
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
544 private static String getExtraResultsKeyForData(String mimeType) {
545 return EXTRA_DATA_TYPE_RESULTS_DATA + mimeType;
546 }
547
548 @Override
549 public int describeContents() {
550 return 0;
551 }
552
553 @Override
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
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}