• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2021 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.content.pm;
18 
19 import android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.annotation.UserIdInt;
22 import android.app.Person;
23 import android.app.appsearch.AppSearchSchema;
24 import android.app.appsearch.GenericDocument;
25 import android.content.ComponentName;
26 import android.content.Intent;
27 import android.content.LocusId;
28 import android.graphics.drawable.Icon;
29 import android.os.Bundle;
30 import android.os.PersistableBundle;
31 import android.text.TextUtils;
32 import android.util.ArraySet;
33 
34 import com.android.internal.annotations.VisibleForTesting;
35 import com.android.internal.util.Preconditions;
36 
37 import java.io.ByteArrayInputStream;
38 import java.io.ByteArrayOutputStream;
39 import java.io.IOException;
40 import java.net.URISyntaxException;
41 import java.util.ArrayList;
42 import java.util.Arrays;
43 import java.util.Collection;
44 import java.util.List;
45 import java.util.Objects;
46 import java.util.Set;
47 
48 /**
49  * @hide
50  */
51 public class AppSearchShortcutInfo extends GenericDocument {
52 
53     /** The name of the schema type for {@link ShortcutInfo} documents.*/
54     public static final String SCHEMA_TYPE = "Shortcut";
55     public static final int SCHEMA_VERSION = 2;
56 
57     public static final String KEY_ACTIVITY = "activity";
58     public static final String KEY_SHORT_LABEL = "shortLabel";
59     public static final String KEY_SHORT_LABEL_RES_ID = "shortLabelResId";
60     public static final String KEY_SHORT_LABEL_RES_NAME = "shortLabelResName";
61     public static final String KEY_LONG_LABEL = "longLabel";
62     public static final String KEY_LONG_LABEL_RES_ID = "longLabelResId";
63     public static final String KEY_LONG_LABEL_RES_NAME = "longLabelResName";
64     public static final String KEY_DISABLED_MESSAGE = "disabledMessage";
65     public static final String KEY_DISABLED_MESSAGE_RES_ID = "disabledMessageResId";
66     public static final String KEY_DISABLED_MESSAGE_RES_NAME = "disabledMessageResName";
67     public static final String KEY_CATEGORIES = "categories";
68     public static final String KEY_INTENTS = "intents";
69     public static final String KEY_INTENT_PERSISTABLE_EXTRAS = "intentPersistableExtras";
70     public static final String KEY_PERSON = "person";
71     public static final String KEY_LOCUS_ID = "locusId";
72     public static final String KEY_RANK = "rank";
73     public static final String KEY_IMPLICIT_RANK = "implicitRank";
74     public static final String KEY_EXTRAS = "extras";
75     public static final String KEY_FLAGS = "flags";
76     public static final String KEY_ICON_RES_ID = "iconResId";
77     public static final String KEY_ICON_RES_NAME = "iconResName";
78     public static final String KEY_ICON_URI = "iconUri";
79     public static final String KEY_BITMAP_PATH = "bitmapPath";
80     public static final String KEY_DISABLED_REASON = "disabledReason";
81 
82     public static final AppSearchSchema SCHEMA = new AppSearchSchema.Builder(SCHEMA_TYPE)
83             .addProperty(new AppSearchSchema.StringPropertyConfig.Builder(KEY_ACTIVITY)
84                     .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
85                     .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
86                     .setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
87                     .build()
88 
89             ).addProperty(new AppSearchSchema.StringPropertyConfig.Builder(KEY_SHORT_LABEL)
90                     .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
91                     .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
92                     .setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
93                     .build()
94 
95             ).addProperty(new AppSearchSchema.LongPropertyConfig.Builder(KEY_SHORT_LABEL_RES_ID)
96                     .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
97                     .build()
98 
99             ).addProperty(new AppSearchSchema.StringPropertyConfig.Builder(KEY_SHORT_LABEL_RES_NAME)
100                     .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
101                     .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_NONE)
102                     .setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_NONE)
103                     .build()
104 
105             ).addProperty(new AppSearchSchema.StringPropertyConfig.Builder(KEY_LONG_LABEL)
106                     .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
107                     .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
108                     .setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
109                     .build()
110 
111             ).addProperty(new AppSearchSchema.LongPropertyConfig.Builder(KEY_LONG_LABEL_RES_ID)
112                     .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
113                     .build()
114 
115             ).addProperty(new AppSearchSchema.StringPropertyConfig.Builder(KEY_LONG_LABEL_RES_NAME)
116                     .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
117                     .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_NONE)
118                     .setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_NONE)
119                     .build()
120 
121             ).addProperty(new AppSearchSchema.StringPropertyConfig.Builder(KEY_DISABLED_MESSAGE)
122                     .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
123                     .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_NONE)
124                     .setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_NONE)
125                     .build()
126 
127             ).addProperty(new AppSearchSchema.LongPropertyConfig.Builder(
128                     KEY_DISABLED_MESSAGE_RES_ID)
129                     .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
130                     .build()
131 
132             ).addProperty(new AppSearchSchema.StringPropertyConfig.Builder(
133                     KEY_DISABLED_MESSAGE_RES_NAME)
134                     .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
135                     .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_NONE)
136                     .setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_NONE)
137                     .build()
138 
139             ).addProperty(new AppSearchSchema.StringPropertyConfig.Builder(KEY_CATEGORIES)
140                     .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
141                     .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
142                     .setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
143                     .build()
144 
145             ).addProperty(new AppSearchSchema.StringPropertyConfig.Builder(KEY_INTENTS)
146                     .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
147                     .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_NONE)
148                     .setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_NONE)
149                     .build()
150 
151             ).addProperty(new AppSearchSchema.BytesPropertyConfig.Builder(
152                     KEY_INTENT_PERSISTABLE_EXTRAS)
153                     .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
154                     .build()
155 
156             ).addProperty(new AppSearchSchema.DocumentPropertyConfig.Builder(
157                     KEY_PERSON, AppSearchPerson.SCHEMA_TYPE)
158                     .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
159                     .build()
160 
161             ).addProperty(new AppSearchSchema.StringPropertyConfig.Builder(KEY_LOCUS_ID)
162                     .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
163                     .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
164                     .setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
165                     .build()
166 
167             ).addProperty(new AppSearchSchema.StringPropertyConfig.Builder(KEY_RANK)
168                     .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
169                     .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
170                     .setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
171                     .build()
172 
173             ).addProperty(new AppSearchSchema.LongPropertyConfig.Builder(KEY_IMPLICIT_RANK)
174                     .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
175                     .build()
176 
177             ).addProperty(new AppSearchSchema.BytesPropertyConfig.Builder(KEY_EXTRAS)
178                     .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
179                     .build()
180 
181             ).addProperty(new AppSearchSchema.StringPropertyConfig.Builder(KEY_FLAGS)
182                     .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
183                     .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
184                     .setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
185                     .build()
186 
187             ).addProperty(new AppSearchSchema.LongPropertyConfig.Builder(KEY_ICON_RES_ID)
188                     .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
189                     .build()
190 
191             ).addProperty(new AppSearchSchema.StringPropertyConfig.Builder(KEY_ICON_RES_NAME)
192                     .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
193                     .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_NONE)
194                     .setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_NONE)
195                     .build()
196 
197             ).addProperty(new AppSearchSchema.StringPropertyConfig.Builder(KEY_ICON_URI)
198                     .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
199                     .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_NONE)
200                     .setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_NONE)
201                     .build()
202 
203             ).addProperty(new AppSearchSchema.StringPropertyConfig.Builder(KEY_BITMAP_PATH)
204                     .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
205                     .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_NONE)
206                     .setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_NONE)
207                     .build()
208 
209             ).addProperty(new AppSearchSchema.StringPropertyConfig.Builder(KEY_DISABLED_REASON)
210                     .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
211                     .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
212                     .setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
213                     .build()
214 
215             ).build();
216 
217     /**
218      * The string representation of every flag within {@link ShortcutInfo}. Note that its value
219      * needs to be camelCase since AppSearch's tokenizer will break the word when it sees
220      * underscore.
221      */
222     private static final String IS_DYNAMIC = "Dyn";
223     private static final String NOT_DYNAMIC = "nDyn";
224     private static final String IS_PINNED = "Pin";
225     private static final String NOT_PINNED = "nPin";
226     private static final String HAS_ICON_RES = "IcR";
227     private static final String NO_ICON_RES = "nIcR";
228     private static final String HAS_ICON_FILE = "IcF";
229     private static final String NO_ICON_FILE = "nIcF";
230     private static final String IS_KEY_FIELD_ONLY = "Key";
231     private static final String NOT_KEY_FIELD_ONLY = "nKey";
232     private static final String IS_MANIFEST = "Man";
233     private static final String NOT_MANIFEST = "nMan";
234     private static final String IS_DISABLED = "Dis";
235     private static final String NOT_DISABLED = "nDis";
236     private static final String ARE_STRINGS_RESOLVED = "Str";
237     private static final String NOT_STRINGS_RESOLVED = "nStr";
238     private static final String IS_IMMUTABLE = "Im";
239     private static final String NOT_IMMUTABLE = "nIm";
240     private static final String HAS_ADAPTIVE_BITMAP = "IcA";
241     private static final String NO_ADAPTIVE_BITMAP = "nIcA";
242     private static final String IS_RETURNED_BY_SERVICE = "Rets";
243     private static final String NOT_RETURNED_BY_SERVICE = "nRets";
244     private static final String HAS_ICON_FILE_PENDING_SAVE = "Pens";
245     private static final String NO_ICON_FILE_PENDING_SAVE = "nPens";
246     private static final String IS_SHADOW = "Sdw";
247     private static final String NOT_SHADOW = "nSdw";
248     private static final String IS_LONG_LIVED = "Liv";
249     private static final String NOT_LONG_LIVED = "nLiv";
250     private static final String HAS_ICON_URI = "IcU";
251     private static final String NO_ICON_URI = "nIcU";
252     private static final String IS_CACHED_NOTIFICATION = "CaN";
253     private static final String NOT_CACHED_NOTIFICATION = "nCaN";
254     private static final String IS_CACHED_BUBBLE = "CaB";
255     private static final String NOT_CACHED_BUBBLE = "nCaB";
256     private static final String IS_CACHED_PEOPLE_TITLE = "CaPT";
257     private static final String NOT_CACHED_PEOPLE_TITLE = "nCaPT";
258 
259     /**
260      * Following flags are not store within ShortcutInfo, but book-keeping states to reduce search
261      * space when performing queries against AppSearch.
262      */
263     private static final String HAS_BITMAP_PATH = "hBiP";
264     private static final String HAS_STRING_RESOURCE = "hStr";
265     private static final String HAS_NON_ZERO_RANK = "hRan";
266 
267     public static final String QUERY_IS_DYNAMIC = KEY_FLAGS + ":" + IS_DYNAMIC;
268     public static final String QUERY_IS_NOT_DYNAMIC = KEY_FLAGS + ":" + NOT_DYNAMIC;
269     public static final String QUERY_IS_PINNED = KEY_FLAGS + ":" + IS_PINNED;
270     public static final String QUERY_IS_NOT_PINNED = KEY_FLAGS + ":" + NOT_PINNED;
271     public static final String QUERY_IS_MANIFEST = KEY_FLAGS + ":" + IS_MANIFEST;
272     public static final String QUERY_IS_NOT_MANIFEST = KEY_FLAGS + ":" + NOT_MANIFEST;
273     public static final String QUERY_IS_PINNED_AND_ENABLED =
274             "(" + KEY_FLAGS + ":" + IS_PINNED + " " + KEY_FLAGS + ":" + NOT_DISABLED + ")";
275     public static final String QUERY_IS_CACHED =
276             "(" + KEY_FLAGS + ":" + IS_CACHED_NOTIFICATION + " OR "
277             + KEY_FLAGS + ":" + IS_CACHED_BUBBLE + " OR "
278             + KEY_FLAGS + ":" + IS_CACHED_PEOPLE_TITLE + ")";
279     public static final String QUERY_IS_NOT_CACHED =
280             "(" + KEY_FLAGS + ":" + NOT_CACHED_NOTIFICATION + " "
281                     + KEY_FLAGS + ":" + NOT_CACHED_BUBBLE + " "
282                     + KEY_FLAGS + ":" + NOT_CACHED_PEOPLE_TITLE + ")";
283     public static final String QUERY_IS_FLOATING =
284             "((" + IS_PINNED + " OR " + QUERY_IS_CACHED + ") "
285                     + QUERY_IS_NOT_DYNAMIC + " " + QUERY_IS_NOT_MANIFEST + ")";
286     public static final String QUERY_IS_NOT_FLOATING =
287             "((" + QUERY_IS_NOT_PINNED + " " + QUERY_IS_NOT_CACHED + ") OR "
288                     + QUERY_IS_DYNAMIC + " OR " + QUERY_IS_MANIFEST + ")";
289     public static final String QUERY_IS_VISIBLE_TO_PUBLISHER =
290             "(" + KEY_DISABLED_REASON + ":" + ShortcutInfo.DISABLED_REASON_NOT_DISABLED
291                     + " OR " + KEY_DISABLED_REASON + ":"
292                     + ShortcutInfo.DISABLED_REASON_BY_APP
293                     + " OR " + KEY_DISABLED_REASON + ":"
294                     + ShortcutInfo.DISABLED_REASON_APP_CHANGED
295                     + " OR " + KEY_DISABLED_REASON + ":"
296                     + ShortcutInfo.DISABLED_REASON_UNKNOWN + ")";
297     public static final String QUERY_DISABLED_REASON_VERSION_LOWER =
298             KEY_DISABLED_REASON + ":" + ShortcutInfo.DISABLED_REASON_VERSION_LOWER;
299     public static final String QUERY_IS_NON_MANIFEST_VISIBLE =
300             "(" + QUERY_IS_NOT_MANIFEST + " " + QUERY_IS_VISIBLE_TO_PUBLISHER + " ("
301                     + QUERY_IS_PINNED + " OR " + QUERY_IS_CACHED + " OR " + QUERY_IS_DYNAMIC + "))";
302     public static final String QUERY_IS_VISIBLE_CACHED_OR_PINNED =
303             "(" + QUERY_IS_VISIBLE_TO_PUBLISHER + " " + QUERY_IS_DYNAMIC
304                     + " (" + QUERY_IS_CACHED + " OR " + QUERY_IS_PINNED + "))";
305     public static final String QUERY_IS_VISIBLE_PINNED_ONLY =
306             "(" + QUERY_IS_VISIBLE_TO_PUBLISHER + " " + QUERY_IS_PINNED + " " + QUERY_IS_NOT_CACHED
307             + " " + QUERY_IS_NOT_DYNAMIC + " " + QUERY_IS_NOT_MANIFEST + ")";
308     public static final String QUERY_HAS_BITMAP_PATH = KEY_FLAGS + ":" + HAS_BITMAP_PATH;
309     public static final String QUERY_HAS_STRING_RESOURCE = KEY_FLAGS + ":" + HAS_STRING_RESOURCE;
310     public static final String QUERY_HAS_NON_ZERO_RANK = KEY_FLAGS + ":" + HAS_NON_ZERO_RANK;
311     public static final String QUERY_IS_FLOATING_AND_HAS_RANK =
312             "(" + QUERY_IS_FLOATING + " " + QUERY_HAS_NON_ZERO_RANK + ")";
313 
AppSearchShortcutInfo(@onNull GenericDocument document)314     public AppSearchShortcutInfo(@NonNull GenericDocument document) {
315         super(document);
316     }
317 
318     /**
319      * @hide
320      */
321     @NonNull
instance(@onNull final ShortcutInfo shortcutInfo)322     public static AppSearchShortcutInfo instance(@NonNull final ShortcutInfo shortcutInfo) {
323         Objects.requireNonNull(shortcutInfo);
324         return new Builder(shortcutInfo.getPackage(), shortcutInfo.getId())
325                 .setActivity(shortcutInfo.getActivity())
326                 .setShortLabel(shortcutInfo.getShortLabel())
327                 .setShortLabelResId(shortcutInfo.getShortLabelResourceId())
328                 .setShortLabelResName(shortcutInfo.getTitleResName())
329                 .setLongLabel(shortcutInfo.getLongLabel())
330                 .setLongLabelResId(shortcutInfo.getLongLabelResourceId())
331                 .setLongLabelResName(shortcutInfo.getTextResName())
332                 .setDisabledMessage(shortcutInfo.getDisabledMessage())
333                 .setDisabledMessageResId(shortcutInfo.getDisabledMessageResourceId())
334                 .setDisabledMessageResName(shortcutInfo.getDisabledMessageResName())
335                 .setCategories(shortcutInfo.getCategories())
336                 .setIntents(shortcutInfo.getIntents())
337                 .setRank(shortcutInfo.getRank())
338                 .setImplicitRank(shortcutInfo.getImplicitRank()
339                         | (shortcutInfo.isRankChanged() ? ShortcutInfo.RANK_CHANGED_BIT : 0))
340                 .setExtras(shortcutInfo.getExtras())
341                 .setCreationTimestampMillis(shortcutInfo.getLastChangedTimestamp())
342                 .setFlags(shortcutInfo.getFlags())
343                 .setIconResId(shortcutInfo.getIconResourceId())
344                 .setIconResName(shortcutInfo.getIconResName())
345                 .setBitmapPath(shortcutInfo.getBitmapPath())
346                 .setIconUri(shortcutInfo.getIconUri())
347                 .setDisabledReason(shortcutInfo.getDisabledReason())
348                 .setPersons(shortcutInfo.getPersons())
349                 .setLocusId(shortcutInfo.getLocusId())
350                 .build();
351     }
352 
353     /**
354      * @hide
355      */
356     @NonNull
toShortcutInfo(@serIdInt int userId)357     public ShortcutInfo toShortcutInfo(@UserIdInt int userId) {
358         final String packageName = getNamespace();
359         final String activityString = getPropertyString(KEY_ACTIVITY);
360         final ComponentName activity = activityString == null
361                 ? null : ComponentName.unflattenFromString(activityString);
362         // TODO: proper icon handling
363         // NOTE: bitmap based icons are currently saved in side-channel (see ShortcutBitmapSaver),
364         // re-creating Icon object at creation time implies turning this function into async since
365         // loading bitmap is I/O bound. Since ShortcutInfo#getIcon is already annotated with
366         // @hide and @UnsupportedAppUsage, we could migrate existing usage in platform with
367         // LauncherApps#getShortcutIconDrawable instead.
368         final Icon icon = null;
369         final String shortLabel = getPropertyString(KEY_SHORT_LABEL);
370         final int shortLabelResId = (int) getPropertyLong(KEY_SHORT_LABEL_RES_ID);
371         final String shortLabelResName = getPropertyString(KEY_SHORT_LABEL_RES_NAME);
372         final String longLabel = getPropertyString(KEY_LONG_LABEL);
373         final int longLabelResId = (int) getPropertyLong(KEY_LONG_LABEL_RES_ID);
374         final String longLabelResName = getPropertyString(KEY_LONG_LABEL_RES_NAME);
375         final String disabledMessage = getPropertyString(KEY_DISABLED_MESSAGE);
376         final int disabledMessageResId = (int) getPropertyLong(KEY_DISABLED_MESSAGE_RES_ID);
377         final String disabledMessageResName = getPropertyString(KEY_DISABLED_MESSAGE_RES_NAME);
378         final String[] categories = getPropertyStringArray(KEY_CATEGORIES);
379         final Set<String> categoriesSet = categories == null
380                 ? null : new ArraySet<>(Arrays.asList(categories));
381         final String[] intentsStrings = getPropertyStringArray(KEY_INTENTS);
382         final Intent[] intents = intentsStrings == null
383                 ? new Intent[0] : Arrays.stream(intentsStrings).map(uri -> {
384                     if (TextUtils.isEmpty(uri)) {
385                         return new Intent(Intent.ACTION_VIEW);
386                     }
387                     try {
388                         return Intent.parseUri(uri, /* flags =*/ 0);
389                     } catch (URISyntaxException e) {
390                         // ignore malformed entry
391                     }
392                     return null;
393                 }).toArray(Intent[]::new);
394         final byte[][] intentExtrasesBytes = getPropertyBytesArray(KEY_INTENT_PERSISTABLE_EXTRAS);
395         final Bundle[] intentExtrases = intentExtrasesBytes == null
396                 ? null : Arrays.stream(intentExtrasesBytes)
397                 .map(this::transformToBundle).toArray(Bundle[]::new);
398         if (intents != null) {
399             for (int i = 0; i < intents.length; i++) {
400                 final Intent intent = intents[i];
401                 if (intent == null || intentExtrases == null || intentExtrases.length <= i
402                         || intentExtrases[i] == null || intentExtrases[i].size() == 0) {
403                     continue;
404                 }
405                 intent.replaceExtras(intentExtrases[i]);
406             }
407         }
408         final Person[] persons = parsePerson(getPropertyDocumentArray(KEY_PERSON));
409         final String locusIdString = getPropertyString(KEY_LOCUS_ID);
410         final LocusId locusId = locusIdString == null ? null : new LocusId(locusIdString);
411         final int rank = Integer.parseInt(getPropertyString(KEY_RANK));
412         final int implicitRank = (int) getPropertyLong(KEY_IMPLICIT_RANK);
413         final byte[] extrasByte = getPropertyBytes(KEY_EXTRAS);
414         final PersistableBundle extras = transformToPersistableBundle(extrasByte);
415         final int flags = parseFlags(getPropertyStringArray(KEY_FLAGS));
416         final int iconResId = (int) getPropertyLong(KEY_ICON_RES_ID);
417         final String iconResName = getPropertyString(KEY_ICON_RES_NAME);
418         final String iconUri = getPropertyString(KEY_ICON_URI);
419         final String bitmapPath = getPropertyString(KEY_BITMAP_PATH);
420         final int disabledReason = Integer.parseInt(getPropertyString(KEY_DISABLED_REASON));
421         final ShortcutInfo si = new ShortcutInfo(
422                 userId, getId(), packageName, activity, icon, shortLabel, shortLabelResId,
423                 shortLabelResName, longLabel, longLabelResId, longLabelResName, disabledMessage,
424                 disabledMessageResId, disabledMessageResName, categoriesSet, intents, rank, extras,
425                 getCreationTimestampMillis(), flags, iconResId, iconResName, bitmapPath, iconUri,
426                 disabledReason, persons, locusId, null);
427         si.setImplicitRank(implicitRank);
428         if ((implicitRank & ShortcutInfo.RANK_CHANGED_BIT) != 0) {
429             si.setRankChanged();
430         }
431         return si;
432     }
433 
434     /**
435      * @hide
436      */
437     @NonNull
toGenericDocuments( @onNull final Collection<ShortcutInfo> shortcuts)438     public static List<GenericDocument> toGenericDocuments(
439             @NonNull final Collection<ShortcutInfo> shortcuts) {
440         final List<GenericDocument> docs = new ArrayList<>(shortcuts.size());
441         for (ShortcutInfo si : shortcuts) {
442             docs.add(AppSearchShortcutInfo.instance(si));
443         }
444         return docs;
445     }
446 
447     /** @hide */
448     @VisibleForTesting
449     public static class Builder extends GenericDocument.Builder<Builder> {
450 
451         private List<String> mFlags = new ArrayList<>(1);
452         private boolean mHasStringResource = false;
453 
Builder(String packageName, String id)454         public Builder(String packageName, String id) {
455             super(/*namespace=*/ packageName, id, SCHEMA_TYPE);
456         }
457 
458         /**
459          * @hide
460          */
461         @NonNull
setLocusId(@ullable final LocusId locusId)462         public Builder setLocusId(@Nullable final LocusId locusId) {
463             if (locusId != null) {
464                 setPropertyString(KEY_LOCUS_ID, locusId.getId());
465             }
466             return this;
467         }
468 
469         /**
470          * @hide
471          */
472         @NonNull
setActivity(@ullable final ComponentName activity)473         public Builder setActivity(@Nullable final ComponentName activity) {
474             if (activity != null) {
475                 setPropertyString(KEY_ACTIVITY, activity.flattenToShortString());
476             }
477             return this;
478         }
479 
480         /**
481          * @hide
482          */
483         @NonNull
setShortLabel(@ullable final CharSequence shortLabel)484         public Builder setShortLabel(@Nullable final CharSequence shortLabel) {
485             if (!TextUtils.isEmpty(shortLabel)) {
486                 setPropertyString(KEY_SHORT_LABEL, Preconditions.checkStringNotEmpty(
487                         shortLabel, "shortLabel cannot be empty").toString());
488             }
489             return this;
490         }
491 
492         /**
493          * @hide
494          */
495         @NonNull
setShortLabelResId(final int shortLabelResId)496         public Builder setShortLabelResId(final int shortLabelResId) {
497             setPropertyLong(KEY_SHORT_LABEL_RES_ID, shortLabelResId);
498             if (shortLabelResId != 0) {
499                 mHasStringResource = true;
500             }
501             return this;
502         }
503 
504         /**
505          * @hide
506          */
setShortLabelResName(@ullable final String shortLabelResName)507         public Builder setShortLabelResName(@Nullable final String shortLabelResName) {
508             if (!TextUtils.isEmpty(shortLabelResName)) {
509                 setPropertyString(KEY_SHORT_LABEL_RES_NAME, shortLabelResName);
510             }
511             return this;
512         }
513 
514         /**
515          * @hide
516          */
517         @NonNull
setLongLabel(@ullable final CharSequence longLabel)518         public Builder setLongLabel(@Nullable final CharSequence longLabel) {
519             if (!TextUtils.isEmpty(longLabel)) {
520                 setPropertyString(KEY_LONG_LABEL, Preconditions.checkStringNotEmpty(
521                         longLabel, "longLabel cannot be empty").toString());
522             }
523             return this;
524         }
525 
526         /**
527          * @hide
528          */
529         @NonNull
setLongLabelResId(final int longLabelResId)530         public Builder setLongLabelResId(final int longLabelResId) {
531             setPropertyLong(KEY_LONG_LABEL_RES_ID, longLabelResId);
532             if (longLabelResId != 0) {
533                 mHasStringResource = true;
534             }
535             return this;
536         }
537 
538         /**
539          * @hide
540          */
setLongLabelResName(@ullable final String longLabelResName)541         public Builder setLongLabelResName(@Nullable final String longLabelResName) {
542             if (!TextUtils.isEmpty(longLabelResName)) {
543                 setPropertyString(KEY_LONG_LABEL_RES_NAME, longLabelResName);
544             }
545             return this;
546         }
547 
548         /**
549          * @hide
550          */
551         @NonNull
setDisabledMessage(@ullable final CharSequence disabledMessage)552         public Builder setDisabledMessage(@Nullable final CharSequence disabledMessage) {
553             if (!TextUtils.isEmpty(disabledMessage)) {
554                 setPropertyString(KEY_DISABLED_MESSAGE, Preconditions.checkStringNotEmpty(
555                         disabledMessage, "disabledMessage cannot be empty").toString());
556             }
557             return this;
558         }
559 
560         /**
561          * @hide
562          */
563         @NonNull
setDisabledMessageResId(final int disabledMessageResId)564         public Builder setDisabledMessageResId(final int disabledMessageResId) {
565             setPropertyLong(KEY_DISABLED_MESSAGE_RES_ID, disabledMessageResId);
566             if (disabledMessageResId != 0) {
567                 mHasStringResource = true;
568             }
569             return this;
570         }
571 
572         /**
573          * @hide
574          */
setDisabledMessageResName(@ullable final String disabledMessageResName)575         public Builder setDisabledMessageResName(@Nullable final String disabledMessageResName) {
576             if (!TextUtils.isEmpty(disabledMessageResName)) {
577                 setPropertyString(KEY_DISABLED_MESSAGE_RES_NAME, disabledMessageResName);
578             }
579             return this;
580         }
581 
582         /**
583          * @hide
584          */
585         @NonNull
setCategories(@ullable final Set<String> categories)586         public Builder setCategories(@Nullable final Set<String> categories) {
587             if (categories != null && !categories.isEmpty()) {
588                 setPropertyString(KEY_CATEGORIES, categories.stream().toArray(String[]::new));
589             }
590             return this;
591         }
592 
593         /**
594          * @hide
595          */
596         @NonNull
setIntent(@ullable final Intent intent)597         public Builder setIntent(@Nullable final Intent intent) {
598             if (intent == null) {
599                 return this;
600             }
601             return setIntents(new Intent[]{intent});
602         }
603 
604         /**
605          * @hide
606          */
607         @NonNull
setIntents(@ullable final Intent[] intents)608         public Builder setIntents(@Nullable final Intent[] intents) {
609             if (intents == null || intents.length == 0) {
610                 return this;
611             }
612             for (Intent intent : intents) {
613                 Objects.requireNonNull(intent, "intents cannot contain null");
614                 Objects.requireNonNull(intent.getAction(), "intent's action must be set");
615             }
616             final byte[][] intentExtrases = new byte[intents.length][];
617             for (int i = 0; i < intents.length; i++) {
618                 final Intent intent = intents[i];
619                 final Bundle extras = intent.getExtras();
620                 intentExtrases[i] = extras == null
621                         ? new byte[0] : transformToByteArray(new PersistableBundle(extras));
622             }
623             setPropertyString(KEY_INTENTS, Arrays.stream(intents).map(it -> it.toUri(0))
624                     .toArray(String[]::new));
625             setPropertyBytes(KEY_INTENT_PERSISTABLE_EXTRAS, intentExtrases);
626             return this;
627         }
628 
629         /**
630          * @hide
631          */
632         @NonNull
setPerson(@ullable final Person person)633         public Builder setPerson(@Nullable final Person person) {
634             if (person == null) {
635                 return this;
636             }
637             return setPersons(new Person[]{person});
638         }
639 
640         /**
641          * @hide
642          */
643         @NonNull
setPersons(@ullable final Person[] persons)644         public Builder setPersons(@Nullable final Person[] persons) {
645             if (persons == null || persons.length == 0) {
646                 return this;
647             }
648             final GenericDocument[] documents = new GenericDocument[persons.length];
649             for (int i = 0; i < persons.length; i++) {
650                 final Person person = persons[i];
651                 if (person == null) continue;
652                 final AppSearchPerson appSearchPerson = AppSearchPerson.instance(person);
653                 documents[i] = appSearchPerson;
654             }
655             setPropertyDocument(KEY_PERSON, documents);
656             return this;
657         }
658 
659         /**
660          * @hide
661          */
662         @NonNull
setRank(final int rank)663         public Builder setRank(final int rank) {
664             Preconditions.checkArgument((0 <= rank), "Rank cannot be negative");
665             setPropertyString(KEY_RANK, String.valueOf(rank));
666             if (rank != 0) {
667                 mFlags.add(HAS_NON_ZERO_RANK);
668             }
669             return this;
670         }
671 
672         /**
673          * @hide
674          */
675         @NonNull
setImplicitRank(final int rank)676         public Builder setImplicitRank(final int rank) {
677             setPropertyLong(KEY_IMPLICIT_RANK, rank);
678             return this;
679         }
680 
681         /**
682          * @hide
683          */
684         @NonNull
setExtras(@ullable final PersistableBundle extras)685         public Builder setExtras(@Nullable final PersistableBundle extras) {
686             if (extras != null) {
687                 setPropertyBytes(KEY_EXTRAS, transformToByteArray(extras));
688             }
689             return this;
690         }
691 
692         /**
693          * @hide
694          */
setFlags(@hortcutInfo.ShortcutFlags final int flags)695         public Builder setFlags(@ShortcutInfo.ShortcutFlags final int flags) {
696             final String[] flagArray = flattenFlags(flags);
697             if (flagArray != null && flagArray.length > 0) {
698                 mFlags.addAll(Arrays.asList(flagArray));
699             }
700             return this;
701         }
702 
703         /**
704          * @hide
705          */
706         @NonNull
setIconResId(@ullable final int iconResId)707         public Builder setIconResId(@Nullable final int iconResId) {
708             setPropertyLong(KEY_ICON_RES_ID, iconResId);
709             return this;
710         }
711 
712         /**
713          * @hide
714          */
setIconResName(@ullable final String iconResName)715         public Builder setIconResName(@Nullable final String iconResName) {
716             if (!TextUtils.isEmpty(iconResName)) {
717                 setPropertyString(KEY_ICON_RES_NAME, iconResName);
718             }
719             return this;
720         }
721 
722         /**
723          * @hide
724          */
setBitmapPath(@ullable final String bitmapPath)725         public Builder setBitmapPath(@Nullable final String bitmapPath) {
726             if (!TextUtils.isEmpty(bitmapPath)) {
727                 setPropertyString(KEY_BITMAP_PATH, bitmapPath);
728                 mFlags.add(HAS_BITMAP_PATH);
729             }
730             return this;
731         }
732 
733         /**
734          * @hide
735          */
setIconUri(@ullable final String iconUri)736         public Builder setIconUri(@Nullable final String iconUri) {
737             if (!TextUtils.isEmpty(iconUri)) {
738                 setPropertyString(KEY_ICON_URI, iconUri);
739             }
740             return this;
741         }
742 
743         /**
744          * @hide
745          */
setDisabledReason(@hortcutInfo.DisabledReason final int disabledReason)746         public Builder setDisabledReason(@ShortcutInfo.DisabledReason final int disabledReason) {
747             setPropertyString(KEY_DISABLED_REASON, String.valueOf(disabledReason));
748             return this;
749         }
750 
751         /**
752          * @hide
753          */
754         @NonNull
755         @Override
build()756         public AppSearchShortcutInfo build() {
757             if (mHasStringResource) {
758                 mFlags.add(HAS_STRING_RESOURCE);
759             }
760             setPropertyString(KEY_FLAGS, mFlags.toArray(new String[0]));
761             return new AppSearchShortcutInfo(super.build());
762         }
763     }
764 
765     /**
766      * Convert PersistableBundle into byte[] for persistence.
767      */
768     @Nullable
transformToByteArray(@onNull final PersistableBundle extras)769     private static byte[] transformToByteArray(@NonNull final PersistableBundle extras) {
770         Objects.requireNonNull(extras);
771         try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
772             new PersistableBundle(extras).writeToStream(baos);
773             return baos.toByteArray();
774         } catch (IOException e) {
775             return null;
776         }
777     }
778 
779     /**
780      * Convert byte[] into Bundle.
781      */
782     @Nullable
transformToBundle(@ullable final byte[] extras)783     private Bundle transformToBundle(@Nullable final byte[] extras) {
784         if (extras == null) {
785             return null;
786         }
787         Objects.requireNonNull(extras);
788         try (ByteArrayInputStream bais = new ByteArrayInputStream(extras)) {
789             final Bundle ret = new Bundle();
790             ret.putAll(PersistableBundle.readFromStream(bais));
791             return ret;
792         } catch (IOException e) {
793             return null;
794         }
795     }
796 
797     /**
798      * Convert byte[] into PersistableBundle.
799      */
800     @Nullable
transformToPersistableBundle(@ullable final byte[] extras)801     private PersistableBundle transformToPersistableBundle(@Nullable final byte[] extras) {
802         if (extras == null) {
803             return null;
804         }
805         try (ByteArrayInputStream bais = new ByteArrayInputStream(extras)) {
806             return PersistableBundle.readFromStream(bais);
807         } catch (IOException e) {
808             return null;
809         }
810     }
811 
flattenFlags(@hortcutInfo.ShortcutFlags final int flags)812     private static String[] flattenFlags(@ShortcutInfo.ShortcutFlags final int flags) {
813         final List<String> flattenedFlags = new ArrayList<>();
814         for (int i = 0; i < 31; i++) {
815             final int mask = 1 << i;
816             final String value = flagToString(flags, mask);
817             if (value != null) {
818                 flattenedFlags.add(value);
819             }
820         }
821         return flattenedFlags.toArray(new String[0]);
822     }
823 
824     @Nullable
flagToString( @hortcutInfo.ShortcutFlags final int flags, final int mask)825     private static String flagToString(
826             @ShortcutInfo.ShortcutFlags final int flags, final int mask) {
827         switch (mask) {
828             case ShortcutInfo.FLAG_DYNAMIC:
829                 return (flags & mask) != 0 ? IS_DYNAMIC : NOT_DYNAMIC;
830             case ShortcutInfo.FLAG_PINNED:
831                 return (flags & mask) != 0 ? IS_PINNED : NOT_PINNED;
832             case ShortcutInfo.FLAG_HAS_ICON_RES:
833                 return (flags & mask) != 0 ? HAS_ICON_RES : NO_ICON_RES;
834             case ShortcutInfo.FLAG_HAS_ICON_FILE:
835                 return (flags & mask) != 0 ? HAS_ICON_FILE : NO_ICON_FILE;
836             case ShortcutInfo.FLAG_KEY_FIELDS_ONLY:
837                 return (flags & mask) != 0 ? IS_KEY_FIELD_ONLY : NOT_KEY_FIELD_ONLY;
838             case ShortcutInfo.FLAG_MANIFEST:
839                 return (flags & mask) != 0 ? IS_MANIFEST : NOT_MANIFEST;
840             case ShortcutInfo.FLAG_DISABLED:
841                 return (flags & mask) != 0 ? IS_DISABLED : NOT_DISABLED;
842             case ShortcutInfo.FLAG_STRINGS_RESOLVED:
843                 return (flags & mask) != 0 ? ARE_STRINGS_RESOLVED : NOT_STRINGS_RESOLVED;
844             case ShortcutInfo.FLAG_IMMUTABLE:
845                 return (flags & mask) != 0 ? IS_IMMUTABLE : NOT_IMMUTABLE;
846             case ShortcutInfo.FLAG_ADAPTIVE_BITMAP:
847                 return (flags & mask) != 0 ? HAS_ADAPTIVE_BITMAP : NO_ADAPTIVE_BITMAP;
848             case ShortcutInfo.FLAG_RETURNED_BY_SERVICE:
849                 return (flags & mask) != 0 ? IS_RETURNED_BY_SERVICE : NOT_RETURNED_BY_SERVICE;
850             case ShortcutInfo.FLAG_ICON_FILE_PENDING_SAVE:
851                 return (flags & mask) != 0 ? HAS_ICON_FILE_PENDING_SAVE : NO_ICON_FILE_PENDING_SAVE;
852             case ShortcutInfo.FLAG_SHADOW:
853                 return (flags & mask) != 0 ? IS_SHADOW : NOT_SHADOW;
854             case ShortcutInfo.FLAG_LONG_LIVED:
855                 return (flags & mask) != 0 ? IS_LONG_LIVED : NOT_LONG_LIVED;
856             case ShortcutInfo.FLAG_HAS_ICON_URI:
857                 return (flags & mask) != 0 ? HAS_ICON_URI : NO_ICON_URI;
858             case ShortcutInfo.FLAG_CACHED_NOTIFICATIONS:
859                 return (flags & mask) != 0 ? IS_CACHED_NOTIFICATION : NOT_CACHED_NOTIFICATION;
860             case ShortcutInfo.FLAG_CACHED_BUBBLES:
861                 return (flags & mask) != 0 ? IS_CACHED_BUBBLE : NOT_CACHED_BUBBLE;
862             case ShortcutInfo.FLAG_CACHED_PEOPLE_TILE:
863                 return (flags & mask) != 0 ? IS_CACHED_PEOPLE_TITLE : NOT_CACHED_PEOPLE_TITLE;
864             default:
865                 return null;
866         }
867     }
868 
parseFlags(@ullable final String[] flags)869     private static int parseFlags(@Nullable final String[] flags) {
870         if (flags == null) {
871             return 0;
872         }
873         int ret = 0;
874         for (int i = 0; i < flags.length; i++) {
875             ret = ret | parseFlag(flags[i]);
876         }
877         return ret;
878     }
879 
parseFlag(final String value)880     private static int parseFlag(final String value) {
881         switch (value) {
882             case IS_DYNAMIC:
883                 return ShortcutInfo.FLAG_DYNAMIC;
884             case IS_PINNED:
885                 return ShortcutInfo.FLAG_PINNED;
886             case HAS_ICON_RES:
887                 return ShortcutInfo.FLAG_HAS_ICON_RES;
888             case HAS_ICON_FILE:
889                 return ShortcutInfo.FLAG_HAS_ICON_FILE;
890             case IS_KEY_FIELD_ONLY:
891                 return ShortcutInfo.FLAG_KEY_FIELDS_ONLY;
892             case IS_MANIFEST:
893                 return ShortcutInfo.FLAG_MANIFEST;
894             case IS_DISABLED:
895                 return ShortcutInfo.FLAG_DISABLED;
896             case ARE_STRINGS_RESOLVED:
897                 return ShortcutInfo.FLAG_STRINGS_RESOLVED;
898             case IS_IMMUTABLE:
899                 return ShortcutInfo.FLAG_IMMUTABLE;
900             case HAS_ADAPTIVE_BITMAP:
901                 return ShortcutInfo.FLAG_ADAPTIVE_BITMAP;
902             case IS_RETURNED_BY_SERVICE:
903                 return ShortcutInfo.FLAG_RETURNED_BY_SERVICE;
904             case HAS_ICON_FILE_PENDING_SAVE:
905                 return ShortcutInfo.FLAG_ICON_FILE_PENDING_SAVE;
906             case IS_SHADOW:
907                 return ShortcutInfo.FLAG_SHADOW;
908             case IS_LONG_LIVED:
909                 return ShortcutInfo.FLAG_LONG_LIVED;
910             case HAS_ICON_URI:
911                 return ShortcutInfo.FLAG_HAS_ICON_URI;
912             case IS_CACHED_NOTIFICATION:
913                 return ShortcutInfo.FLAG_CACHED_NOTIFICATIONS;
914             case IS_CACHED_BUBBLE:
915                 return ShortcutInfo.FLAG_CACHED_BUBBLES;
916             case IS_CACHED_PEOPLE_TITLE:
917                 return ShortcutInfo.FLAG_CACHED_PEOPLE_TILE;
918             default:
919                 return 0;
920         }
921     }
922 
923     @NonNull
parsePerson(@ullable final GenericDocument[] persons)924     private static Person[] parsePerson(@Nullable final GenericDocument[] persons) {
925         if (persons == null) return new Person[0];
926         final Person[] ret = new Person[persons.length];
927         for (int i = 0; i < persons.length; i++) {
928             final GenericDocument document = persons[i];
929             if (document == null) continue;
930             final AppSearchPerson person = new AppSearchPerson(document);
931             ret[i] = person.toPerson();
932         }
933         return ret;
934     }
935 }
936