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 // 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 RemoteInput(String resultKey, CharSequence label, CharSequence[] choices, int flags, Bundle extras, ArraySet<String> allowedDataTypes)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 */ getResultKey()123 public String getResultKey() { 124 return mResultKey; 125 } 126 127 /** 128 * Get the label to display to users when collecting this input. 129 */ getLabel()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 */ getChoices()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 */ getAllowedDataTypes()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 */ isDataOnly()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 */ getAllowFreeFormInput()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 */ getExtras()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 */ Builder(@onNull String resultKey)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 setLabel(@ullable CharSequence label)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 setChoices(@ullable CharSequence[] choices)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 setAllowDataType(@onNull String mimeType, boolean doAllow)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 setAllowFreeFormInput(boolean allowFreeFormTextInput)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 addExtras(@onNull Bundle extras)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 getExtras()297 public Bundle getExtras() { 298 return mExtras; 299 } 300 setFlag(int mask, boolean value)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 build()314 public RemoteInput build() { 315 return new RemoteInput( 316 mResultKey, mLabel, mChoices, mFlags, mExtras, mAllowedDataTypes); 317 } 318 } 319 RemoteInput(Parcel in)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 */ getDataResultsFromIntent( Intent intent, String remoteInputResultKey)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 */ getResultsFromIntent(Intent intent)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 */ addResultsToIntent(RemoteInput[] remoteInputs, Intent intent, Bundle results)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 */ addDataResultToIntent(RemoteInput remoteInput, Intent intent, Map<String, Uri> results)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 */ setResultsSource(Intent intent, @Source int source)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 getResultsSource(Intent intent)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 getExtraResultsKeyForData(String mimeType)495 private static String getExtraResultsKeyForData(String mimeType) { 496 return EXTRA_DATA_TYPE_RESULTS_DATA + mimeType; 497 } 498 499 @Override describeContents()500 public int describeContents() { 501 return 0; 502 } 503 504 @Override writeToParcel(Parcel out, int flags)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 getClipDataIntentFromIntent(Intent intent)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