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, "Reply", 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