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