blob: 1e4c7cac4404ae586b96cfebb22e1f5657c29ece [file] [log] [blame]
/*
* Copyright (C) 2019 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 com.android.settingslib.drawer;
import static com.android.settingslib.drawer.TileUtils.META_DATA_KEY_ORDER;
import static com.android.settingslib.drawer.TileUtils.META_DATA_KEY_PROFILE;
import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_ICON;
import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_ICON_URI;
import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_KEYHINT;
import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_SUMMARY;
import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_SUMMARY_URI;
import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_SWITCH_URI;
import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_TITLE;
import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_TITLE_URI;
import static com.android.settingslib.drawer.TileUtils.PROFILE_ALL;
import static com.android.settingslib.drawer.TileUtils.PROFILE_PRIMARY;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ComponentInfo;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.drawable.Icon;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.UserHandle;
import android.text.TextUtils;
import android.util.Log;
import androidx.annotation.VisibleForTesting;
import java.util.ArrayList;
import java.util.Comparator;
/**
* Description of a single dashboard tile that the user can select.
*/
public abstract class Tile implements Parcelable {
private static final String TAG = "Tile";
/**
* Optional list of user handles which the intent should be launched on.
*/
public ArrayList<UserHandle> userHandle = new ArrayList<>();
@VisibleForTesting
long mLastUpdateTime;
private final String mComponentPackage;
private final String mComponentName;
private final Intent mIntent;
protected ComponentInfo mComponentInfo;
private CharSequence mSummaryOverride;
private Bundle mMetaData;
private String mCategory;
public Tile(ComponentInfo info, String category) {
mComponentInfo = info;
mComponentPackage = mComponentInfo.packageName;
mComponentName = mComponentInfo.name;
mCategory = category;
mIntent = new Intent().setClassName(mComponentPackage, mComponentName);
}
Tile(Parcel in) {
final boolean isProviderTile = in.readBoolean();
mComponentPackage = in.readString();
mComponentName = in.readString();
mIntent = new Intent().setClassName(mComponentPackage, mComponentName);
final int number = in.readInt();
for (int i = 0; i < number; i++) {
userHandle.add(UserHandle.CREATOR.createFromParcel(in));
}
mCategory = in.readString();
mMetaData = in.readBundle();
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeBoolean(this instanceof ProviderTile);
dest.writeString(mComponentPackage);
dest.writeString(mComponentName);
final int size = userHandle.size();
dest.writeInt(size);
for (int i = 0; i < size; i++) {
userHandle.get(i).writeToParcel(dest, flags);
}
dest.writeString(mCategory);
dest.writeBundle(mMetaData);
}
/**
* Unique ID of the tile
*/
public abstract int getId();
/**
* Human-readable description of the tile
*/
public abstract String getDescription();
protected abstract ComponentInfo getComponentInfo(Context context);
protected abstract CharSequence getComponentLabel(Context context);
protected abstract int getComponentIcon(ComponentInfo info);
public String getPackageName() {
return mComponentPackage;
}
public String getComponentName() {
return mComponentName;
}
/**
* Intent to launch when the preference is selected.
*/
public Intent getIntent() {
return mIntent;
}
/**
* Category in which the tile should be placed.
*/
public String getCategory() {
return mCategory;
}
public void setCategory(String newCategoryKey) {
mCategory = newCategoryKey;
}
/**
* Priority of this tile, used for display ordering.
*/
public int getOrder() {
if (hasOrder()) {
return mMetaData.getInt(META_DATA_KEY_ORDER);
} else {
return 0;
}
}
/**
* Check whether tile has order.
*/
public boolean hasOrder() {
return mMetaData.containsKey(META_DATA_KEY_ORDER)
&& mMetaData.get(META_DATA_KEY_ORDER) instanceof Integer;
}
/**
* Check whether tile has a switch.
*/
public boolean hasSwitch() {
return mMetaData != null && mMetaData.containsKey(META_DATA_PREFERENCE_SWITCH_URI);
}
/**
* Title of the tile that is shown to the user.
*/
public CharSequence getTitle(Context context) {
CharSequence title = null;
ensureMetadataNotStale(context);
final PackageManager packageManager = context.getPackageManager();
if (mMetaData.containsKey(META_DATA_PREFERENCE_TITLE)) {
if (mMetaData.containsKey(META_DATA_PREFERENCE_TITLE_URI)) {
// If has as uri to provide dynamic title, skip loading here. UI will later load
// at tile binding time.
return null;
}
if (mMetaData.get(META_DATA_PREFERENCE_TITLE) instanceof Integer) {
try {
final Resources res =
packageManager.getResourcesForApplication(mComponentPackage);
title = res.getString(mMetaData.getInt(META_DATA_PREFERENCE_TITLE));
} catch (PackageManager.NameNotFoundException | Resources.NotFoundException e) {
Log.w(TAG, "Couldn't find info", e);
}
} else {
title = mMetaData.getString(META_DATA_PREFERENCE_TITLE);
}
}
// Set the preference title by the component if no meta-data is found
if (title == null) {
title = getComponentLabel(context);
}
return title;
}
/**
* Overrides the summary. This can happen when injected tile wants to provide dynamic summary.
*/
public void overrideSummary(CharSequence summaryOverride) {
mSummaryOverride = summaryOverride;
}
/**
* Optional summary describing what this tile controls.
*/
public CharSequence getSummary(Context context) {
if (mSummaryOverride != null) {
return mSummaryOverride;
}
ensureMetadataNotStale(context);
CharSequence summary = null;
final PackageManager packageManager = context.getPackageManager();
if (mMetaData != null) {
if (mMetaData.containsKey(META_DATA_PREFERENCE_SUMMARY_URI)) {
// If has as uri to provide dynamic summary, skip loading here. UI will later load
// at tile binding time.
return null;
}
if (mMetaData.containsKey(META_DATA_PREFERENCE_SUMMARY)) {
if (mMetaData.get(META_DATA_PREFERENCE_SUMMARY) instanceof Integer) {
try {
final Resources res =
packageManager.getResourcesForApplication(mComponentPackage);
summary = res.getString(mMetaData.getInt(META_DATA_PREFERENCE_SUMMARY));
} catch (PackageManager.NameNotFoundException | Resources.NotFoundException e) {
Log.d(TAG, "Couldn't find info", e);
}
} else {
summary = mMetaData.getString(META_DATA_PREFERENCE_SUMMARY);
}
}
}
return summary;
}
public void setMetaData(Bundle metaData) {
mMetaData = metaData;
}
/**
* The metaData from the activity that defines this tile.
*/
public Bundle getMetaData() {
return mMetaData;
}
/**
* Optional key to use for this tile.
*/
public String getKey(Context context) {
if (!hasKey()) {
return null;
}
ensureMetadataNotStale(context);
if (mMetaData.get(META_DATA_PREFERENCE_KEYHINT) instanceof Integer) {
return context.getResources().getString(mMetaData.getInt(META_DATA_PREFERENCE_KEYHINT));
} else {
return mMetaData.getString(META_DATA_PREFERENCE_KEYHINT);
}
}
/**
* Check whether title has key.
*/
public boolean hasKey() {
return mMetaData != null && mMetaData.containsKey(META_DATA_PREFERENCE_KEYHINT);
}
/**
* Optional icon to show for this tile.
*
* @attr ref android.R.styleable#PreferenceHeader_icon
*/
public Icon getIcon(Context context) {
if (context == null || mMetaData == null) {
return null;
}
ensureMetadataNotStale(context);
final ComponentInfo componentInfo = getComponentInfo(context);
if (componentInfo == null) {
Log.w(TAG, "Cannot find ComponentInfo for " + getDescription());
return null;
}
int iconResId = mMetaData.getInt(META_DATA_PREFERENCE_ICON);
// Set the icon
if (iconResId == 0) {
// Only fallback to componentInfo.icon if metadata does not contain ICON_URI.
// ICON_URI should be loaded in app UI when need the icon object. Handling IPC at this
// level is too complex because we don't have a strong threading contract for this class
if (!mMetaData.containsKey(META_DATA_PREFERENCE_ICON_URI)) {
iconResId = getComponentIcon(componentInfo);
}
}
if (iconResId != 0) {
final Icon icon = Icon.createWithResource(componentInfo.packageName, iconResId);
if (isIconTintable(context)) {
final TypedArray a = context.obtainStyledAttributes(new int[]{
android.R.attr.colorControlNormal});
final int tintColor = a.getColor(0, 0);
a.recycle();
icon.setTint(tintColor);
}
return icon;
} else {
return null;
}
}
/**
* Whether the icon can be tinted. This is true when icon needs to be monochrome (single-color)
*/
public boolean isIconTintable(Context context) {
ensureMetadataNotStale(context);
if (mMetaData != null
&& mMetaData.containsKey(TileUtils.META_DATA_PREFERENCE_ICON_TINTABLE)) {
return mMetaData.getBoolean(TileUtils.META_DATA_PREFERENCE_ICON_TINTABLE);
}
return false;
}
/**
* Ensures metadata is not stale for this tile.
*/
private void ensureMetadataNotStale(Context context) {
final PackageManager pm = context.getApplicationContext().getPackageManager();
try {
final long lastUpdateTime = pm.getPackageInfo(mComponentPackage,
PackageManager.GET_META_DATA).lastUpdateTime;
if (lastUpdateTime == mLastUpdateTime) {
// All good. Do nothing
return;
}
// App has been updated since we load metadata last time. Reload metadata.
mComponentInfo = null;
getComponentInfo(context);
mLastUpdateTime = lastUpdateTime;
} catch (PackageManager.NameNotFoundException e) {
Log.d(TAG, "Can't find package, probably uninstalled.");
}
}
public static final Creator<Tile> CREATOR = new Creator<Tile>() {
public Tile createFromParcel(Parcel source) {
final boolean isProviderTile = source.readBoolean();
// reset the Parcel pointer before delegating to the real constructor.
source.setDataPosition(0);
return isProviderTile ? new ProviderTile(source) : new ActivityTile(source);
}
public Tile[] newArray(int size) {
return new Tile[size];
}
};
/**
* Check whether tile only has primary profile.
*/
public boolean isPrimaryProfileOnly() {
String profile = mMetaData != null
? mMetaData.getString(META_DATA_KEY_PROFILE) : PROFILE_ALL;
profile = (profile != null ? profile : PROFILE_ALL);
return TextUtils.equals(profile, PROFILE_PRIMARY);
}
public static final Comparator<Tile> TILE_COMPARATOR =
(lhs, rhs) -> rhs.getOrder() - lhs.getOrder();
}