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.ArrayMap;
33 import android.util.ArraySet;
34 
35 import com.android.internal.annotations.VisibleForTesting;
36 import com.android.internal.util.Preconditions;
37 
38 import java.io.ByteArrayInputStream;
39 import java.io.ByteArrayOutputStream;
40 import java.io.IOException;
41 import java.net.URISyntaxException;
42 import java.util.ArrayList;
43 import java.util.Arrays;
44 import java.util.Collection;
45 import java.util.List;
46 import java.util.Map;
47 import java.util.Objects;
48 import java.util.Set;
49 import java.util.concurrent.TimeUnit;
50 
51 /**
52  * A {@link GenericDocument} representation of {@link ShortcutInfo} object.
53  * @hide
54  */
55 public class AppSearchShortcutInfo extends GenericDocument {
56 
57     /** The TTL (time-to-live) of the shortcut, in milli-second. */
58     public static final long SHORTCUT_TTL = TimeUnit.DAYS.toMillis(90);
59 
60     /** The name of the schema type for {@link ShortcutInfo} documents.*/
61     public static final String SCHEMA_TYPE = "Shortcut";
62 
63     /** @hide */
64     public static final int SCHEMA_VERSION = 3;
65 
66     /**
67      * Property name of the activity this {@link ShortcutInfo} is associated with.
68      * See {@link ShortcutInfo#getActivity()}.
69      */
70     public static final String KEY_ACTIVITY = "activity";
71 
72     /**
73      * Property name of the short description of this {@link ShortcutInfo}.
74      * See {@link ShortcutInfo#getShortLabel()}.
75      */
76     public static final String KEY_SHORT_LABEL = "shortLabel";
77 
78     /**
79      * Property name of the long description of this {@link ShortcutInfo}.
80      * See {@link ShortcutInfo#getLongLabel()}.
81      */
82     public static final String KEY_LONG_LABEL = "longLabel";
83 
84     /**
85      * @hide
86      */
87     public static final String KEY_DISABLED_MESSAGE = "disabledMessage";
88 
89     /**
90      * Property name of the categories this {@link ShortcutInfo} is associated with.
91      * See {@link ShortcutInfo#getCategories()}.
92      */
93     public static final String KEY_CATEGORIES = "categories";
94 
95     /**
96      * Property name of the intents this {@link ShortcutInfo} is associated with.
97      * See {@link ShortcutInfo#getIntents()}.
98      */
99     public static final String KEY_INTENTS = "intents";
100 
101     /**
102      * @hide
103      */
104     public static final String KEY_INTENT_PERSISTABLE_EXTRAS = "intentPersistableExtras";
105 
106     /**
107      * Property name of {@link Person} objects this {@link ShortcutInfo} is associated with.
108      * See {@link ShortcutInfo#getPersons()}.
109      */
110     public static final String KEY_PERSON = "person";
111 
112     /**
113      * Property name of {@link LocusId} this {@link ShortcutInfo} is associated with.
114      * See {@link ShortcutInfo#getLocusId()}.
115      */
116     public static final String KEY_LOCUS_ID = "locusId";
117 
118     /**
119      * @hide
120      */
121     public static final String KEY_EXTRAS = "extras";
122 
123     /**
124      * Property name of the states this {@link ShortcutInfo} is currently in.
125      * Possible values are one or more of the following:
126      *     {@link #IS_DYNAMIC}, {@link #NOT_DYNAMIC}, {@link #IS_MANIFEST}, {@link #NOT_MANIFEST},
127      *     {@link #IS_DISABLED}, {@link #NOT_DISABLED}, {@link #IS_IMMUTABLE},
128      *     {@link #NOT_IMMUTABLE}
129      *
130      */
131     public static final String KEY_FLAGS = "flags";
132 
133     /**
134      * @hide
135      */
136     public static final String KEY_ICON_RES_ID = "iconResId";
137 
138     /**
139      * @hide
140      */
141     public static final String KEY_ICON_RES_NAME = "iconResName";
142 
143     /**
144      * @hide
145      */
146     public static final String KEY_ICON_URI = "iconUri";
147 
148     /**
149      * @hide
150      */
151     public static final String KEY_DISABLED_REASON = "disabledReason";
152 
153     /**
154      * Property name of capability this {@link ShortcutInfo} is associated with.
155      * See {@link ShortcutInfo#hasCapability(String)}.
156      */
157     public static final String KEY_CAPABILITY = "capability";
158 
159     /**
160      * Property name of capability binding this {@link ShortcutInfo} is associated with.
161      * See {@link ShortcutInfo#getCapabilityParameters(String, String)}.
162      */
163     public static final String KEY_CAPABILITY_BINDINGS = "capabilityBindings";
164 
165     public static final AppSearchSchema SCHEMA = new AppSearchSchema.Builder(SCHEMA_TYPE)
166             .addProperty(new AppSearchSchema.StringPropertyConfig.Builder(KEY_ACTIVITY)
167                     .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
168                     .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
169                     .setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
170                     .build()
171 
172             ).addProperty(new AppSearchSchema.StringPropertyConfig.Builder(KEY_SHORT_LABEL)
173                     .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
174                     .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
175                     .setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
176                     .build()
177 
178             ).addProperty(new AppSearchSchema.StringPropertyConfig.Builder(KEY_LONG_LABEL)
179                     .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
180                     .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
181                     .setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
182                     .build()
183 
184             ).addProperty(new AppSearchSchema.StringPropertyConfig.Builder(KEY_DISABLED_MESSAGE)
185                     .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
186                     .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_NONE)
187                     .setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_NONE)
188                     .build()
189 
190             ).addProperty(new AppSearchSchema.StringPropertyConfig.Builder(KEY_CATEGORIES)
191                     .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
192                     .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
193                     .setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
194                     .build()
195 
196             ).addProperty(new AppSearchSchema.StringPropertyConfig.Builder(KEY_INTENTS)
197                     .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
198                     .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_NONE)
199                     .setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_NONE)
200                     .build()
201 
202             ).addProperty(new AppSearchSchema.BytesPropertyConfig.Builder(
203                     KEY_INTENT_PERSISTABLE_EXTRAS)
204                     .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
205                     .build()
206 
207             ).addProperty(new AppSearchSchema.DocumentPropertyConfig.Builder(
208                     KEY_PERSON, AppSearchShortcutPerson.SCHEMA_TYPE)
209                     .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
210                     .build()
211 
212             ).addProperty(new AppSearchSchema.StringPropertyConfig.Builder(KEY_LOCUS_ID)
213                     .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
214                     .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
215                     .setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
216                     .build()
217 
218             ).addProperty(new AppSearchSchema.BytesPropertyConfig.Builder(KEY_EXTRAS)
219                     .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
220                     .build()
221 
222             ).addProperty(new AppSearchSchema.StringPropertyConfig.Builder(KEY_FLAGS)
223                     .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
224                     .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
225                     .setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
226                     .build()
227 
228             ).addProperty(new AppSearchSchema.LongPropertyConfig.Builder(KEY_ICON_RES_ID)
229                     .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
230                     .build()
231 
232             ).addProperty(new AppSearchSchema.StringPropertyConfig.Builder(KEY_ICON_RES_NAME)
233                     .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
234                     .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_NONE)
235                     .setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_NONE)
236                     .build()
237 
238             ).addProperty(new AppSearchSchema.StringPropertyConfig.Builder(KEY_ICON_URI)
239                     .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
240                     .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_NONE)
241                     .setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_NONE)
242                     .build()
243 
244             ).addProperty(new AppSearchSchema.StringPropertyConfig.Builder(KEY_DISABLED_REASON)
245                     .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
246                     .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
247                     .setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
248                     .build()
249 
250             ).addProperty(new AppSearchSchema.StringPropertyConfig.Builder(KEY_CAPABILITY)
251                     .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
252                     .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
253                     .setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
254                     .build()
255 
256             ).addProperty(new AppSearchSchema.StringPropertyConfig.Builder(KEY_CAPABILITY_BINDINGS)
257                     .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
258                     .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
259                     .setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
260                     .build()
261 
262             ).build();
263 
264     /**
265      * The string representation of every flag within {@link ShortcutInfo}. Note that its value
266      * needs to be camelCase since AppSearch's tokenizer will break the word when it sees
267      * underscore.
268      */
269 
270     /**
271      * Indicates the {@link ShortcutInfo} is dynamic shortcut.
272      * See {@link #KEY_FLAGS}
273      * See {@link ShortcutInfo#isDynamic()}.
274      */
275     public static final String IS_DYNAMIC = "Dyn";
276 
277     /**
278      * Indicates the {@link ShortcutInfo} is not a dynamic shortcut.
279      * See {@link #KEY_FLAGS}
280      * See {@link ShortcutInfo#isDynamic()}.
281      */
282     public static final String NOT_DYNAMIC = "nDyn";
283 
284     /**
285      * Indicates the {@link ShortcutInfo} is manifest shortcut.
286      * See {@link #KEY_FLAGS}
287      * See {@link ShortcutInfo#isDeclaredInManifest()}.
288      */
289     public static final String IS_MANIFEST = "Man";
290 
291     /**
292      * Indicates the {@link ShortcutInfo} is manifest shortcut.
293      * See {@link #KEY_FLAGS}
294      * See {@link ShortcutInfo#isDeclaredInManifest()}.
295      */
296     public static final String NOT_MANIFEST = "nMan";
297 
298     /**
299      * Indicates the {@link ShortcutInfo} is disabled.
300      * See {@link #KEY_FLAGS}
301      * See {@link ShortcutInfo#isEnabled()}.
302      */
303     public static final String IS_DISABLED = "Dis";
304 
305     /**
306      * Indicates the {@link ShortcutInfo} is enabled.
307      * See {@link #KEY_FLAGS}
308      * See {@link ShortcutInfo#isEnabled()}.
309      */
310     public static final String NOT_DISABLED = "nDis";
311 
312     /**
313      * Indicates the {@link ShortcutInfo} was originally from manifest, but currently disabled.
314      * See {@link #KEY_FLAGS}
315      * See {@link ShortcutInfo#isOriginallyFromManifest()}.
316      */
317     public static final String IS_IMMUTABLE = "Im";
318 
319     /**
320      * Indicates the {@link ShortcutInfo} was not originally from manifest.
321      * See {@link #KEY_FLAGS}
322      * See {@link ShortcutInfo#isOriginallyFromManifest()}.
323      */
324     public static final String NOT_IMMUTABLE = "nIm";
325 
AppSearchShortcutInfo(@onNull GenericDocument document)326     public AppSearchShortcutInfo(@NonNull GenericDocument document) {
327         super(document);
328     }
329 
330     /**
331      * @hide
332      */
333     @NonNull
instance(@onNull final ShortcutInfo shortcutInfo)334     public static AppSearchShortcutInfo instance(@NonNull final ShortcutInfo shortcutInfo) {
335         Objects.requireNonNull(shortcutInfo);
336         return new Builder(shortcutInfo.getPackage(), shortcutInfo.getId())
337                 .setActivity(shortcutInfo.getActivity())
338                 .setShortLabel(shortcutInfo.getShortLabel())
339                 .setLongLabel(shortcutInfo.getLongLabel())
340                 .setDisabledMessage(shortcutInfo.getDisabledMessage())
341                 .setCategories(shortcutInfo.getCategories())
342                 .setIntents(shortcutInfo.getIntents())
343                 .setExtras(shortcutInfo.getExtras())
344                 .setCreationTimestampMillis(shortcutInfo.getLastChangedTimestamp())
345                 .setFlags(shortcutInfo.getFlags())
346                 .setIconResId(shortcutInfo.getIconResourceId())
347                 .setIconResName(shortcutInfo.getIconResName())
348                 .setIconUri(shortcutInfo.getIconUri())
349                 .setDisabledReason(shortcutInfo.getDisabledReason())
350                 .setPersons(shortcutInfo.getPersons())
351                 .setLocusId(shortcutInfo.getLocusId())
352                 .setCapabilityBindings(shortcutInfo.getCapabilityBindingsInternal())
353                 .setTtlMillis(SHORTCUT_TTL)
354                 .build();
355     }
356 
357     /**
358      * Converts this {@link GenericDocument} object into {@link ShortcutInfo} to read the
359      * information.
360      */
361     @NonNull
toShortcutInfo(@serIdInt int userId)362     public ShortcutInfo toShortcutInfo(@UserIdInt int userId) {
363         final String packageName = getNamespace();
364         final String activityString = getPropertyString(KEY_ACTIVITY);
365         final ComponentName activity = activityString == null
366                 ? null : ComponentName.unflattenFromString(activityString);
367         // TODO: proper icon handling
368         // NOTE: bitmap based icons are currently saved in side-channel (see ShortcutBitmapSaver),
369         // re-creating Icon object at creation time implies turning this function into async since
370         // loading bitmap is I/O bound. Since ShortcutInfo#getIcon is already annotated with
371         // @hide and @UnsupportedAppUsage, we could migrate existing usage in platform with
372         // LauncherApps#getShortcutIconDrawable instead.
373         final Icon icon = null;
374         final String shortLabel = getPropertyString(KEY_SHORT_LABEL);
375         final String longLabel = getPropertyString(KEY_LONG_LABEL);
376         final String disabledMessage = getPropertyString(KEY_DISABLED_MESSAGE);
377         final String[] categories = getPropertyStringArray(KEY_CATEGORIES);
378         final Set<String> categoriesSet = categories == null
379                 ? null : new ArraySet<>(Arrays.asList(categories));
380         final String[] intentsStrings = getPropertyStringArray(KEY_INTENTS);
381         final Intent[] intents = intentsStrings == null
382                 ? new Intent[0] : Arrays.stream(intentsStrings).map(uri -> {
383                     if (TextUtils.isEmpty(uri)) {
384                         return new Intent(Intent.ACTION_VIEW);
385                     }
386                     try {
387                         return Intent.parseUri(uri, /* flags =*/ 0);
388                     } catch (URISyntaxException e) {
389                         // ignore malformed entry
390                     }
391                     return null;
392                 }).toArray(Intent[]::new);
393         final byte[][] intentExtrasesBytes = getPropertyBytesArray(KEY_INTENT_PERSISTABLE_EXTRAS);
394         final Bundle[] intentExtrases = intentExtrasesBytes == null
395                 ? null : Arrays.stream(intentExtrasesBytes)
396                 .map(this::transformToBundle).toArray(Bundle[]::new);
397         if (intents != null) {
398             for (int i = 0; i < intents.length; i++) {
399                 final Intent intent = intents[i];
400                 if (intent == null || intentExtrases == null || intentExtrases.length <= i
401                         || intentExtrases[i] == null || intentExtrases[i].size() == 0) {
402                     continue;
403                 }
404                 intent.replaceExtras(intentExtrases[i]);
405             }
406         }
407         final Person[] persons = parsePerson(getPropertyDocumentArray(KEY_PERSON));
408         final String locusIdString = getPropertyString(KEY_LOCUS_ID);
409         final LocusId locusId = locusIdString == null ? null : new LocusId(locusIdString);
410         final byte[] extrasByte = getPropertyBytes(KEY_EXTRAS);
411         final PersistableBundle extras = transformToPersistableBundle(extrasByte);
412         final int flags = parseFlags(getPropertyStringArray(KEY_FLAGS));
413         final int iconResId = (int) getPropertyLong(KEY_ICON_RES_ID);
414         final String iconResName = getPropertyString(KEY_ICON_RES_NAME);
415         final String iconUri = getPropertyString(KEY_ICON_URI);
416         final String disabledReasonString = getPropertyString(KEY_DISABLED_REASON);
417         final int disabledReason = !TextUtils.isEmpty(disabledReasonString)
418                 ? Integer.parseInt(getPropertyString(KEY_DISABLED_REASON))
419                 : ShortcutInfo.DISABLED_REASON_NOT_DISABLED;
420         final Map<String, Map<String, List<String>>> capabilityBindings =
421                 parseCapabilityBindings(getPropertyStringArray(KEY_CAPABILITY_BINDINGS));
422         return new ShortcutInfo(
423                 userId, getId(), packageName, activity, icon, shortLabel, 0,
424                 null, longLabel, 0, null, disabledMessage,
425                 0, null, categoriesSet, intents,
426                 ShortcutInfo.RANK_NOT_SET, extras, getCreationTimestampMillis(), flags, iconResId,
427                 iconResName, null, iconUri, disabledReason, persons, locusId,
428                 null, capabilityBindings);
429     }
430 
431     /**
432      * @hide
433      */
434     @NonNull
toGenericDocuments( @onNull final Collection<ShortcutInfo> shortcuts)435     public static List<GenericDocument> toGenericDocuments(
436             @NonNull final Collection<ShortcutInfo> shortcuts) {
437         final List<GenericDocument> docs = new ArrayList<>(shortcuts.size());
438         for (ShortcutInfo si : shortcuts) {
439             docs.add(AppSearchShortcutInfo.instance(si));
440         }
441         return docs;
442     }
443 
444     /** @hide */
445     @VisibleForTesting
446     public static class Builder extends GenericDocument.Builder<Builder> {
447 
448         private final List<String> mFlags = new ArrayList<>(1);
449 
Builder(String packageName, String id)450         public Builder(String packageName, String id) {
451             super(/*namespace=*/ packageName, id, SCHEMA_TYPE);
452         }
453 
454         /**
455          * @hide
456          */
457         @NonNull
setLocusId(@ullable final LocusId locusId)458         public Builder setLocusId(@Nullable final LocusId locusId) {
459             if (locusId != null) {
460                 setPropertyString(KEY_LOCUS_ID, locusId.getId());
461             }
462             return this;
463         }
464 
465         /**
466          * @hide
467          */
468         @NonNull
setActivity(@ullable final ComponentName activity)469         public Builder setActivity(@Nullable final ComponentName activity) {
470             if (activity != null) {
471                 setPropertyString(KEY_ACTIVITY, activity.flattenToShortString());
472             }
473             return this;
474         }
475 
476         /**
477          * @hide
478          */
479         @NonNull
setShortLabel(@ullable final CharSequence shortLabel)480         public Builder setShortLabel(@Nullable final CharSequence shortLabel) {
481             if (!TextUtils.isEmpty(shortLabel)) {
482                 setPropertyString(KEY_SHORT_LABEL, Preconditions.checkStringNotEmpty(
483                         shortLabel, "shortLabel cannot be empty").toString());
484             }
485             return this;
486         }
487 
488         /**
489          * @hide
490          */
491         @NonNull
setLongLabel(@ullable final CharSequence longLabel)492         public Builder setLongLabel(@Nullable final CharSequence longLabel) {
493             if (!TextUtils.isEmpty(longLabel)) {
494                 setPropertyString(KEY_LONG_LABEL, Preconditions.checkStringNotEmpty(
495                         longLabel, "longLabel cannot be empty").toString());
496             }
497             return this;
498         }
499 
500         /**
501          * @hide
502          */
503         @NonNull
setDisabledMessage(@ullable final CharSequence disabledMessage)504         public Builder setDisabledMessage(@Nullable final CharSequence disabledMessage) {
505             if (!TextUtils.isEmpty(disabledMessage)) {
506                 setPropertyString(KEY_DISABLED_MESSAGE, Preconditions.checkStringNotEmpty(
507                         disabledMessage, "disabledMessage cannot be empty").toString());
508             }
509             return this;
510         }
511 
512         /**
513          * @hide
514          */
515         @NonNull
setCategories(@ullable final Set<String> categories)516         public Builder setCategories(@Nullable final Set<String> categories) {
517             if (categories != null && !categories.isEmpty()) {
518                 setPropertyString(KEY_CATEGORIES, categories.stream().toArray(String[]::new));
519             }
520             return this;
521         }
522 
523         /**
524          * @hide
525          */
526         @NonNull
setIntent(@ullable final Intent intent)527         public Builder setIntent(@Nullable final Intent intent) {
528             if (intent == null) {
529                 return this;
530             }
531             return setIntents(new Intent[]{intent});
532         }
533 
534         /**
535          * @hide
536          */
537         @NonNull
setIntents(@ullable final Intent[] intents)538         public Builder setIntents(@Nullable final Intent[] intents) {
539             if (intents == null || intents.length == 0) {
540                 return this;
541             }
542             for (Intent intent : intents) {
543                 Objects.requireNonNull(intent, "intents cannot contain null");
544                 Objects.requireNonNull(intent.getAction(), "intent's action must be set");
545             }
546             final byte[][] intentExtrases = new byte[intents.length][];
547             for (int i = 0; i < intents.length; i++) {
548                 final Intent intent = intents[i];
549                 final Bundle extras = intent.getExtras();
550                 intentExtrases[i] = extras == null
551                         ? new byte[0] : transformToByteArray(new PersistableBundle(extras));
552             }
553             setPropertyString(KEY_INTENTS, Arrays.stream(intents).map(it -> it.toUri(0))
554                     .toArray(String[]::new));
555             setPropertyBytes(KEY_INTENT_PERSISTABLE_EXTRAS, intentExtrases);
556             return this;
557         }
558 
559         /**
560          * @hide
561          */
562         @NonNull
setPerson(@ullable final Person person)563         public Builder setPerson(@Nullable final Person person) {
564             if (person == null) {
565                 return this;
566             }
567             return setPersons(new Person[]{person});
568         }
569 
570         /**
571          * @hide
572          */
573         @NonNull
setPersons(@ullable final Person[] persons)574         public Builder setPersons(@Nullable final Person[] persons) {
575             if (persons == null || persons.length == 0) {
576                 return this;
577             }
578             final GenericDocument[] documents = new GenericDocument[persons.length];
579             for (int i = 0; i < persons.length; i++) {
580                 final Person person = persons[i];
581                 if (person == null) continue;
582                 final AppSearchShortcutPerson personEntity =
583                         AppSearchShortcutPerson.instance(person);
584                 documents[i] = personEntity;
585             }
586             setPropertyDocument(KEY_PERSON, documents);
587             return this;
588         }
589 
590         /**
591          * @hide
592          */
593         @NonNull
setExtras(@ullable final PersistableBundle extras)594         public Builder setExtras(@Nullable final PersistableBundle extras) {
595             if (extras != null) {
596                 setPropertyBytes(KEY_EXTRAS, transformToByteArray(extras));
597             }
598             return this;
599         }
600 
601         /**
602          * @hide
603          */
setFlags(@hortcutInfo.ShortcutFlags final int flags)604         public Builder setFlags(@ShortcutInfo.ShortcutFlags final int flags) {
605             final String[] flagArray = flattenFlags(flags);
606             if (flagArray != null && flagArray.length > 0) {
607                 mFlags.addAll(Arrays.asList(flagArray));
608             }
609             return this;
610         }
611 
612         /**
613          * @hide
614          */
615         @NonNull
setIconResId(@ullable final int iconResId)616         public Builder setIconResId(@Nullable final int iconResId) {
617             setPropertyLong(KEY_ICON_RES_ID, iconResId);
618             return this;
619         }
620 
621         /**
622          * @hide
623          */
setIconResName(@ullable final String iconResName)624         public Builder setIconResName(@Nullable final String iconResName) {
625             if (!TextUtils.isEmpty(iconResName)) {
626                 setPropertyString(KEY_ICON_RES_NAME, iconResName);
627             }
628             return this;
629         }
630 
631         /**
632          * @hide
633          */
setIconUri(@ullable final String iconUri)634         public Builder setIconUri(@Nullable final String iconUri) {
635             if (!TextUtils.isEmpty(iconUri)) {
636                 setPropertyString(KEY_ICON_URI, iconUri);
637             }
638             return this;
639         }
640 
641         /**
642          * @hide
643          */
setDisabledReason(@hortcutInfo.DisabledReason final int disabledReason)644         public Builder setDisabledReason(@ShortcutInfo.DisabledReason final int disabledReason) {
645             setPropertyString(KEY_DISABLED_REASON, String.valueOf(disabledReason));
646             return this;
647         }
648 
649         /**
650          * @hide
651          */
setCapabilityBindings( @ullable final Map<String, Map<String, List<String>>> bindings)652         public Builder setCapabilityBindings(
653                 @Nullable final Map<String, Map<String, List<String>>> bindings) {
654             if (bindings != null && !bindings.isEmpty()) {
655                 final Set<String> capabilityNames = bindings.keySet();
656                 final Set<String> capabilityBindings = new ArraySet<>(1);
657                 for (String capabilityName: capabilityNames) {
658                     final Map<String, List<String>> params =
659                             bindings.get(capabilityName);
660                     for (String paramName: params.keySet()) {
661                         params.get(paramName).stream()
662                                 .map(v -> capabilityName + "/" + paramName + "/" + v)
663                                 .forEach(capabilityBindings::add);
664                     }
665                 }
666                 setPropertyString(KEY_CAPABILITY, capabilityNames.toArray(new String[0]));
667                 setPropertyString(KEY_CAPABILITY_BINDINGS,
668                         capabilityBindings.toArray(new String[0]));
669             }
670             return this;
671         }
672 
673         /**
674          * @hide
675          */
676         @NonNull
677         @Override
build()678         public AppSearchShortcutInfo build() {
679             setPropertyString(KEY_FLAGS, mFlags.toArray(new String[0]));
680             return new AppSearchShortcutInfo(super.build());
681         }
682     }
683 
684     /**
685      * Convert PersistableBundle into byte[] for persistence.
686      */
687     @Nullable
transformToByteArray(@onNull final PersistableBundle extras)688     private static byte[] transformToByteArray(@NonNull final PersistableBundle extras) {
689         Objects.requireNonNull(extras);
690         try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
691             new PersistableBundle(extras).writeToStream(baos);
692             return baos.toByteArray();
693         } catch (IOException e) {
694             return null;
695         }
696     }
697 
698     /**
699      * Convert byte[] into Bundle.
700      */
701     @Nullable
transformToBundle(@ullable final byte[] extras)702     private Bundle transformToBundle(@Nullable final byte[] extras) {
703         if (extras == null) {
704             return null;
705         }
706         Objects.requireNonNull(extras);
707         try (ByteArrayInputStream bais = new ByteArrayInputStream(extras)) {
708             final Bundle ret = new Bundle();
709             ret.putAll(PersistableBundle.readFromStream(bais));
710             return ret;
711         } catch (IOException e) {
712             return null;
713         }
714     }
715 
716     /**
717      * Convert byte[] into PersistableBundle.
718      */
719     @Nullable
transformToPersistableBundle(@ullable final byte[] extras)720     private PersistableBundle transformToPersistableBundle(@Nullable final byte[] extras) {
721         if (extras == null) {
722             return null;
723         }
724         try (ByteArrayInputStream bais = new ByteArrayInputStream(extras)) {
725             return PersistableBundle.readFromStream(bais);
726         } catch (IOException e) {
727             return null;
728         }
729     }
730 
flattenFlags(@hortcutInfo.ShortcutFlags final int flags)731     private static String[] flattenFlags(@ShortcutInfo.ShortcutFlags final int flags) {
732         final List<String> flattenedFlags = new ArrayList<>();
733         for (int i = 0; i < 31; i++) {
734             final int mask = 1 << i;
735             final String value = flagToString(flags, mask);
736             if (value != null) {
737                 flattenedFlags.add(value);
738             }
739         }
740         return flattenedFlags.toArray(new String[0]);
741     }
742 
743     @Nullable
flagToString( @hortcutInfo.ShortcutFlags final int flags, final int mask)744     private static String flagToString(
745             @ShortcutInfo.ShortcutFlags final int flags, final int mask) {
746         switch (mask) {
747             case ShortcutInfo.FLAG_DYNAMIC:
748                 return (flags & mask) != 0 ? IS_DYNAMIC : NOT_DYNAMIC;
749             case ShortcutInfo.FLAG_MANIFEST:
750                 return (flags & mask) != 0 ? IS_MANIFEST : NOT_MANIFEST;
751             case ShortcutInfo.FLAG_DISABLED:
752                 return (flags & mask) != 0 ? IS_DISABLED : NOT_DISABLED;
753             case ShortcutInfo.FLAG_IMMUTABLE:
754                 return (flags & mask) != 0 ? IS_IMMUTABLE : NOT_IMMUTABLE;
755             default:
756                 return null;
757         }
758     }
759 
parseFlags(@ullable final String[] flags)760     private static int parseFlags(@Nullable final String[] flags) {
761         if (flags == null) {
762             return 0;
763         }
764         int ret = 0;
765         for (int i = 0; i < flags.length; i++) {
766             ret = ret | parseFlag(flags[i]);
767         }
768         return ret;
769     }
770 
parseFlag(final String value)771     private static int parseFlag(final String value) {
772         switch (value) {
773             case IS_DYNAMIC:
774                 return ShortcutInfo.FLAG_DYNAMIC;
775             case IS_MANIFEST:
776                 return ShortcutInfo.FLAG_MANIFEST;
777             case IS_DISABLED:
778                 return ShortcutInfo.FLAG_DISABLED;
779             case IS_IMMUTABLE:
780                 return ShortcutInfo.FLAG_IMMUTABLE;
781             default:
782                 return 0;
783         }
784     }
785 
786     @NonNull
parsePerson(@ullable final GenericDocument[] persons)787     private static Person[] parsePerson(@Nullable final GenericDocument[] persons) {
788         if (persons == null) return new Person[0];
789         final Person[] ret = new Person[persons.length];
790         for (int i = 0; i < persons.length; i++) {
791             final GenericDocument document = persons[i];
792             if (document == null) continue;
793             final AppSearchShortcutPerson person = new AppSearchShortcutPerson(document);
794             ret[i] = person.toPerson();
795         }
796         return ret;
797     }
798 
799     @Nullable
parseCapabilityBindings( @ullable final String[] capabilityBindings)800     private static Map<String, Map<String, List<String>>> parseCapabilityBindings(
801             @Nullable final String[] capabilityBindings) {
802         if (capabilityBindings == null || capabilityBindings.length == 0) {
803             return null;
804         }
805         final Map<String, Map<String, List<String>>> ret = new ArrayMap<>(1);
806         Arrays.stream(capabilityBindings).forEach(binding -> {
807             if (TextUtils.isEmpty(binding)) {
808                 return;
809             }
810             final int capabilityStopIndex = binding.indexOf("/");
811             if (capabilityStopIndex == -1 || capabilityStopIndex == binding.length() - 1) {
812                 return;
813             }
814             final String capabilityName = binding.substring(0, capabilityStopIndex);
815             final int paramStopIndex = binding.indexOf("/", capabilityStopIndex + 1);
816             if (paramStopIndex == -1 || paramStopIndex == binding.length() - 1) {
817                 return;
818             }
819             final String paramName = binding.substring(capabilityStopIndex + 1, paramStopIndex);
820             final String paramValue = binding.substring(paramStopIndex + 1);
821             if (!ret.containsKey(capabilityName)) {
822                 ret.put(capabilityName, new ArrayMap<>(1));
823             }
824             final Map<String, List<String>> params = ret.get(capabilityName);
825             if (!params.containsKey(paramName)) {
826                 params.put(paramName, new ArrayList<>(1));
827             }
828             params.get(paramName).add(paramValue);
829         });
830         return ret;
831     }
832 }
833