fix: 首次提交

This commit is contained in:
2024-12-09 11:25:23 +08:00
parent d0c01071e9
commit 2c2109a5f3
4741 changed files with 290641 additions and 0 deletions

View File

@@ -0,0 +1,27 @@
package {
// See: http://go/android-license-faq
// A large-scale-change added 'default_applicable_licenses' to import
// all of the 'license_kinds' from "frameworks_base_license"
// to get the below license kinds:
// SPDX-license-identifier-Apache-2.0
default_applicable_licenses: ["frameworks_base_license"],
}
android_library {
name: "SettingsLibTile",
use_resource_processor: true,
defaults: [
"SettingsLintDefaults",
],
srcs: ["src/**/*.java"],
static_libs: [
"androidx.annotation_annotation",
],
min_sdk_version: "21",
lint: {
baseline_filename: "lint-baseline.xml",
},
}

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.settingslib.drawer">
<uses-sdk android:minSdkVersion="21" />
</manifest>

View File

@@ -0,0 +1,50 @@
/**
* Include this gradle file if you are building against this as a standalone gradle library project,
* as opposed to building it as part of the git-tree. This is typically the file you want to include
* if you create a new project in Android Studio.
*
* For example, you can include the following in your settings.gradle file:
* include ':setupcompat'
* project(':setupcompat').projectDir = new File(PATH_TO_THIS_DIRECTORY)
*
* And then you can include the :setupcompat project as one of your dependencies
* dependencies {
* implementation project(path: ':setupcompat')
* }
*/
plugins {
alias(libs.plugins.android.library)
alias(libs.plugins.kotlin.android)
}
android {
// Not specifying compileSdkVersion here so clients can specify it; must be at least Q
namespace = "com.android.settingslib.drawer"
compileSdk 34
defaultConfig {
minSdkVersion 31
targetSdkVersion 34
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard.flags'
}
}
sourceSets.main {
manifest.srcFile 'AndroidManifest.xml'
java.srcDirs = ['src']
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
}
dependencies {
implementation libs.androidx.annotation.annotation
}

View File

@@ -0,0 +1,59 @@
<?xml version="1.0" encoding="UTF-8"?>
<issues format="6" by="lint 8.4.0-alpha01" type="baseline" client="" dependencies="true" name="" variant="all" version="8.4.0-alpha01">
<issue
id="NewApi"
message="Call requires API level 29 (current min is 21): `android.os.Parcel#writeBoolean`"
errorLine1=" dest.writeBoolean(this instanceof ProviderTile);"
errorLine2=" ~~~~~~~~~~~~">
<location
file="frameworks/base/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/Tile.java"
line="114"
column="14"/>
</issue>
<issue
id="NewApi"
message="Call requires API level 23 (current min is 21): `android.graphics.drawable.Icon#createWithResource`"
errorLine1=" final Icon icon = Icon.createWithResource(componentInfo.packageName, iconResId);"
errorLine2=" ~~~~~~~~~~~~~~~~~~">
<location
file="frameworks/base/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/Tile.java"
line="326"
column="36"/>
</issue>
<issue
id="NewApi"
message="Call requires API level 23 (current min is 21): `android.graphics.drawable.Icon#setTint`"
errorLine1=" icon.setTint(tintColor);"
errorLine2=" ~~~~~~~">
<location
file="frameworks/base/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/Tile.java"
line="332"
column="22"/>
</issue>
<issue
id="NewApi"
message="Call requires API level 29 (current min is 21): `android.os.Parcel#readBoolean`"
errorLine1=" final boolean isProviderTile = source.readBoolean();"
errorLine2=" ~~~~~~~~~~~">
<location
file="frameworks/base/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/Tile.java"
line="387"
column="51"/>
</issue>
<issue
id="NewApi"
message="Call requires API level 31 (current min is 21): `android.content.Context#getAttributionSource`"
errorLine1=" return provider.call(context.getAttributionSource(),"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
<location
file="frameworks/base/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/TileUtils.java"
line="601"
column="42"/>
</issue>
</issues>

View File

@@ -0,0 +1,86 @@
/*
* 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 android.content.Context;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.ComponentInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.os.Parcel;
import android.util.Log;
import java.util.List;
import java.util.Objects;
/**
* Description of a single dashboard tile which is generated from an activity.
*/
public class ActivityTile extends Tile {
private static final String TAG = "ActivityTile";
public ActivityTile(ActivityInfo info, String category) {
super(info, category, info.metaData);
}
ActivityTile(Parcel in) {
super(in);
}
@Override
public int getId() {
return Objects.hash(getPackageName(), getComponentName());
}
@Override
public String getDescription() {
return getPackageName() + "/" + getComponentName();
}
@Override
protected ComponentInfo getComponentInfo(Context context) {
if (mComponentInfo == null) {
final PackageManager pm = context.getApplicationContext().getPackageManager();
final Intent intent = getIntent();
final List<ResolveInfo> infoList =
pm.queryIntentActivities(intent, PackageManager.GET_META_DATA);
if (infoList != null && !infoList.isEmpty()) {
mComponentInfo = infoList.get(0).activityInfo;
setMetaData(mComponentInfo.metaData);
} else {
Log.e(TAG, "Cannot find package info for "
+ intent.getComponent().flattenToString());
}
}
return mComponentInfo;
}
@Override
protected CharSequence getComponentLabel(Context context) {
final PackageManager pm = context.getPackageManager();
final ComponentInfo info = getComponentInfo(context);
return info == null
? null
: info.loadLabel(pm);
}
@Override
protected int getComponentIcon(ComponentInfo componentInfo) {
return componentInfo.icon;
}
}

View File

@@ -0,0 +1,163 @@
/*
* 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 java.lang.String.CASE_INSENSITIVE_ORDER;
import android.os.Parcel;
import android.os.Parcelable;
import android.text.TextUtils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* The category for handle {@link Tile}
*/
public class DashboardCategory implements Parcelable {
/**
* Key used for placing external tiles.
*/
public final String key;
/**
* List of the category's children
*/
private List<Tile> mTiles = new ArrayList<>();
public DashboardCategory(String key) {
this.key = key;
}
DashboardCategory(Parcel in) {
key = in.readString();
final int count = in.readInt();
for (int n = 0; n < count; n++) {
Tile tile = Tile.CREATOR.createFromParcel(in);
mTiles.add(tile);
}
}
/**
* Get a copy of the list of the category's children.
*
* Note: the returned list serves as a read-only list. If tiles needs to be added or removed
* from the actual tiles list, it should be done through {@link #addTile}, {@link #removeTile}.
*/
public synchronized List<Tile> getTiles() {
final List<Tile> result = new ArrayList<>(mTiles.size());
for (Tile tile : mTiles) {
result.add(tile);
}
return result;
}
/**
* Add tile
*/
public synchronized void addTile(Tile tile) {
mTiles.add(tile);
}
/**
* Remove tile
*/
public synchronized void removeTile(int n) {
mTiles.remove(n);
}
/**
* Get size of tile
*/
public int getTilesCount() {
return mTiles.size();
}
/**
* Get tile
*/
public Tile getTile(int n) {
return mTiles.get(n);
}
/**
* Sort priority value for tiles in this category.
*/
public void sortTiles() {
Collections.sort(mTiles, Tile.TILE_COMPARATOR);
}
/**
* Sort priority value and package name for tiles in this category.
*/
public synchronized void sortTiles(String skipPackageName) {
// Sort mTiles based on [order, package within order]
Collections.sort(mTiles, (tile1, tile2) -> {
// First sort by order
final int orderCompare = tile2.getOrder() - tile1.getOrder();
if (orderCompare != 0) {
return orderCompare;
}
// Then sort by package name, skip package take precedence
final String package1 = tile1.getPackageName();
final String package2 = tile2.getPackageName();
final int packageCompare = CASE_INSENSITIVE_ORDER.compare(package1, package2);
if (packageCompare != 0) {
if (TextUtils.equals(package1, skipPackageName)) {
return -1;
}
if (TextUtils.equals(package2, skipPackageName)) {
return 1;
}
}
return packageCompare;
});
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(key);
final int count = mTiles.size();
dest.writeInt(count);
for (int n = 0; n < count; n++) {
Tile tile = mTiles.get(n);
tile.writeToParcel(dest, flags);
}
}
public static final Creator<DashboardCategory> CREATOR = new Creator<DashboardCategory>() {
public DashboardCategory createFromParcel(Parcel source) {
return new DashboardCategory(source);
}
public DashboardCategory[] newArray(int size) {
return new DashboardCategory[size];
}
};
}

View File

@@ -0,0 +1,23 @@
/*
* 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;
/** Interface for {@link EntryController} whose instances support dynamic summary */
public interface DynamicSummary {
/** @return the dynamic summary text */
String getDynamicSummary();
}

View File

@@ -0,0 +1,23 @@
/*
* 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;
/** Interface for {@link EntryController} whose instances support dynamic title */
public interface DynamicTitle {
/** @return the dynamic title text */
String getDynamicTitle();
}

View File

@@ -0,0 +1,222 @@
/*
* Copyright (C) 2023 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_PREFERENCE_KEYHINT;
import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_SUMMARY;
import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_TITLE;
import android.content.ContentProvider;
import android.content.ContentValues;
import android.content.Context;
import android.content.pm.ProviderInfo;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.Log;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* An abstract class for injecting entries to Settings.
*/
public abstract class EntriesProvider extends ContentProvider {
private static final String TAG = "EntriesProvider";
public static final String METHOD_GET_ENTRY_DATA = "getEntryData";
public static final String METHOD_GET_PROVIDER_ICON = "getProviderIcon";
public static final String METHOD_GET_DYNAMIC_TITLE = "getDynamicTitle";
public static final String METHOD_GET_DYNAMIC_SUMMARY = "getDynamicSummary";
public static final String METHOD_IS_CHECKED = "isChecked";
public static final String METHOD_ON_CHECKED_CHANGED = "onCheckedChanged";
/**
* @deprecated use {@link #METHOD_GET_ENTRY_DATA} instead.
*/
@Deprecated
public static final String METHOD_GET_SWITCH_DATA = "getSwitchData";
public static final String EXTRA_ENTRY_DATA = "entry_data";
public static final String EXTRA_SWITCH_CHECKED_STATE = "checked_state";
public static final String EXTRA_SWITCH_SET_CHECKED_ERROR = "set_checked_error";
public static final String EXTRA_SWITCH_SET_CHECKED_ERROR_MESSAGE = "set_checked_error_message";
/**
* @deprecated use {@link #EXTRA_ENTRY_DATA} instead.
*/
@Deprecated
public static final String EXTRA_SWITCH_DATA = "switch_data";
private String mAuthority;
private final Map<String, EntryController> mControllerMap = new LinkedHashMap<>();
private final List<Bundle> mEntryDataList = new ArrayList<>();
/**
* Get a list of {@link EntryController} for this provider.
*/
protected abstract List<? extends EntryController> createEntryControllers();
protected EntryController getController(String key) {
return mControllerMap.get(key);
}
@Override
public void attachInfo(Context context, ProviderInfo info) {
mAuthority = info.authority;
Log.i(TAG, mAuthority);
super.attachInfo(context, info);
}
@Override
public boolean onCreate() {
final List<? extends EntryController> controllers = createEntryControllers();
if (controllers == null || controllers.isEmpty()) {
throw new IllegalArgumentException();
}
for (EntryController controller : controllers) {
final String key = controller.getKey();
if (TextUtils.isEmpty(key)) {
throw new NullPointerException("Entry key cannot be null: "
+ controller.getClass().getSimpleName());
} else if (mControllerMap.containsKey(key)) {
throw new IllegalArgumentException("Entry key " + key + " is duplicated by: "
+ controller.getClass().getSimpleName());
}
controller.setAuthority(mAuthority);
mControllerMap.put(key, controller);
if (!(controller instanceof PrimarySwitchController)) {
mEntryDataList.add(controller.getBundle());
}
}
return true;
}
@Override
public Bundle call(String method, String uriString, Bundle extras) {
final Bundle bundle = new Bundle();
final String key = extras != null
? extras.getString(META_DATA_PREFERENCE_KEYHINT)
: null;
if (TextUtils.isEmpty(key)) {
switch (method) {
case METHOD_GET_ENTRY_DATA:
bundle.putParcelableList(EXTRA_ENTRY_DATA, mEntryDataList);
return bundle;
case METHOD_GET_SWITCH_DATA:
bundle.putParcelableList(EXTRA_SWITCH_DATA, mEntryDataList);
return bundle;
default:
return null;
}
}
final EntryController controller = mControllerMap.get(key);
if (controller == null) {
return null;
}
switch (method) {
case METHOD_GET_ENTRY_DATA:
case METHOD_GET_SWITCH_DATA:
if (!(controller instanceof PrimarySwitchController)) {
return controller.getBundle();
}
break;
case METHOD_GET_PROVIDER_ICON:
if (controller instanceof ProviderIcon) {
return ((ProviderIcon) controller).getProviderIcon();
}
break;
case METHOD_GET_DYNAMIC_TITLE:
if (controller instanceof DynamicTitle) {
bundle.putString(META_DATA_PREFERENCE_TITLE,
((DynamicTitle) controller).getDynamicTitle());
return bundle;
}
break;
case METHOD_GET_DYNAMIC_SUMMARY:
if (controller instanceof DynamicSummary) {
bundle.putString(META_DATA_PREFERENCE_SUMMARY,
((DynamicSummary) controller).getDynamicSummary());
return bundle;
}
break;
case METHOD_IS_CHECKED:
if (controller instanceof ProviderSwitch) {
bundle.putBoolean(EXTRA_SWITCH_CHECKED_STATE,
((ProviderSwitch) controller).isSwitchChecked());
return bundle;
}
break;
case METHOD_ON_CHECKED_CHANGED:
if (controller instanceof ProviderSwitch) {
return onSwitchCheckedChanged(extras.getBoolean(EXTRA_SWITCH_CHECKED_STATE),
(ProviderSwitch) controller);
}
break;
}
return null;
}
private Bundle onSwitchCheckedChanged(boolean checked, ProviderSwitch controller) {
final boolean success = controller.onSwitchCheckedChanged(checked);
final Bundle bundle = new Bundle();
bundle.putBoolean(EXTRA_SWITCH_SET_CHECKED_ERROR, !success);
if (success) {
if (controller instanceof DynamicSummary) {
((EntryController) controller).notifySummaryChanged(getContext());
}
} else {
bundle.putString(EXTRA_SWITCH_SET_CHECKED_ERROR_MESSAGE,
controller.getSwitchErrorMessage(checked));
}
return bundle;
}
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
String sortOrder) {
throw new UnsupportedOperationException();
}
@Override
public String getType(Uri uri) {
throw new UnsupportedOperationException();
}
@Override
public Uri insert(Uri uri, ContentValues values) {
throw new UnsupportedOperationException();
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
throw new UnsupportedOperationException();
}
@Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
throw new UnsupportedOperationException();
}
}

View File

@@ -0,0 +1,246 @@
/*
* Copyright (C) 2023 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.SwitchesProvider.METHOD_GET_DYNAMIC_SUMMARY;
import static com.android.settingslib.drawer.SwitchesProvider.METHOD_GET_DYNAMIC_TITLE;
import static com.android.settingslib.drawer.TileUtils.EXTRA_CATEGORY_KEY;
import static com.android.settingslib.drawer.TileUtils.META_DATA_KEY_ORDER;
import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_ICON;
import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_ICON_BACKGROUND_ARGB;
import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_ICON_BACKGROUND_HINT;
import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_ICON_TINTABLE;
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_PENDING_INTENT;
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 android.app.PendingIntent;
import android.content.ContentResolver;
import android.content.Context;
import android.net.Uri;
import android.os.Bundle;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.annotation.StringRes;
/**
* A controller that manages events for switch.
*/
public abstract class EntryController {
private String mAuthority;
/**
* Returns the key for this switch.
*/
public abstract String getKey();
/**
* Returns the {@link MetaData} for this switch.
*/
protected abstract MetaData getMetaData();
/**
* Notify registered observers that title was updated and attempt to sync changes.
*/
public void notifyTitleChanged(Context context) {
if (this instanceof DynamicTitle) {
notifyChanged(context, METHOD_GET_DYNAMIC_TITLE);
}
}
/**
* Notify registered observers that summary was updated and attempt to sync changes.
*/
public void notifySummaryChanged(Context context) {
if (this instanceof DynamicSummary) {
notifyChanged(context, METHOD_GET_DYNAMIC_SUMMARY);
}
}
void setAuthority(String authority) {
mAuthority = authority;
}
Bundle getBundle() {
final MetaData metaData = getMetaData();
if (metaData == null) {
throw new NullPointerException("Should not return null in getMetaData()");
}
final Bundle bundle = metaData.build();
final String uriString = new Uri.Builder()
.scheme(ContentResolver.SCHEME_CONTENT)
.authority(mAuthority)
.build()
.toString();
bundle.putString(META_DATA_PREFERENCE_KEYHINT, getKey());
if (this instanceof ProviderIcon) {
bundle.putString(META_DATA_PREFERENCE_ICON_URI, uriString);
}
if (this instanceof DynamicTitle) {
bundle.putString(META_DATA_PREFERENCE_TITLE_URI, uriString);
}
if (this instanceof DynamicSummary) {
bundle.putString(META_DATA_PREFERENCE_SUMMARY_URI, uriString);
}
if (this instanceof ProviderSwitch) {
bundle.putString(META_DATA_PREFERENCE_SWITCH_URI, uriString);
}
return bundle;
}
private void notifyChanged(Context context, String method) {
final Uri uri = TileUtils.buildUri(mAuthority, method, getKey());
context.getContentResolver().notifyChange(uri, null);
}
/**
* Collects all meta data of the item.
*/
protected static class MetaData {
private String mCategory;
private int mOrder;
@DrawableRes
private int mIcon;
private int mIconBackgroundHint;
private int mIconBackgroundArgb;
private Boolean mIconTintable;
@StringRes
private int mTitleId;
private String mTitle;
@StringRes
private int mSummaryId;
private String mSummary;
private PendingIntent mPendingIntent;
/**
* @param category the category of the switch. This value must be from {@link CategoryKey}.
*/
public MetaData(@NonNull String category) {
mCategory = category;
}
/**
* Set the order of the item that should be displayed on screen. Bigger value items displays
* closer on top.
*/
public MetaData setOrder(int order) {
mOrder = order;
return this;
}
/** Set the icon that should be displayed for the item. */
public MetaData setIcon(@DrawableRes int icon) {
mIcon = icon;
return this;
}
/** Set the icon background color. The value may or may not be used by Settings app. */
public MetaData setIconBackgoundHint(int hint) {
mIconBackgroundHint = hint;
return this;
}
/** Set the icon background color as raw ARGB. */
public MetaData setIconBackgoundArgb(int argb) {
mIconBackgroundArgb = argb;
return this;
}
/** Specify whether the icon is tintable. */
public MetaData setIconTintable(boolean tintable) {
mIconTintable = tintable;
return this;
}
/** Set the title that should be displayed for the item. */
public MetaData setTitle(@StringRes int id) {
mTitleId = id;
return this;
}
/** Set the title that should be displayed for the item. */
public MetaData setTitle(String title) {
mTitle = title;
return this;
}
/** Set the summary text that should be displayed for the item. */
public MetaData setSummary(@StringRes int id) {
mSummaryId = id;
return this;
}
/** Set the summary text that should be displayed for the item. */
public MetaData setSummary(String summary) {
mSummary = summary;
return this;
}
public MetaData setPendingIntent(PendingIntent pendingIntent) {
mPendingIntent = pendingIntent;
return this;
}
protected Bundle build() {
final Bundle bundle = new Bundle();
bundle.putString(EXTRA_CATEGORY_KEY, mCategory);
if (mOrder != 0) {
bundle.putInt(META_DATA_KEY_ORDER, mOrder);
}
if (mIcon != 0) {
bundle.putInt(META_DATA_PREFERENCE_ICON, mIcon);
}
if (mIconBackgroundHint != 0) {
bundle.putInt(META_DATA_PREFERENCE_ICON_BACKGROUND_HINT, mIconBackgroundHint);
}
if (mIconBackgroundArgb != 0) {
bundle.putInt(META_DATA_PREFERENCE_ICON_BACKGROUND_ARGB, mIconBackgroundArgb);
}
if (mIconTintable != null) {
bundle.putBoolean(META_DATA_PREFERENCE_ICON_TINTABLE, mIconTintable);
}
if (mTitleId != 0) {
bundle.putInt(META_DATA_PREFERENCE_TITLE, mTitleId);
} else if (mTitle != null) {
bundle.putString(META_DATA_PREFERENCE_TITLE, mTitle);
}
if (mSummaryId != 0) {
bundle.putInt(META_DATA_PREFERENCE_SUMMARY, mSummaryId);
} else if (mSummary != null) {
bundle.putString(META_DATA_PREFERENCE_SUMMARY, mSummary);
}
if (mPendingIntent != null) {
bundle.putParcelable(META_DATA_PREFERENCE_PENDING_INTENT, mPendingIntent);
}
return bundle;
}
}
}

View File

@@ -0,0 +1,35 @@
/*
* 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 android.os.Bundle;
/**
* A controller that manages event for Primary switch.
*/
public abstract class PrimarySwitchController extends SwitchController {
@Override
protected final MetaData getMetaData() {
throw new UnsupportedOperationException();
}
@Override
final Bundle getBundle() {
throw new UnsupportedOperationException();
}
}

View File

@@ -0,0 +1,31 @@
/*
* 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 android.os.Bundle;
/**
* Interface for {@link EntryController} whose instances support icon provided from the content
* provider
*/
public interface ProviderIcon {
/**
* @return the bundle of icon info including {@link TileUtils#EXTRA_PREFERENCE_ICON_PACKAGE} and
* {@link TileUtils#META_DATA_PREFERENCE_ICON}.
*/
Bundle getProviderIcon();
}

View File

@@ -0,0 +1,41 @@
/*
* Copyright (C) 2023 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;
/**
* Interface for {@link EntryController} whose instances support switch widget provided from the
* content provider
*/
public interface ProviderSwitch {
/**
* Returns the checked state of this switch.
*/
boolean isSwitchChecked();
/**
* Called when the checked state of this switch is changed.
*
* @return true if the checked state was successfully changed, otherwise false
*/
boolean onSwitchCheckedChanged(boolean checked);
/**
* Returns the error message which will be toasted when {@link #onSwitchCheckedChanged} returns
* false.
*/
String getSwitchErrorMessage(boolean attemptedChecked);
}

View File

@@ -0,0 +1,104 @@
/*
* 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_PREFERENCE_KEYHINT;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ComponentInfo;
import android.content.pm.PackageManager;
import android.content.pm.ProviderInfo;
import android.content.pm.ResolveInfo;
import android.os.Bundle;
import android.os.Parcel;
import android.util.Log;
import java.util.List;
import java.util.Objects;
/**
* Description of a single dashboard tile which is generated from a content provider.
*/
public class ProviderTile extends Tile {
private static final String TAG = "ProviderTile";
private static final boolean DEBUG_TIMING = false;
private String mAuthority;
private String mKey;
public ProviderTile(ProviderInfo info, String category, Bundle metaData) {
super(info, category, metaData);
mAuthority = info.authority;
mKey = metaData.getString(META_DATA_PREFERENCE_KEYHINT);
}
ProviderTile(Parcel in) {
super(in);
mAuthority = ((ProviderInfo) mComponentInfo).authority;
mKey = getMetaData().getString(META_DATA_PREFERENCE_KEYHINT);
}
@Override
public int getId() {
return Objects.hash(mAuthority, mKey);
}
@Override
public String getDescription() {
return mAuthority + "/" + mKey;
}
@Override
protected ComponentInfo getComponentInfo(Context context) {
if (mComponentInfo == null) {
final long startTime = System.currentTimeMillis();
final PackageManager pm = context.getApplicationContext().getPackageManager();
final Intent intent = getIntent();
final List<ResolveInfo> infoList =
pm.queryIntentContentProviders(intent, 0 /* flags */);
if (infoList != null && !infoList.isEmpty()) {
final ProviderInfo providerInfo = infoList.get(0).providerInfo;
mComponentInfo = providerInfo;
setMetaData(TileUtils.getEntryDataFromProvider(context, providerInfo.authority,
mKey));
} else {
Log.e(TAG, "Cannot find package info for "
+ intent.getComponent().flattenToString());
}
if (DEBUG_TIMING) {
Log.d(TAG, "getComponentInfo took "
+ (System.currentTimeMillis() - startTime) + " ms");
}
}
return mComponentInfo;
}
@Override
protected CharSequence getComponentLabel(Context context) {
// Getting provider label for a tile title isn't supported.
return null;
}
@Override
protected int getComponentIcon(ComponentInfo info) {
// Getting provider icon for a tile title isn't supported.
return 0;
}
}

View File

@@ -0,0 +1,89 @@
/*
* 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 androidx.annotation.NonNull;
/**
* A controller that manages events for switch.
*
* @deprecated Use {@link EntriesProvider} with {@link ProviderSwitch} instead.
*/
@Deprecated
public abstract class SwitchController extends EntryController implements ProviderSwitch {
/**
* Returns the key for this switch.
*/
public abstract String getSwitchKey();
/**
* Returns the checked state of this switch.
*/
protected abstract boolean isChecked();
/**
* Called when the checked state of this switch is changed.
*
* @return true if the checked state was successfully changed, otherwise false
*/
protected abstract boolean onCheckedChanged(boolean checked);
/**
* Returns the error message which will be toasted when {@link #onCheckedChanged} returns false.
*/
protected abstract String getErrorMessage(boolean attemptedChecked);
@Override
public String getKey() {
return getSwitchKey();
}
@Override
public boolean isSwitchChecked() {
return isChecked();
}
@Override
public boolean onSwitchCheckedChanged(boolean checked) {
return onCheckedChanged(checked);
}
@Override
public String getSwitchErrorMessage(boolean attemptedChecked) {
return getErrorMessage(attemptedChecked);
}
/**
* Same as {@link EntryController.MetaData}, for backwards compatibility purpose.
*
* @deprecated Use {@link EntryController.MetaData} instead.
*/
@Deprecated
protected static class MetaData extends EntryController.MetaData {
/**
* @param category the category of the switch. This value must be from {@link CategoryKey}.
*
* @deprecated Use {@link EntryController.MetaData} instead.
*/
@Deprecated
public MetaData(@NonNull String category) {
super(category);
}
}
}

View File

@@ -0,0 +1,38 @@
/*
* 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 java.util.List;
/**
* An abstract class for injecting switches to Settings.
*
* @deprecated Use {@link EntriesProvider} instead.
*/
@Deprecated
public abstract class SwitchesProvider extends EntriesProvider {
/**
* Get a list of {@link SwitchController} for this provider.
*/
protected abstract List<SwitchController> createSwitchControllers();
@Override
protected List<? extends EntryController> createEntryControllers() {
return createSwitchControllers();
}
}

View File

@@ -0,0 +1,491 @@
/*
* 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_NEW_TASK;
import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_GROUP_KEY;
import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_ICON;
import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_KEYHINT;
import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_SEARCHABLE;
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.app.PendingIntent;
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;
import java.util.HashMap;
/**
* 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<>();
public HashMap<UserHandle, PendingIntent> pendingIntentMap = new HashMap<>();
@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, Bundle metaData) {
mComponentInfo = info;
mComponentPackage = mComponentInfo.packageName;
mComponentName = mComponentInfo.name;
mCategory = category;
mMetaData = metaData;
mIntent = new Intent().setClassName(mComponentPackage, mComponentName);
if (isNewTask()) {
mIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
}
}
Tile(Parcel in) {
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();
if (isNewTask()) {
mIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
}
}
@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 != null
&& 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);
}
/**
* Check whether tile has a pending intent.
*/
public boolean hasPendingIntent() {
return !pendingIntentMap.isEmpty();
}
/**
* 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 != null && 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) {
ensureMetadataNotStale(context);
if (!hasKey()) {
return null;
}
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. Skip the transparent color for backward compatibility since Android S.
if (iconResId != 0 && iconResId != android.R.color.transparent) {
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;
}
/**
* Whether the {@link Activity} should be launched in a separate task.
*/
public boolean isNewTask() {
if (mMetaData != null
&& mMetaData.containsKey(META_DATA_NEW_TASK)) {
return mMetaData.getBoolean(META_DATA_NEW_TASK);
}
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() {
return isPrimaryProfileOnly(mMetaData);
}
static boolean isPrimaryProfileOnly(Bundle metaData) {
String profile = metaData != null
? metaData.getString(META_DATA_KEY_PROFILE) : PROFILE_ALL;
profile = (profile != null ? profile : PROFILE_ALL);
return TextUtils.equals(profile, PROFILE_PRIMARY);
}
/**
* Returns whether the tile belongs to another group / category.
*/
public boolean hasGroupKey() {
return mMetaData != null
&& !TextUtils.isEmpty(mMetaData.getString(META_DATA_PREFERENCE_GROUP_KEY));
}
/**
* Returns the group / category key this tile belongs to.
*/
public String getGroupKey() {
return (mMetaData == null) ? null : mMetaData.getString(META_DATA_PREFERENCE_GROUP_KEY);
}
/**
* Returns if this is searchable.
*/
public boolean isSearchable() {
return mMetaData == null || mMetaData.getBoolean(META_DATA_PREFERENCE_SEARCHABLE, true);
}
/**
* The type of the tile.
*/
public enum Type {
/**
* A preference that can be tapped on to open a new page.
*/
ACTION,
/**
* A preference that can be tapped on to open an external app.
*/
EXTERNAL_ACTION,
/**
* A preference that shows an on / off switch that can be toggled by the user.
*/
SWITCH,
/**
* A preference with both an on / off switch, and a tappable area that can perform an
* action.
*/
SWITCH_WITH_ACTION,
/**
* A preference category with a title that can be used to group multiple preferences
* together.
*/
GROUP;
}
/**
* Returns the type of the tile.
*
* @see Type
*/
public Type getType() {
boolean hasExternalAction = hasPendingIntent();
boolean hasAction = hasExternalAction || this instanceof ActivityTile;
boolean hasSwitch = hasSwitch();
if (hasSwitch && hasAction) {
return Type.SWITCH_WITH_ACTION;
} else if (hasSwitch) {
return Type.SWITCH;
} else if (hasExternalAction) {
return Type.EXTERNAL_ACTION;
} else if (hasAction) {
return Type.ACTION;
} else {
return Type.GROUP;
}
}
public static final Comparator<Tile> TILE_COMPARATOR =
(lhs, rhs) -> rhs.getOrder() - lhs.getOrder();
}

View File

@@ -0,0 +1,636 @@
/*
* 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 android.app.ActivityManager;
import android.content.ContentResolver;
import android.content.Context;
import android.content.IContentProvider;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.ComponentInfo;
import android.content.pm.PackageManager;
import android.content.pm.ProviderInfo;
import android.content.pm.ResolveInfo;
import android.net.Uri;
import android.os.Bundle;
import android.os.RemoteException;
import android.os.UserHandle;
import android.os.UserManager;
import android.provider.Settings.Global;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.Log;
import android.util.Pair;
import androidx.annotation.VisibleForTesting;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Utils is a helper class that contains profile key, meta data, settings action
* and static methods for get icon or text from uri.
*/
public class TileUtils {
private static final boolean DEBUG_TIMING = false;
private static final String LOG_TAG = "TileUtils";
@VisibleForTesting
static final String SETTING_PKG = "com.android.settings";
/**
* Settings will search for system activities of this action and add them as a top level
* settings tile using the following parameters.
*
* <p>A category must be specified in the meta-data for the activity named
* {@link #EXTRA_CATEGORY_KEY}
*
* <p>The title may be defined by meta-data named {@link #META_DATA_PREFERENCE_TITLE}
* otherwise the label for the activity will be used.
*
* <p>The icon may be defined by meta-data named {@link #META_DATA_PREFERENCE_ICON}
* otherwise the icon for the activity will be used.
*
* <p>A summary my be defined by meta-data named {@link #META_DATA_PREFERENCE_SUMMARY}
*/
public static final String EXTRA_SETTINGS_ACTION = "com.android.settings.action.EXTRA_SETTINGS";
/**
* @See {@link #EXTRA_SETTINGS_ACTION}.
*/
public static final String IA_SETTINGS_ACTION = "com.android.settings.action.IA_SETTINGS";
/**
* Same as #EXTRA_SETTINGS_ACTION but used for the platform Settings activities.
*/
private static final String SETTINGS_ACTION = "com.android.settings.action.SETTINGS";
private static final String OPERATOR_SETTINGS =
"com.android.settings.OPERATOR_APPLICATION_SETTING";
private static final String OPERATOR_DEFAULT_CATEGORY =
"com.android.settings.category.wireless";
private static final String MANUFACTURER_SETTINGS =
"com.android.settings.MANUFACTURER_APPLICATION_SETTING";
private static final String MANUFACTURER_DEFAULT_CATEGORY =
"com.android.settings.category.device";
/**
* The key used to get the category from metadata of activities of action
* {@link #EXTRA_SETTINGS_ACTION}
* The value must be from {@link CategoryKey}.
*/
static final String EXTRA_CATEGORY_KEY = "com.android.settings.category";
/**
* The key used to get the package name of the icon resource for the preference.
*/
static final String EXTRA_PREFERENCE_ICON_PACKAGE = "com.android.settings.icon_package";
/**
* Name of the meta-data item that should be set in the AndroidManifest.xml
* to specify the key that should be used for the preference.
*/
public static final String META_DATA_PREFERENCE_KEYHINT = "com.android.settings.keyhint";
/**
* Name of the meta-data item that can be set in the AndroidManifest.xml or in the content
* provider to specify the key of a group / category where this preference belongs to.
*/
public static final String META_DATA_PREFERENCE_GROUP_KEY = "com.android.settings.group_key";
/**
* Order of the item that should be displayed on screen. Bigger value items displays closer on
* top.
*/
public static final String META_DATA_KEY_ORDER = "com.android.settings.order";
/**
* Name of the meta-data item that should be set in the AndroidManifest.xml
* to specify the icon that should be displayed for the preference.
*/
public static final String META_DATA_PREFERENCE_ICON = "com.android.settings.icon";
/**
* Name of the meta-data item that should be set in the AndroidManifest.xml
* to specify the icon background color. The value may or may not be used by Settings app.
*/
public static final String META_DATA_PREFERENCE_ICON_BACKGROUND_HINT =
"com.android.settings.bg.hint";
/**
* Name of the meta-data item that should be set in the AndroidManifest.xml
* to specify the icon background color as raw ARGB.
*/
public static final String META_DATA_PREFERENCE_ICON_BACKGROUND_ARGB =
"com.android.settings.bg.argb";
/**
* Name of the meta-data item that should be set in the AndroidManifest.xml
* to specify the content provider providing the icon that should be displayed for
* the preference.
*
* Icon provided by the content provider overrides any static icon.
*/
public static final String META_DATA_PREFERENCE_ICON_URI = "com.android.settings.icon_uri";
/**
* Name of the meta-data item that should be set in the AndroidManifest.xml
* to specify whether the icon is tintable. This should be a boolean value {@code true} or
* {@code false}, set using {@code android:value}
*/
public static final String META_DATA_PREFERENCE_ICON_TINTABLE =
"com.android.settings.icon_tintable";
/**
* Name of the meta-data item that should be set in the AndroidManifest.xml
* to specify the title that should be displayed for the preference.
*
* <p>Note: It is preferred to provide this value using {@code android:resource} with a string
* resource for localization.
*/
public static final String META_DATA_PREFERENCE_TITLE = "com.android.settings.title";
/**
* Name of the meta-data item that should be set in the AndroidManifest.xml
* to specify the content provider providing the title text that should be displayed for the
* preference.
*
* Title provided by the content provider overrides any static title.
*/
public static final String META_DATA_PREFERENCE_TITLE_URI =
"com.android.settings.title_uri";
/**
* Name of the meta-data item that should be set in the AndroidManifest.xml
* to specify the summary text that should be displayed for the preference.
*/
public static final String META_DATA_PREFERENCE_SUMMARY = "com.android.settings.summary";
/**
* Name of the meta-data item that should be set in the AndroidManifest.xml
* to specify the content provider providing the summary text that should be displayed for the
* preference.
*
* Summary provided by the content provider overrides any static summary.
*/
public static final String META_DATA_PREFERENCE_SUMMARY_URI =
"com.android.settings.summary_uri";
/**
* Name of the meta-data item that should be set in the AndroidManifest.xml
* to specify the content provider providing the switch that should be displayed for the
* preference.
*
* This works with {@link #META_DATA_PREFERENCE_KEYHINT} which should also be set in the
* AndroidManifest.xml
*/
public static final String META_DATA_PREFERENCE_SWITCH_URI =
"com.android.settings.switch_uri";
/**
* Name of the meta-data item that can be set from the content provider providing the intent
* that will be executed when the user taps on the preference.
*/
public static final String META_DATA_PREFERENCE_PENDING_INTENT =
"com.android.settings.pending_intent";
/**
* Value for {@link #META_DATA_KEY_PROFILE}. When the device has a managed profile,
* the app will always be run in the primary profile.
*
* @see #META_DATA_KEY_PROFILE
*/
public static final String PROFILE_PRIMARY = "primary_profile_only";
/**
* Value for {@link #META_DATA_KEY_PROFILE}. When the device has a managed profile, the user
* will be presented with a dialog to choose the profile the app will be run in.
*
* @see #META_DATA_KEY_PROFILE
*/
public static final String PROFILE_ALL = "all_profiles";
/**
* Name of the meta-data item that should be set in the AndroidManifest.xml
* to specify the profile in which the app should be run when the device has a managed profile.
* The default value is {@link #PROFILE_ALL} which means the user will be presented with a
* dialog to choose the profile. If set to {@link #PROFILE_PRIMARY} the app will always be
* run in the primary profile.
*
* @see #PROFILE_PRIMARY
* @see #PROFILE_ALL
*/
public static final String META_DATA_KEY_PROFILE = "com.android.settings.profile";
/**
* Name of the meta-data item that should be set in the AndroidManifest.xml
* to specify whether the {@link android.app.Activity} should be launched in a separate task.
* This should be a boolean value {@code true} or {@code false}, set using {@code android:value}
*/
public static final String META_DATA_NEW_TASK = "com.android.settings.new_task";
/**
* If the entry should be shown in settings search results. Defaults to true.
*/
public static final String META_DATA_PREFERENCE_SEARCHABLE = "com.android.settings.searchable";
/**
* Build a list of DashboardCategory.
*/
public static List<DashboardCategory> getCategories(Context context,
Map<Pair<String, String>, Tile> cache) {
final long startTime = System.currentTimeMillis();
final boolean setup =
Global.getInt(context.getContentResolver(), Global.DEVICE_PROVISIONED, 0) != 0;
final ArrayList<Tile> tiles = new ArrayList<>();
final UserManager userManager = (UserManager) context.getSystemService(
Context.USER_SERVICE);
for (UserHandle user : userManager.getUserProfiles()) {
// TODO: Needs much optimization, too many PM queries going on here.
if (user.getIdentifier() == ActivityManager.getCurrentUser()) {
// Only add Settings for this user.
loadTilesForAction(context, user, SETTINGS_ACTION, cache, null, tiles, true);
loadTilesForAction(context, user, OPERATOR_SETTINGS, cache,
OPERATOR_DEFAULT_CATEGORY, tiles, false);
loadTilesForAction(context, user, MANUFACTURER_SETTINGS, cache,
MANUFACTURER_DEFAULT_CATEGORY, tiles, false);
}
if (setup) {
loadTilesForAction(context, user, EXTRA_SETTINGS_ACTION, cache, null, tiles, false);
loadTilesForAction(context, user, IA_SETTINGS_ACTION, cache, null, tiles, false);
}
}
final HashMap<String, DashboardCategory> categoryMap = new HashMap<>();
for (Tile tile : tiles) {
final String categoryKey = tile.getCategory();
DashboardCategory category = categoryMap.get(categoryKey);
if (category == null) {
category = new DashboardCategory(categoryKey);
if (category == null) {
Log.w(LOG_TAG, "Couldn't find category " + categoryKey);
continue;
}
categoryMap.put(categoryKey, category);
}
category.addTile(tile);
}
final ArrayList<DashboardCategory> categories = new ArrayList<>(categoryMap.values());
for (DashboardCategory category : categories) {
category.sortTiles();
}
if (DEBUG_TIMING) {
Log.d(LOG_TAG, "getCategories took "
+ (System.currentTimeMillis() - startTime) + " ms");
}
return categories;
}
@VisibleForTesting
static void loadTilesForAction(Context context,
UserHandle user, String action, Map<Pair<String, String>, Tile> addedCache,
String defaultCategory, List<Tile> outTiles, boolean requireSettings) {
final Intent intent = new Intent(action);
if (requireSettings) {
intent.setPackage(SETTING_PKG);
}
loadActivityTiles(context, user, addedCache, defaultCategory, outTiles, intent);
loadProviderTiles(context, user, addedCache, defaultCategory, outTiles, intent);
}
private static void loadActivityTiles(Context context,
UserHandle user, Map<Pair<String, String>, Tile> addedCache,
String defaultCategory, List<Tile> outTiles, Intent intent) {
final PackageManager pm = context.getPackageManager();
final List<ResolveInfo> results = pm.queryIntentActivitiesAsUser(intent,
PackageManager.GET_META_DATA, user.getIdentifier());
for (ResolveInfo resolved : results) {
if (!resolved.system) {
// Do not allow any app to add to settings, only system ones.
continue;
}
final ActivityInfo activityInfo = resolved.activityInfo;
final Bundle metaData = activityInfo.metaData;
loadTile(user, addedCache, defaultCategory, outTiles, intent, metaData, activityInfo);
}
}
private static void loadProviderTiles(Context context,
UserHandle user, Map<Pair<String, String>, Tile> addedCache,
String defaultCategory, List<Tile> outTiles, Intent intent) {
final PackageManager pm = context.getPackageManager();
final List<ResolveInfo> results = pm.queryIntentContentProvidersAsUser(intent,
0 /* flags */, user.getIdentifier());
for (ResolveInfo resolved : results) {
if (!resolved.system) {
// Do not allow any app to add to settings, only system ones.
continue;
}
final ProviderInfo providerInfo = resolved.providerInfo;
final List<Bundle> entryData = getEntryDataFromProvider(
// Build new context so the entry data is retrieved for the queried user.
context.createContextAsUser(user, 0 /* flags */),
providerInfo.authority);
if (entryData == null || entryData.isEmpty()) {
continue;
}
for (Bundle metaData : entryData) {
loadTile(user, addedCache, defaultCategory, outTiles, intent, metaData,
providerInfo);
}
}
}
private static void loadTile(UserHandle user, Map<Pair<String, String>, Tile> addedCache,
String defaultCategory, List<Tile> outTiles, Intent intent, Bundle metaData,
ComponentInfo componentInfo) {
// Skip loading tile if the component is tagged primary_profile_only but not running on
// the current user.
if (user.getIdentifier() != ActivityManager.getCurrentUser()
&& Tile.isPrimaryProfileOnly(componentInfo.metaData)) {
Log.w(LOG_TAG, "Found " + componentInfo.name + " for intent "
+ intent + " is primary profile only, skip loading tile for uid "
+ user.getIdentifier());
return;
}
String categoryKey = defaultCategory;
// Load category
if ((metaData == null || !metaData.containsKey(EXTRA_CATEGORY_KEY))
&& categoryKey == null) {
Log.w(LOG_TAG, "Found " + componentInfo.name + " for intent "
+ intent + " missing metadata "
+ (metaData == null ? "" : EXTRA_CATEGORY_KEY));
return;
} else {
categoryKey = metaData.getString(EXTRA_CATEGORY_KEY);
}
final boolean isProvider = componentInfo instanceof ProviderInfo;
final Pair<String, String> key = isProvider
? new Pair<>(((ProviderInfo) componentInfo).authority,
metaData.getString(META_DATA_PREFERENCE_KEYHINT))
: new Pair<>(componentInfo.packageName, componentInfo.name);
Tile tile = addedCache.get(key);
if (tile == null) {
tile = isProvider
? new ProviderTile((ProviderInfo) componentInfo, categoryKey, metaData)
: new ActivityTile((ActivityInfo) componentInfo, categoryKey);
addedCache.put(key, tile);
} else {
tile.setMetaData(metaData);
}
if (!tile.userHandle.contains(user)) {
tile.userHandle.add(user);
}
if (metaData.containsKey(META_DATA_PREFERENCE_PENDING_INTENT)) {
tile.pendingIntentMap.put(
user, metaData.getParcelable(META_DATA_PREFERENCE_PENDING_INTENT));
}
if (!outTiles.contains(tile)) {
outTiles.add(tile);
}
}
/** Returns the entry data of the key specified from the provider */
// TODO(b/144732809): rearrange methods by access level modifiers
static Bundle getEntryDataFromProvider(Context context, String authority, String key) {
final Map<String, IContentProvider> providerMap = new ArrayMap<>();
final Uri uri = buildUri(authority, EntriesProvider.METHOD_GET_ENTRY_DATA, key);
Bundle result = getBundleFromUri(context, uri, providerMap, null /* bundle */);
if (result == null) {
Uri fallbackUri = buildUri(authority, EntriesProvider.METHOD_GET_SWITCH_DATA, key);
result = getBundleFromUri(context, fallbackUri, providerMap, null /* bundle */);
}
return result;
}
/** Returns all entry data from the provider */
private static List<Bundle> getEntryDataFromProvider(Context context, String authority) {
final Map<String, IContentProvider> providerMap = new ArrayMap<>();
final Uri uri = buildUri(authority, EntriesProvider.METHOD_GET_ENTRY_DATA);
final Bundle result = getBundleFromUri(context, uri, providerMap, null /* bundle */);
if (result != null) {
return result.getParcelableArrayList(EntriesProvider.EXTRA_ENTRY_DATA);
} else {
Uri fallbackUri = buildUri(authority, EntriesProvider.METHOD_GET_SWITCH_DATA);
Bundle fallbackResult =
getBundleFromUri(context, fallbackUri, providerMap, null /* bundle */);
return fallbackResult != null
? fallbackResult.getParcelableArrayList(EntriesProvider.EXTRA_SWITCH_DATA)
: null;
}
}
/**
* Returns the complete uri from the meta data key of the tile.
*
* A complete uri should contain at least one path segment and be one of the following types:
* content://authority/method
* content://authority/method/key
*
* If the uri from the tile is not complete, build a uri by the default method and the
* preference key.
*
* @param tile Tile which contains meta data
* @param metaDataKey Key mapping to the uri in meta data
* @param defaultMethod Method to be attached to the uri by default if it has no path segment
* @return Uri associated with the key
*/
public static Uri getCompleteUri(Tile tile, String metaDataKey, String defaultMethod) {
final String uriString = tile.getMetaData().getString(metaDataKey);
if (TextUtils.isEmpty(uriString)) {
return null;
}
final Uri uri = Uri.parse(uriString);
final List<String> pathSegments = uri.getPathSegments();
if (pathSegments != null && !pathSegments.isEmpty()) {
return uri;
}
final String key = tile.getMetaData().getString(META_DATA_PREFERENCE_KEYHINT);
if (TextUtils.isEmpty(key)) {
Log.w(LOG_TAG, "Please specify the meta-data " + META_DATA_PREFERENCE_KEYHINT
+ " in AndroidManifest.xml for " + uriString);
return buildUri(uri.getAuthority(), defaultMethod);
}
return buildUri(uri.getAuthority(), defaultMethod, key);
}
static Uri buildUri(String authority, String method, String key) {
return new Uri.Builder()
.scheme(ContentResolver.SCHEME_CONTENT)
.authority(authority)
.appendPath(method)
.appendPath(key)
.build();
}
private static Uri buildUri(String authority, String method) {
return new Uri.Builder()
.scheme(ContentResolver.SCHEME_CONTENT)
.authority(authority)
.appendPath(method)
.build();
}
/**
* Gets the icon package name and resource id from content provider.
*
* @param context context
* @param packageName package name of the target activity
* @param uri URI for the content provider
* @param providerMap Maps URI authorities to providers
* @return package name and resource id of the icon specified
*/
public static Pair<String, Integer> getIconFromUri(Context context, String packageName,
Uri uri, Map<String, IContentProvider> providerMap) {
final Bundle bundle = getBundleFromUri(context, uri, providerMap, null /* bundle */);
if (bundle == null) {
return null;
}
final String iconPackageName = bundle.getString(EXTRA_PREFERENCE_ICON_PACKAGE);
if (TextUtils.isEmpty(iconPackageName)) {
return null;
}
int resId = bundle.getInt(META_DATA_PREFERENCE_ICON, 0);
if (resId == 0) {
return null;
}
// Icon can either come from the target package or from the Settings app.
if (iconPackageName.equals(packageName)
|| iconPackageName.equals(context.getPackageName())) {
return Pair.create(iconPackageName, resId);
}
return null;
}
/**
* Gets text associated with the input key from the content provider.
*
* @param context context
* @param uri URI for the content provider
* @param providerMap Maps URI authorities to providers
* @param key Key mapping to the text in bundle returned by the content provider
* @return Text associated with the key, if returned by the content provider
*/
public static String getTextFromUri(Context context, Uri uri,
Map<String, IContentProvider> providerMap, String key) {
final Bundle bundle = getBundleFromUri(context, uri, providerMap, null /* bundle */);
return (bundle != null) ? bundle.getString(key) : null;
}
/**
* Gets boolean associated with the input key from the content provider.
*
* @param context context
* @param uri URI for the content provider
* @param providerMap Maps URI authorities to providers
* @param key Key mapping to the text in bundle returned by the content provider
* @return Boolean associated with the key, if returned by the content provider
*/
public static boolean getBooleanFromUri(Context context, Uri uri,
Map<String, IContentProvider> providerMap, String key) {
final Bundle bundle = getBundleFromUri(context, uri, providerMap, null /* bundle */);
return (bundle != null) ? bundle.getBoolean(key) : false;
}
/**
* Puts boolean associated with the input key to the content provider.
*
* @param context context
* @param uri URI for the content provider
* @param providerMap Maps URI authorities to providers
* @param key Key mapping to the text in bundle returned by the content provider
* @param value Boolean associated with the key
* @return Bundle associated with the action, if returned by the content provider
*/
public static Bundle putBooleanToUriAndGetResult(Context context, Uri uri,
Map<String, IContentProvider> providerMap, String key, boolean value) {
final Bundle bundle = new Bundle();
bundle.putBoolean(key, value);
return getBundleFromUri(context, uri, providerMap, bundle);
}
private static Bundle getBundleFromUri(Context context, Uri uri,
Map<String, IContentProvider> providerMap, Bundle bundle) {
final Pair<String, String> args = getMethodAndKey(uri);
if (args == null) {
return null;
}
final String method = args.first;
final String key = args.second;
if (TextUtils.isEmpty(method)) {
return null;
}
final IContentProvider provider = getProviderFromUri(context, uri, providerMap);
if (provider == null) {
return null;
}
if (!TextUtils.isEmpty(key)) {
if (bundle == null) {
bundle = new Bundle();
}
bundle.putString(META_DATA_PREFERENCE_KEYHINT, key);
}
try {
return provider.call(context.getAttributionSource(),
uri.getAuthority(), method, uri.toString(), bundle);
} catch (RemoteException e) {
return null;
}
}
private static IContentProvider getProviderFromUri(Context context, Uri uri,
Map<String, IContentProvider> providerMap) {
if (uri == null) {
return null;
}
final String authority = uri.getAuthority();
if (TextUtils.isEmpty(authority)) {
return null;
}
if (!providerMap.containsKey(authority)) {
providerMap.put(authority, context.getContentResolver().acquireUnstableProvider(uri));
}
return providerMap.get(authority);
}
/** Returns method and key of the complete uri. */
private static Pair<String, String> getMethodAndKey(Uri uri) {
if (uri == null) {
return null;
}
final List<String> pathSegments = uri.getPathSegments();
if (pathSegments == null || pathSegments.isEmpty()) {
return null;
}
final String method = pathSegments.get(0);
final String key = pathSegments.size() > 1 ? pathSegments.get(1) : null;
return Pair.create(method, key);
}
}