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