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