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, "Reply", 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 /** @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 RemoteInput(String resultKey, CharSequence label, CharSequence[] choices, int flags, int editChoicesBeforeSending, Bundle extras, ArraySet<String> allowedDataTypes)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 */ getResultKey()147 public String getResultKey() { 148 return mResultKey; 149 } 150 151 /** 152 * Get the label to display to users when collecting this input. 153 */ getLabel()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 */ getChoices()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 */ getAllowedDataTypes()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 */ isDataOnly()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 */ getAllowFreeFormInput()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 getEditChoicesBeforeSending()200 public int getEditChoicesBeforeSending() { 201 return mEditChoicesBeforeSending; 202 } 203 204 /** 205 * Get additional metadata carried around with this remote input. 206 */ getExtras()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 */ Builder(@onNull String resultKey)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 setLabel(@ullable CharSequence label)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 setChoices(@ullable CharSequence[] choices)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 setAllowDataType(@onNull String mimeType, boolean doAllow)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 setAllowFreeFormInput(boolean allowFreeFormTextInput)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 setEditChoicesBeforeSending( @ditChoicesBeforeSending int editChoicesBeforeSending)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 addExtras(@onNull Bundle extras)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 getExtras()345 public Bundle getExtras() { 346 return mExtras; 347 } 348 setFlag(int mask, boolean value)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 build()362 public RemoteInput build() { 363 return new RemoteInput(mResultKey, mLabel, mChoices, mFlags, mEditChoicesBeforeSending, 364 mExtras, mAllowedDataTypes); 365 } 366 } 367 RemoteInput(Parcel in)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 */ getDataResultsFromIntent( Intent intent, String remoteInputResultKey)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 */ getResultsFromIntent(Intent intent)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 */ addResultsToIntent(RemoteInput[] remoteInputs, Intent intent, Bundle results)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 */ addDataResultToIntent(RemoteInput remoteInput, Intent intent, Map<String, Uri> results)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 */ setResultsSource(Intent intent, @Source int source)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 getResultsSource(Intent intent)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 getExtraResultsKeyForData(String mimeType)544 private static String getExtraResultsKeyForData(String mimeType) { 545 return EXTRA_DATA_TYPE_RESULTS_DATA + mimeType; 546 } 547 548 @Override describeContents()549 public int describeContents() { 550 return 0; 551 } 552 553 @Override writeToParcel(Parcel out, int flags)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 getClipDataIntentFromIntent(Intent intent)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 } 591