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.Bundle;
24 import android.support.annotation.RequiresApi;
25 
26 import java.util.ArrayList;
27 import java.util.HashMap;
28 import java.util.HashSet;
29 import java.util.Map;
30 import java.util.Set;
31 
32 @RequiresApi(16)
33 class RemoteInputCompatJellybean {
34     /** Label used to denote the clip data type used for remote input transport */
35     public static final String RESULTS_CLIP_LABEL = "android.remoteinput.results";
36 
37     /** Extra added to a clip data intent object to hold the results bundle. */
38     public static final String EXTRA_RESULTS_DATA = "android.remoteinput.resultsData";
39 
40     /** Extra added to a clip data intent object to hold the data results bundle. */
41     private static final String EXTRA_DATA_TYPE_RESULTS_DATA =
42             "android.remoteinput.dataTypeResultsData";
43 
44     private static final String KEY_RESULT_KEY = "resultKey";
45     private static final String KEY_LABEL = "label";
46     private static final String KEY_CHOICES = "choices";
47     private static final String KEY_ALLOW_FREE_FORM_INPUT = "allowFreeFormInput";
48     private static final String KEY_EXTRAS = "extras";
49     private static final String KEY_ALLOWED_DATA_TYPES = "allowedDataTypes";
50 
fromBundle(Bundle data, RemoteInputCompatBase.RemoteInput.Factory factory)51     static RemoteInputCompatBase.RemoteInput fromBundle(Bundle data,
52             RemoteInputCompatBase.RemoteInput.Factory factory) {
53         ArrayList<String> allowedDataTypesAsList = data.getStringArrayList(KEY_ALLOWED_DATA_TYPES);
54         Set<String> allowedDataTypes = new HashSet<>();
55         if (allowedDataTypesAsList != null) {
56             for (String type : allowedDataTypesAsList) {
57                 allowedDataTypes.add(type);
58             }
59         }
60         return factory.build(data.getString(KEY_RESULT_KEY),
61                 data.getCharSequence(KEY_LABEL),
62                 data.getCharSequenceArray(KEY_CHOICES),
63                 data.getBoolean(KEY_ALLOW_FREE_FORM_INPUT),
64                 data.getBundle(KEY_EXTRAS),
65                 allowedDataTypes);
66     }
67 
toBundle(RemoteInputCompatBase.RemoteInput remoteInput)68     static Bundle toBundle(RemoteInputCompatBase.RemoteInput remoteInput) {
69         Bundle data = new Bundle();
70         data.putString(KEY_RESULT_KEY, remoteInput.getResultKey());
71         data.putCharSequence(KEY_LABEL, remoteInput.getLabel());
72         data.putCharSequenceArray(KEY_CHOICES, remoteInput.getChoices());
73         data.putBoolean(KEY_ALLOW_FREE_FORM_INPUT, remoteInput.getAllowFreeFormInput());
74         data.putBundle(KEY_EXTRAS, remoteInput.getExtras());
75 
76         Set<String> allowedDataTypes = remoteInput.getAllowedDataTypes();
77         if (allowedDataTypes != null && !allowedDataTypes.isEmpty()) {
78             ArrayList<String> allowedDataTypesAsList = new ArrayList<>(allowedDataTypes.size());
79             for (String type : allowedDataTypes) {
80                 allowedDataTypesAsList.add(type);
81             }
82             data.putStringArrayList(KEY_ALLOWED_DATA_TYPES, allowedDataTypesAsList);
83         }
84         return data;
85     }
86 
fromBundleArray(Bundle[] bundles, RemoteInputCompatBase.RemoteInput.Factory factory)87     static RemoteInputCompatBase.RemoteInput[] fromBundleArray(Bundle[] bundles,
88             RemoteInputCompatBase.RemoteInput.Factory factory) {
89         if (bundles == null) {
90             return null;
91         }
92         RemoteInputCompatBase.RemoteInput[] remoteInputs = factory.newArray(bundles.length);
93         for (int i = 0; i < bundles.length; i++) {
94             remoteInputs[i] = fromBundle(bundles[i], factory);
95         }
96         return remoteInputs;
97     }
98 
toBundleArray(RemoteInputCompatBase.RemoteInput[] remoteInputs)99     static Bundle[] toBundleArray(RemoteInputCompatBase.RemoteInput[] remoteInputs) {
100         if (remoteInputs == null) {
101             return null;
102         }
103         Bundle[] bundles = new Bundle[remoteInputs.length];
104         for (int i = 0; i < remoteInputs.length; i++) {
105             bundles[i] = toBundle(remoteInputs[i]);
106         }
107         return bundles;
108     }
109 
getResultsFromIntent(Intent intent)110     static Bundle getResultsFromIntent(Intent intent) {
111         Intent clipDataIntent = getClipDataIntentFromIntent(intent);
112         if (clipDataIntent == null) {
113             return null;
114         }
115         return clipDataIntent.getExtras().getParcelable(EXTRA_RESULTS_DATA);
116     }
117 
getDataResultsFromIntent(Intent intent, String remoteInputResultKey)118     static Map<String, Uri> getDataResultsFromIntent(Intent intent, String remoteInputResultKey) {
119         Intent clipDataIntent = getClipDataIntentFromIntent(intent);
120         if (clipDataIntent == null) {
121             return null;
122         }
123         Map<String, Uri> results = new HashMap<>();
124         Bundle extras = clipDataIntent.getExtras();
125         for (String key : extras.keySet()) {
126             if (key.startsWith(EXTRA_DATA_TYPE_RESULTS_DATA)) {
127                 String mimeType = key.substring(EXTRA_DATA_TYPE_RESULTS_DATA.length());
128                 if (mimeType == null || mimeType.isEmpty()) {
129                     continue;
130                 }
131                 Bundle bundle = clipDataIntent.getBundleExtra(key);
132                 String uriStr = bundle.getString(remoteInputResultKey);
133                 if (uriStr == null || uriStr.isEmpty()) {
134                     continue;
135                 }
136                 results.put(mimeType, Uri.parse(uriStr));
137             }
138         }
139         return results.isEmpty() ? null : results;
140     }
141 
addResultsToIntent(RemoteInputCompatBase.RemoteInput[] remoteInputs, Intent intent, Bundle results)142     static void addResultsToIntent(RemoteInputCompatBase.RemoteInput[] remoteInputs, Intent intent,
143             Bundle results) {
144         Intent clipDataIntent = getClipDataIntentFromIntent(intent);
145         if (clipDataIntent == null) {
146             clipDataIntent = new Intent();  // First time we've added a result.
147         }
148         Bundle resultsBundle = clipDataIntent.getBundleExtra(EXTRA_RESULTS_DATA);
149         if (resultsBundle == null) {
150             resultsBundle = new Bundle();
151         }
152         for (RemoteInputCompatBase.RemoteInput remoteInput : remoteInputs) {
153             Object result = results.get(remoteInput.getResultKey());
154             if (result instanceof CharSequence) {
155                 resultsBundle.putCharSequence(remoteInput.getResultKey(), (CharSequence) result);
156             }
157         }
158         clipDataIntent.putExtra(EXTRA_RESULTS_DATA, resultsBundle);
159         intent.setClipData(ClipData.newIntent(RESULTS_CLIP_LABEL, clipDataIntent));
160     }
161 
162     /**
163      * Same as {@link #addResultsToIntent} but for setting data results.
164      * @param remoteInput The remote input for which results are being provided
165      * @param intent The intent to add remote input results to. The {@link ClipData}
166      *               field of the intent will be modified to contain the results.
167      * @param results A map of mime type to the Uri result for that mime type.
168      */
addDataResultToIntent(RemoteInput remoteInput, Intent intent, Map<String, Uri> results)169     public static void addDataResultToIntent(RemoteInput remoteInput, Intent intent,
170             Map<String, Uri> results) {
171         Intent clipDataIntent = getClipDataIntentFromIntent(intent);
172         if (clipDataIntent == null) {
173             clipDataIntent = new Intent();  // First time we've added a result.
174         }
175         for (Map.Entry<String, Uri> entry : results.entrySet()) {
176             String mimeType = entry.getKey();
177             Uri uri = entry.getValue();
178             if (mimeType == null) {
179                 continue;
180             }
181             Bundle resultsBundle =
182                     clipDataIntent.getBundleExtra(getExtraResultsKeyForData(mimeType));
183             if (resultsBundle == null) {
184                 resultsBundle = new Bundle();
185             }
186             resultsBundle.putString(remoteInput.getResultKey(), uri.toString());
187             clipDataIntent.putExtra(getExtraResultsKeyForData(mimeType), resultsBundle);
188         }
189         intent.setClipData(ClipData.newIntent(RESULTS_CLIP_LABEL, clipDataIntent));
190     }
191 
getExtraResultsKeyForData(String mimeType)192     private static String getExtraResultsKeyForData(String mimeType) {
193         return EXTRA_DATA_TYPE_RESULTS_DATA + mimeType;
194     }
195 
getClipDataIntentFromIntent(Intent intent)196     private static Intent getClipDataIntentFromIntent(Intent intent) {
197         ClipData clipData = intent.getClipData();
198         if (clipData == null) {
199             return null;
200         }
201         ClipDescription clipDescription = clipData.getDescription();
202         if (!clipDescription.hasMimeType(ClipDescription.MIMETYPE_TEXT_INTENT)) {
203             return null;
204         }
205         if (!clipDescription.getLabel().equals(RESULTS_CLIP_LABEL)) {
206             return null;
207         }
208         return clipData.getItemAt(0).getIntent();
209     }
210 }
211