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