blob: eb50924579f61121ad9fdd79a80107ba238a55b4 [file] [log] [blame]
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.content.pm;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UserIdInt;
import android.app.Person;
import android.app.appsearch.AppSearchSchema;
import android.app.appsearch.GenericDocument;
import android.content.ComponentName;
import android.content.Intent;
import android.content.LocusId;
import android.graphics.drawable.Icon;
import android.os.Bundle;
import android.os.PersistableBundle;
import android.text.TextUtils;
import android.util.ArraySet;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.Preconditions;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.Set;
/**
* @hide
*/
public class AppSearchShortcutInfo extends GenericDocument {
/** The name of the schema type for {@link ShortcutInfo} documents.*/
public static final String SCHEMA_TYPE = "Shortcut";
public static final int SCHEMA_VERSION = 2;
public static final String KEY_ACTIVITY = "activity";
public static final String KEY_SHORT_LABEL = "shortLabel";
public static final String KEY_SHORT_LABEL_RES_ID = "shortLabelResId";
public static final String KEY_SHORT_LABEL_RES_NAME = "shortLabelResName";
public static final String KEY_LONG_LABEL = "longLabel";
public static final String KEY_LONG_LABEL_RES_ID = "longLabelResId";
public static final String KEY_LONG_LABEL_RES_NAME = "longLabelResName";
public static final String KEY_DISABLED_MESSAGE = "disabledMessage";
public static final String KEY_DISABLED_MESSAGE_RES_ID = "disabledMessageResId";
public static final String KEY_DISABLED_MESSAGE_RES_NAME = "disabledMessageResName";
public static final String KEY_CATEGORIES = "categories";
public static final String KEY_INTENTS = "intents";
public static final String KEY_INTENT_PERSISTABLE_EXTRAS = "intentPersistableExtras";
public static final String KEY_PERSON = "person";
public static final String KEY_LOCUS_ID = "locusId";
public static final String KEY_RANK = "rank";
public static final String KEY_IMPLICIT_RANK = "implicitRank";
public static final String KEY_EXTRAS = "extras";
public static final String KEY_FLAGS = "flags";
public static final String KEY_ICON_RES_ID = "iconResId";
public static final String KEY_ICON_RES_NAME = "iconResName";
public static final String KEY_ICON_URI = "iconUri";
public static final String KEY_BITMAP_PATH = "bitmapPath";
public static final String KEY_DISABLED_REASON = "disabledReason";
public static final AppSearchSchema SCHEMA = new AppSearchSchema.Builder(SCHEMA_TYPE)
.addProperty(new AppSearchSchema.StringPropertyConfig.Builder(KEY_ACTIVITY)
.setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
.setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
.setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
.build()
).addProperty(new AppSearchSchema.StringPropertyConfig.Builder(KEY_SHORT_LABEL)
.setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
.setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
.setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
.build()
).addProperty(new AppSearchSchema.Int64PropertyConfig.Builder(KEY_SHORT_LABEL_RES_ID)
.setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
.build()
).addProperty(new AppSearchSchema.StringPropertyConfig.Builder(KEY_SHORT_LABEL_RES_NAME)
.setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
.setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_NONE)
.setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_NONE)
.build()
).addProperty(new AppSearchSchema.StringPropertyConfig.Builder(KEY_LONG_LABEL)
.setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
.setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
.setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
.build()
).addProperty(new AppSearchSchema.Int64PropertyConfig.Builder(KEY_LONG_LABEL_RES_ID)
.setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
.build()
).addProperty(new AppSearchSchema.StringPropertyConfig.Builder(KEY_LONG_LABEL_RES_NAME)
.setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
.setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_NONE)
.setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_NONE)
.build()
).addProperty(new AppSearchSchema.StringPropertyConfig.Builder(KEY_DISABLED_MESSAGE)
.setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
.setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_NONE)
.setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_NONE)
.build()
).addProperty(new AppSearchSchema.Int64PropertyConfig.Builder(
KEY_DISABLED_MESSAGE_RES_ID)
.setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
.build()
).addProperty(new AppSearchSchema.StringPropertyConfig.Builder(
KEY_DISABLED_MESSAGE_RES_NAME)
.setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
.setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_NONE)
.setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_NONE)
.build()
).addProperty(new AppSearchSchema.StringPropertyConfig.Builder(KEY_CATEGORIES)
.setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
.setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
.setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
.build()
).addProperty(new AppSearchSchema.StringPropertyConfig.Builder(KEY_INTENTS)
.setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
.setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_NONE)
.setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_NONE)
.build()
).addProperty(new AppSearchSchema.BytesPropertyConfig.Builder(
KEY_INTENT_PERSISTABLE_EXTRAS)
.setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
.build()
).addProperty(new AppSearchSchema.DocumentPropertyConfig.Builder(KEY_PERSON)
.setSchemaType(AppSearchPerson.SCHEMA_TYPE)
.setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
.build()
).addProperty(new AppSearchSchema.StringPropertyConfig.Builder(KEY_LOCUS_ID)
.setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
.setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
.setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
.build()
).addProperty(new AppSearchSchema.StringPropertyConfig.Builder(KEY_RANK)
.setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
.setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
.setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
.build()
).addProperty(new AppSearchSchema.Int64PropertyConfig.Builder(KEY_IMPLICIT_RANK)
.setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
.build()
).addProperty(new AppSearchSchema.BytesPropertyConfig.Builder(KEY_EXTRAS)
.setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
.build()
).addProperty(new AppSearchSchema.StringPropertyConfig.Builder(KEY_FLAGS)
.setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
.setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
.setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
.build()
).addProperty(new AppSearchSchema.Int64PropertyConfig.Builder(KEY_ICON_RES_ID)
.setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
.build()
).addProperty(new AppSearchSchema.StringPropertyConfig.Builder(KEY_ICON_RES_NAME)
.setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
.setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_NONE)
.setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_NONE)
.build()
).addProperty(new AppSearchSchema.StringPropertyConfig.Builder(KEY_ICON_URI)
.setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
.setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_NONE)
.setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_NONE)
.build()
).addProperty(new AppSearchSchema.StringPropertyConfig.Builder(KEY_BITMAP_PATH)
.setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
.setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_NONE)
.setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_NONE)
.build()
).addProperty(new AppSearchSchema.StringPropertyConfig.Builder(KEY_DISABLED_REASON)
.setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED)
.setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
.setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
.build()
).build();
/**
* The string representation of every flag within {@link ShortcutInfo}. Note that its value
* needs to be camelCase since AppSearch's tokenizer will break the word when it sees
* underscore.
*/
private static final String IS_DYNAMIC = "Dyn";
private static final String NOT_DYNAMIC = "nDyn";
private static final String IS_PINNED = "Pin";
private static final String NOT_PINNED = "nPin";
private static final String HAS_ICON_RES = "IcR";
private static final String NO_ICON_RES = "nIcR";
private static final String HAS_ICON_FILE = "IcF";
private static final String NO_ICON_FILE = "nIcF";
private static final String IS_KEY_FIELD_ONLY = "Key";
private static final String NOT_KEY_FIELD_ONLY = "nKey";
private static final String IS_MANIFEST = "Man";
private static final String NOT_MANIFEST = "nMan";
private static final String IS_DISABLED = "Dis";
private static final String NOT_DISABLED = "nDis";
private static final String ARE_STRINGS_RESOLVED = "Str";
private static final String NOT_STRINGS_RESOLVED = "nStr";
private static final String IS_IMMUTABLE = "Im";
private static final String NOT_IMMUTABLE = "nIm";
private static final String HAS_ADAPTIVE_BITMAP = "IcA";
private static final String NO_ADAPTIVE_BITMAP = "nIcA";
private static final String IS_RETURNED_BY_SERVICE = "Rets";
private static final String NOT_RETURNED_BY_SERVICE = "nRets";
private static final String HAS_ICON_FILE_PENDING_SAVE = "Pens";
private static final String NO_ICON_FILE_PENDING_SAVE = "nPens";
private static final String IS_SHADOW = "Sdw";
private static final String NOT_SHADOW = "nSdw";
private static final String IS_LONG_LIVED = "Liv";
private static final String NOT_LONG_LIVED = "nLiv";
private static final String HAS_ICON_URI = "IcU";
private static final String NO_ICON_URI = "nIcU";
private static final String IS_CACHED_NOTIFICATION = "CaN";
private static final String NOT_CACHED_NOTIFICATION = "nCaN";
private static final String IS_CACHED_BUBBLE = "CaB";
private static final String NOT_CACHED_BUBBLE = "nCaB";
private static final String IS_CACHED_PEOPLE_TITLE = "CaPT";
private static final String NOT_CACHED_PEOPLE_TITLE = "nCaPT";
/**
* Following flags are not store within ShortcutInfo, but book-keeping states to reduce search
* space when performing queries against AppSearch.
*/
private static final String HAS_BITMAP_PATH = "hBiP";
private static final String HAS_STRING_RESOURCE = "hStr";
private static final String HAS_NON_ZERO_RANK = "hRan";
public static final String QUERY_IS_DYNAMIC = KEY_FLAGS + ":" + IS_DYNAMIC;
public static final String QUERY_IS_NOT_DYNAMIC = KEY_FLAGS + ":" + NOT_DYNAMIC;
public static final String QUERY_IS_PINNED = KEY_FLAGS + ":" + IS_PINNED;
public static final String QUERY_IS_NOT_PINNED = KEY_FLAGS + ":" + NOT_PINNED;
public static final String QUERY_IS_MANIFEST = KEY_FLAGS + ":" + IS_MANIFEST;
public static final String QUERY_IS_NOT_MANIFEST = KEY_FLAGS + ":" + NOT_MANIFEST;
public static final String QUERY_IS_PINNED_AND_ENABLED =
"(" + KEY_FLAGS + ":" + IS_PINNED + " " + KEY_FLAGS + ":" + NOT_DISABLED + ")";
public static final String QUERY_IS_CACHED =
"(" + KEY_FLAGS + ":" + IS_CACHED_NOTIFICATION + " OR "
+ KEY_FLAGS + ":" + IS_CACHED_BUBBLE + " OR "
+ KEY_FLAGS + ":" + IS_CACHED_PEOPLE_TITLE + ")";
public static final String QUERY_IS_NOT_CACHED =
"(" + KEY_FLAGS + ":" + NOT_CACHED_NOTIFICATION + " "
+ KEY_FLAGS + ":" + NOT_CACHED_BUBBLE + " "
+ KEY_FLAGS + ":" + NOT_CACHED_PEOPLE_TITLE + ")";
public static final String QUERY_IS_FLOATING =
"((" + IS_PINNED + " OR " + QUERY_IS_CACHED + ") "
+ QUERY_IS_NOT_DYNAMIC + " " + QUERY_IS_NOT_MANIFEST + ")";
public static final String QUERY_IS_NOT_FLOATING =
"((" + QUERY_IS_NOT_PINNED + " " + QUERY_IS_NOT_CACHED + ") OR "
+ QUERY_IS_DYNAMIC + " OR " + QUERY_IS_MANIFEST + ")";
public static final String QUERY_IS_VISIBLE_TO_PUBLISHER =
"(" + KEY_DISABLED_REASON + ":" + ShortcutInfo.DISABLED_REASON_NOT_DISABLED
+ " OR " + KEY_DISABLED_REASON + ":"
+ ShortcutInfo.DISABLED_REASON_BY_APP
+ " OR " + KEY_DISABLED_REASON + ":"
+ ShortcutInfo.DISABLED_REASON_APP_CHANGED
+ " OR " + KEY_DISABLED_REASON + ":"
+ ShortcutInfo.DISABLED_REASON_UNKNOWN + ")";
public static final String QUERY_DISABLED_REASON_VERSION_LOWER =
KEY_DISABLED_REASON + ":" + ShortcutInfo.DISABLED_REASON_VERSION_LOWER;
public static final String QUERY_IS_NON_MANIFEST_VISIBLE =
"(" + QUERY_IS_NOT_MANIFEST + " " + QUERY_IS_VISIBLE_TO_PUBLISHER + " ("
+ QUERY_IS_PINNED + " OR " + QUERY_IS_CACHED + " OR " + QUERY_IS_DYNAMIC + "))";
public static final String QUERY_IS_VISIBLE_CACHED_OR_PINNED =
"(" + QUERY_IS_VISIBLE_TO_PUBLISHER + " " + QUERY_IS_DYNAMIC
+ " (" + QUERY_IS_CACHED + " OR " + QUERY_IS_PINNED + "))";
public static final String QUERY_IS_VISIBLE_PINNED_ONLY =
"(" + QUERY_IS_VISIBLE_TO_PUBLISHER + " " + QUERY_IS_PINNED + " " + QUERY_IS_NOT_CACHED
+ " " + QUERY_IS_NOT_DYNAMIC + " " + QUERY_IS_NOT_MANIFEST + ")";
public static final String QUERY_HAS_BITMAP_PATH = KEY_FLAGS + ":" + HAS_BITMAP_PATH;
public static final String QUERY_HAS_STRING_RESOURCE = KEY_FLAGS + ":" + HAS_STRING_RESOURCE;
public static final String QUERY_HAS_NON_ZERO_RANK = KEY_FLAGS + ":" + HAS_NON_ZERO_RANK;
public static final String QUERY_IS_FLOATING_AND_HAS_RANK =
"(" + QUERY_IS_FLOATING + " " + QUERY_HAS_NON_ZERO_RANK + ")";
public AppSearchShortcutInfo(@NonNull GenericDocument document) {
super(document);
}
/**
* @hide
*/
@NonNull
public static AppSearchShortcutInfo instance(@NonNull final ShortcutInfo shortcutInfo) {
Objects.requireNonNull(shortcutInfo);
return new Builder(shortcutInfo.getPackage(), shortcutInfo.getId())
.setActivity(shortcutInfo.getActivity())
.setShortLabel(shortcutInfo.getShortLabel())
.setShortLabelResId(shortcutInfo.getShortLabelResourceId())
.setShortLabelResName(shortcutInfo.getTitleResName())
.setLongLabel(shortcutInfo.getLongLabel())
.setLongLabelResId(shortcutInfo.getLongLabelResourceId())
.setLongLabelResName(shortcutInfo.getTextResName())
.setDisabledMessage(shortcutInfo.getDisabledMessage())
.setDisabledMessageResId(shortcutInfo.getDisabledMessageResourceId())
.setDisabledMessageResName(shortcutInfo.getDisabledMessageResName())
.setCategories(shortcutInfo.getCategories())
.setIntents(shortcutInfo.getIntents())
.setRank(shortcutInfo.getRank())
.setImplicitRank(shortcutInfo.getImplicitRank()
| (shortcutInfo.isRankChanged() ? ShortcutInfo.RANK_CHANGED_BIT : 0))
.setExtras(shortcutInfo.getExtras())
.setCreationTimestampMillis(shortcutInfo.getLastChangedTimestamp())
.setFlags(shortcutInfo.getFlags())
.setIconResId(shortcutInfo.getIconResourceId())
.setIconResName(shortcutInfo.getIconResName())
.setBitmapPath(shortcutInfo.getBitmapPath())
.setIconUri(shortcutInfo.getIconUri())
.setDisabledReason(shortcutInfo.getDisabledReason())
.setPersons(shortcutInfo.getPersons())
.setLocusId(shortcutInfo.getLocusId())
.build();
}
/**
* @hide
*/
@NonNull
public ShortcutInfo toShortcutInfo(@UserIdInt int userId) {
final String packageName = getNamespace();
final String activityString = getPropertyString(KEY_ACTIVITY);
final ComponentName activity = activityString == null
? null : ComponentName.unflattenFromString(activityString);
// TODO: proper icon handling
// NOTE: bitmap based icons are currently saved in side-channel (see ShortcutBitmapSaver),
// re-creating Icon object at creation time implies turning this function into async since
// loading bitmap is I/O bound. Since ShortcutInfo#getIcon is already annotated with
// @hide and @UnsupportedAppUsage, we could migrate existing usage in platform with
// LauncherApps#getShortcutIconDrawable instead.
final Icon icon = null;
final String shortLabel = getPropertyString(KEY_SHORT_LABEL);
final int shortLabelResId = (int) getPropertyLong(KEY_SHORT_LABEL_RES_ID);
final String shortLabelResName = getPropertyString(KEY_SHORT_LABEL_RES_NAME);
final String longLabel = getPropertyString(KEY_LONG_LABEL);
final int longLabelResId = (int) getPropertyLong(KEY_LONG_LABEL_RES_ID);
final String longLabelResName = getPropertyString(KEY_LONG_LABEL_RES_NAME);
final String disabledMessage = getPropertyString(KEY_DISABLED_MESSAGE);
final int disabledMessageResId = (int) getPropertyLong(KEY_DISABLED_MESSAGE_RES_ID);
final String disabledMessageResName = getPropertyString(KEY_DISABLED_MESSAGE_RES_NAME);
final String[] categories = getPropertyStringArray(KEY_CATEGORIES);
final Set<String> categoriesSet = categories == null
? null : new ArraySet<>(Arrays.asList(categories));
final String[] intentsStrings = getPropertyStringArray(KEY_INTENTS);
final Intent[] intents = intentsStrings == null
? new Intent[0] : Arrays.stream(intentsStrings).map(uri -> {
if (TextUtils.isEmpty(uri)) {
return new Intent(Intent.ACTION_VIEW);
}
try {
return Intent.parseUri(uri, /* flags =*/ 0);
} catch (URISyntaxException e) {
// ignore malformed entry
}
return null;
}).toArray(Intent[]::new);
final byte[][] intentExtrasesBytes = getPropertyBytesArray(KEY_INTENT_PERSISTABLE_EXTRAS);
final Bundle[] intentExtrases = intentExtrasesBytes == null
? null : Arrays.stream(intentExtrasesBytes)
.map(this::transformToBundle).toArray(Bundle[]::new);
if (intents != null) {
for (int i = 0; i < intents.length; i++) {
final Intent intent = intents[i];
if (intent == null || intentExtrases == null || intentExtrases.length <= i
|| intentExtrases[i] == null || intentExtrases[i].size() == 0) {
continue;
}
intent.replaceExtras(intentExtrases[i]);
}
}
final Person[] persons = parsePerson(getPropertyDocumentArray(KEY_PERSON));
final String locusIdString = getPropertyString(KEY_LOCUS_ID);
final LocusId locusId = locusIdString == null ? null : new LocusId(locusIdString);
final int rank = Integer.parseInt(getPropertyString(KEY_RANK));
final int implicitRank = (int) getPropertyLong(KEY_IMPLICIT_RANK);
final byte[] extrasByte = getPropertyBytes(KEY_EXTRAS);
final PersistableBundle extras = transformToPersistableBundle(extrasByte);
final int flags = parseFlags(getPropertyStringArray(KEY_FLAGS));
final int iconResId = (int) getPropertyLong(KEY_ICON_RES_ID);
final String iconResName = getPropertyString(KEY_ICON_RES_NAME);
final String iconUri = getPropertyString(KEY_ICON_URI);
final String bitmapPath = getPropertyString(KEY_BITMAP_PATH);
final int disabledReason = Integer.parseInt(getPropertyString(KEY_DISABLED_REASON));
final ShortcutInfo si = new ShortcutInfo(
userId, getUri(), packageName, activity, icon, shortLabel, shortLabelResId,
shortLabelResName, longLabel, longLabelResId, longLabelResName, disabledMessage,
disabledMessageResId, disabledMessageResName, categoriesSet, intents, rank, extras,
getCreationTimestampMillis(), flags, iconResId, iconResName, bitmapPath, iconUri,
disabledReason, persons, locusId, 0);
si.setImplicitRank(implicitRank);
if ((implicitRank & ShortcutInfo.RANK_CHANGED_BIT) != 0) {
si.setRankChanged();
}
return si;
}
/**
* @hide
*/
@NonNull
public static List<GenericDocument> toGenericDocuments(
@NonNull final Collection<ShortcutInfo> shortcuts) {
final List<GenericDocument> docs = new ArrayList<>(shortcuts.size());
for (ShortcutInfo si : shortcuts) {
docs.add(AppSearchShortcutInfo.instance(si));
}
return docs;
}
/** @hide */
@VisibleForTesting
public static class Builder extends GenericDocument.Builder<Builder> {
private List<String> mFlags = new ArrayList<>(1);
private boolean mHasStringResource = false;
public Builder(String packageName, String id) {
super(/*namespace=*/ packageName, id, SCHEMA_TYPE);
}
/**
* @hide
*/
@NonNull
public Builder setLocusId(@Nullable final LocusId locusId) {
if (locusId != null) {
setPropertyString(KEY_LOCUS_ID, locusId.getId());
}
return this;
}
/**
* @hide
*/
@NonNull
public Builder setActivity(@Nullable final ComponentName activity) {
if (activity != null) {
setPropertyString(KEY_ACTIVITY, activity.flattenToShortString());
}
return this;
}
/**
* @hide
*/
@NonNull
public Builder setShortLabel(@Nullable final CharSequence shortLabel) {
if (!TextUtils.isEmpty(shortLabel)) {
setPropertyString(KEY_SHORT_LABEL, Preconditions.checkStringNotEmpty(
shortLabel, "shortLabel cannot be empty").toString());
}
return this;
}
/**
* @hide
*/
@NonNull
public Builder setShortLabelResId(final int shortLabelResId) {
setPropertyLong(KEY_SHORT_LABEL_RES_ID, shortLabelResId);
if (shortLabelResId != 0) {
mHasStringResource = true;
}
return this;
}
/**
* @hide
*/
public Builder setShortLabelResName(@Nullable final String shortLabelResName) {
if (!TextUtils.isEmpty(shortLabelResName)) {
setPropertyString(KEY_SHORT_LABEL_RES_NAME, shortLabelResName);
}
return this;
}
/**
* @hide
*/
@NonNull
public Builder setLongLabel(@Nullable final CharSequence longLabel) {
if (!TextUtils.isEmpty(longLabel)) {
setPropertyString(KEY_LONG_LABEL, Preconditions.checkStringNotEmpty(
longLabel, "longLabel cannot be empty").toString());
}
return this;
}
/**
* @hide
*/
@NonNull
public Builder setLongLabelResId(final int longLabelResId) {
setPropertyLong(KEY_LONG_LABEL_RES_ID, longLabelResId);
if (longLabelResId != 0) {
mHasStringResource = true;
}
return this;
}
/**
* @hide
*/
public Builder setLongLabelResName(@Nullable final String longLabelResName) {
if (!TextUtils.isEmpty(longLabelResName)) {
setPropertyString(KEY_LONG_LABEL_RES_NAME, longLabelResName);
}
return this;
}
/**
* @hide
*/
@NonNull
public Builder setDisabledMessage(@Nullable final CharSequence disabledMessage) {
if (!TextUtils.isEmpty(disabledMessage)) {
setPropertyString(KEY_DISABLED_MESSAGE, Preconditions.checkStringNotEmpty(
disabledMessage, "disabledMessage cannot be empty").toString());
}
return this;
}
/**
* @hide
*/
@NonNull
public Builder setDisabledMessageResId(final int disabledMessageResId) {
setPropertyLong(KEY_DISABLED_MESSAGE_RES_ID, disabledMessageResId);
if (disabledMessageResId != 0) {
mHasStringResource = true;
}
return this;
}
/**
* @hide
*/
public Builder setDisabledMessageResName(@Nullable final String disabledMessageResName) {
if (!TextUtils.isEmpty(disabledMessageResName)) {
setPropertyString(KEY_DISABLED_MESSAGE_RES_NAME, disabledMessageResName);
}
return this;
}
/**
* @hide
*/
@NonNull
public Builder setCategories(@Nullable final Set<String> categories) {
if (categories != null && !categories.isEmpty()) {
setPropertyString(KEY_CATEGORIES, categories.stream().toArray(String[]::new));
}
return this;
}
/**
* @hide
*/
@NonNull
public Builder setIntent(@Nullable final Intent intent) {
if (intent == null) {
return this;
}
return setIntents(new Intent[]{intent});
}
/**
* @hide
*/
@NonNull
public Builder setIntents(@Nullable final Intent[] intents) {
if (intents == null || intents.length == 0) {
return this;
}
for (Intent intent : intents) {
Objects.requireNonNull(intent, "intents cannot contain null");
Objects.requireNonNull(intent.getAction(), "intent's action must be set");
}
final byte[][] intentExtrases = new byte[intents.length][];
for (int i = 0; i < intents.length; i++) {
final Intent intent = intents[i];
final Bundle extras = intent.getExtras();
intentExtrases[i] = extras == null
? new byte[0] : transformToByteArray(new PersistableBundle(extras));
}
setPropertyString(KEY_INTENTS, Arrays.stream(intents).map(it -> it.toUri(0))
.toArray(String[]::new));
setPropertyBytes(KEY_INTENT_PERSISTABLE_EXTRAS, intentExtrases);
return this;
}
/**
* @hide
*/
@NonNull
public Builder setPerson(@Nullable final Person person) {
if (person == null) {
return this;
}
return setPersons(new Person[]{person});
}
/**
* @hide
*/
@NonNull
public Builder setPersons(@Nullable final Person[] persons) {
if (persons == null || persons.length == 0) {
return this;
}
final GenericDocument[] documents = new GenericDocument[persons.length];
for (int i = 0; i < persons.length; i++) {
final Person person = persons[i];
if (person == null) continue;
final AppSearchPerson appSearchPerson = AppSearchPerson.instance(person);
documents[i] = appSearchPerson;
}
setPropertyDocument(KEY_PERSON, documents);
return this;
}
/**
* @hide
*/
@NonNull
public Builder setRank(final int rank) {
Preconditions.checkArgument((0 <= rank), "Rank cannot be negative");
setPropertyString(KEY_RANK, String.valueOf(rank));
if (rank != 0) {
mFlags.add(HAS_NON_ZERO_RANK);
}
return this;
}
/**
* @hide
*/
@NonNull
public Builder setImplicitRank(final int rank) {
setPropertyLong(KEY_IMPLICIT_RANK, rank);
return this;
}
/**
* @hide
*/
@NonNull
public Builder setExtras(@Nullable final PersistableBundle extras) {
if (extras != null) {
setPropertyBytes(KEY_EXTRAS, transformToByteArray(extras));
}
return this;
}
/**
* @hide
*/
public Builder setFlags(@ShortcutInfo.ShortcutFlags final int flags) {
final String[] flagArray = flattenFlags(flags);
if (flagArray != null && flagArray.length > 0) {
mFlags.addAll(Arrays.asList(flagArray));
}
return this;
}
/**
* @hide
*/
@NonNull
public Builder setIconResId(@Nullable final int iconResId) {
setPropertyLong(KEY_ICON_RES_ID, iconResId);
return this;
}
/**
* @hide
*/
public Builder setIconResName(@Nullable final String iconResName) {
if (!TextUtils.isEmpty(iconResName)) {
setPropertyString(KEY_ICON_RES_NAME, iconResName);
}
return this;
}
/**
* @hide
*/
public Builder setBitmapPath(@Nullable final String bitmapPath) {
if (!TextUtils.isEmpty(bitmapPath)) {
setPropertyString(KEY_BITMAP_PATH, bitmapPath);
mFlags.add(HAS_BITMAP_PATH);
}
return this;
}
/**
* @hide
*/
public Builder setIconUri(@Nullable final String iconUri) {
if (!TextUtils.isEmpty(iconUri)) {
setPropertyString(KEY_ICON_URI, iconUri);
}
return this;
}
/**
* @hide
*/
public Builder setDisabledReason(@ShortcutInfo.DisabledReason final int disabledReason) {
setPropertyString(KEY_DISABLED_REASON, String.valueOf(disabledReason));
return this;
}
/**
* @hide
*/
@NonNull
@Override
public AppSearchShortcutInfo build() {
if (mHasStringResource) {
mFlags.add(HAS_STRING_RESOURCE);
}
setPropertyString(KEY_FLAGS, mFlags.toArray(new String[0]));
return new AppSearchShortcutInfo(super.build());
}
}
/**
* Convert PersistableBundle into byte[] for persistence.
*/
@Nullable
private static byte[] transformToByteArray(@NonNull final PersistableBundle extras) {
Objects.requireNonNull(extras);
try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
new PersistableBundle(extras).writeToStream(baos);
return baos.toByteArray();
} catch (IOException e) {
return null;
}
}
/**
* Convert byte[] into Bundle.
*/
@Nullable
private Bundle transformToBundle(@Nullable final byte[] extras) {
if (extras == null) {
return null;
}
Objects.requireNonNull(extras);
try (ByteArrayInputStream bais = new ByteArrayInputStream(extras)) {
final Bundle ret = new Bundle();
ret.putAll(PersistableBundle.readFromStream(bais));
return ret;
} catch (IOException e) {
return null;
}
}
/**
* Convert byte[] into PersistableBundle.
*/
@Nullable
private PersistableBundle transformToPersistableBundle(@Nullable final byte[] extras) {
if (extras == null) {
return null;
}
try (ByteArrayInputStream bais = new ByteArrayInputStream(extras)) {
return PersistableBundle.readFromStream(bais);
} catch (IOException e) {
return null;
}
}
private static String[] flattenFlags(@ShortcutInfo.ShortcutFlags final int flags) {
final List<String> flattenedFlags = new ArrayList<>();
for (int i = 0; i < 31; i++) {
final int mask = 1 << i;
final String value = flagToString(flags, mask);
if (value != null) {
flattenedFlags.add(value);
}
}
return flattenedFlags.toArray(new String[0]);
}
@Nullable
private static String flagToString(
@ShortcutInfo.ShortcutFlags final int flags, final int mask) {
switch (mask) {
case ShortcutInfo.FLAG_DYNAMIC:
return (flags & mask) != 0 ? IS_DYNAMIC : NOT_DYNAMIC;
case ShortcutInfo.FLAG_PINNED:
return (flags & mask) != 0 ? IS_PINNED : NOT_PINNED;
case ShortcutInfo.FLAG_HAS_ICON_RES:
return (flags & mask) != 0 ? HAS_ICON_RES : NO_ICON_RES;
case ShortcutInfo.FLAG_HAS_ICON_FILE:
return (flags & mask) != 0 ? HAS_ICON_FILE : NO_ICON_FILE;
case ShortcutInfo.FLAG_KEY_FIELDS_ONLY:
return (flags & mask) != 0 ? IS_KEY_FIELD_ONLY : NOT_KEY_FIELD_ONLY;
case ShortcutInfo.FLAG_MANIFEST:
return (flags & mask) != 0 ? IS_MANIFEST : NOT_MANIFEST;
case ShortcutInfo.FLAG_DISABLED:
return (flags & mask) != 0 ? IS_DISABLED : NOT_DISABLED;
case ShortcutInfo.FLAG_STRINGS_RESOLVED:
return (flags & mask) != 0 ? ARE_STRINGS_RESOLVED : NOT_STRINGS_RESOLVED;
case ShortcutInfo.FLAG_IMMUTABLE:
return (flags & mask) != 0 ? IS_IMMUTABLE : NOT_IMMUTABLE;
case ShortcutInfo.FLAG_ADAPTIVE_BITMAP:
return (flags & mask) != 0 ? HAS_ADAPTIVE_BITMAP : NO_ADAPTIVE_BITMAP;
case ShortcutInfo.FLAG_RETURNED_BY_SERVICE:
return (flags & mask) != 0 ? IS_RETURNED_BY_SERVICE : NOT_RETURNED_BY_SERVICE;
case ShortcutInfo.FLAG_ICON_FILE_PENDING_SAVE:
return (flags & mask) != 0 ? HAS_ICON_FILE_PENDING_SAVE : NO_ICON_FILE_PENDING_SAVE;
case ShortcutInfo.FLAG_SHADOW:
return (flags & mask) != 0 ? IS_SHADOW : NOT_SHADOW;
case ShortcutInfo.FLAG_LONG_LIVED:
return (flags & mask) != 0 ? IS_LONG_LIVED : NOT_LONG_LIVED;
case ShortcutInfo.FLAG_HAS_ICON_URI:
return (flags & mask) != 0 ? HAS_ICON_URI : NO_ICON_URI;
case ShortcutInfo.FLAG_CACHED_NOTIFICATIONS:
return (flags & mask) != 0 ? IS_CACHED_NOTIFICATION : NOT_CACHED_NOTIFICATION;
case ShortcutInfo.FLAG_CACHED_BUBBLES:
return (flags & mask) != 0 ? IS_CACHED_BUBBLE : NOT_CACHED_BUBBLE;
case ShortcutInfo.FLAG_CACHED_PEOPLE_TILE:
return (flags & mask) != 0 ? IS_CACHED_PEOPLE_TITLE : NOT_CACHED_PEOPLE_TITLE;
default:
return null;
}
}
private static int parseFlags(@Nullable final String[] flags) {
if (flags == null) {
return 0;
}
int ret = 0;
for (int i = 0; i < flags.length; i++) {
ret = ret | parseFlag(flags[i]);
}
return ret;
}
private static int parseFlag(final String value) {
switch (value) {
case IS_DYNAMIC:
return ShortcutInfo.FLAG_DYNAMIC;
case IS_PINNED:
return ShortcutInfo.FLAG_PINNED;
case HAS_ICON_RES:
return ShortcutInfo.FLAG_HAS_ICON_RES;
case HAS_ICON_FILE:
return ShortcutInfo.FLAG_HAS_ICON_FILE;
case IS_KEY_FIELD_ONLY:
return ShortcutInfo.FLAG_KEY_FIELDS_ONLY;
case IS_MANIFEST:
return ShortcutInfo.FLAG_MANIFEST;
case IS_DISABLED:
return ShortcutInfo.FLAG_DISABLED;
case ARE_STRINGS_RESOLVED:
return ShortcutInfo.FLAG_STRINGS_RESOLVED;
case IS_IMMUTABLE:
return ShortcutInfo.FLAG_IMMUTABLE;
case HAS_ADAPTIVE_BITMAP:
return ShortcutInfo.FLAG_ADAPTIVE_BITMAP;
case IS_RETURNED_BY_SERVICE:
return ShortcutInfo.FLAG_RETURNED_BY_SERVICE;
case HAS_ICON_FILE_PENDING_SAVE:
return ShortcutInfo.FLAG_ICON_FILE_PENDING_SAVE;
case IS_SHADOW:
return ShortcutInfo.FLAG_SHADOW;
case IS_LONG_LIVED:
return ShortcutInfo.FLAG_LONG_LIVED;
case HAS_ICON_URI:
return ShortcutInfo.FLAG_HAS_ICON_URI;
case IS_CACHED_NOTIFICATION:
return ShortcutInfo.FLAG_CACHED_NOTIFICATIONS;
case IS_CACHED_BUBBLE:
return ShortcutInfo.FLAG_CACHED_BUBBLES;
case IS_CACHED_PEOPLE_TITLE:
return ShortcutInfo.FLAG_CACHED_PEOPLE_TILE;
default:
return 0;
}
}
@NonNull
private static Person[] parsePerson(@Nullable final GenericDocument[] persons) {
if (persons == null) return new Person[0];
final Person[] ret = new Person[persons.length];
for (int i = 0; i < persons.length; i++) {
final GenericDocument document = persons[i];
if (document == null) continue;
final AppSearchPerson person = new AppSearchPerson(document);
ret[i] = person.toPerson();
}
return ret;
}
}