diff options
10 files changed, 1419 insertions, 54 deletions
diff --git a/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/ActivityTile.java b/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/ActivityTile.java index 06a4a45b4853..8cd33a5b3f6c 100644 --- a/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/ActivityTile.java +++ b/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/ActivityTile.java @@ -53,15 +53,6 @@ public class ActivityTile extends Tile { } @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 ComponentInfo getComponentInfo(Context context) { if (mComponentInfo == null) { final PackageManager pm = context.getApplicationContext().getPackageManager(); @@ -78,4 +69,18 @@ public class ActivityTile extends Tile { } 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; + } } diff --git a/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/ProviderTile.java b/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/ProviderTile.java new file mode 100644 index 000000000000..b2ba5deee731 --- /dev/null +++ b/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/ProviderTile.java @@ -0,0 +1,105 @@ +/* + * 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(ComponentInfo info, String category, Bundle metaData) { + super(info, category); + setMetaData(metaData); + mAuthority = ((ProviderInfo) 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.getSwitchDataFromProvider(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; + } +} diff --git a/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/SwitchController.java b/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/SwitchController.java new file mode 100644 index 000000000000..e48da8608780 --- /dev/null +++ b/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/SwitchController.java @@ -0,0 +1,256 @@ +/* + * 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.SwitchesProvider.METHOD_GET_DYNAMIC_SUMMARY; +import static com.android.settingslib.drawer.SwitchesProvider.METHOD_GET_DYNAMIC_TITLE; +import static com.android.settingslib.drawer.SwitchesProvider.METHOD_IS_CHECKED; +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_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.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 SwitchController { + + private String mAuthority; + + /** + * Returns the key for this switch. + */ + public abstract String getSwitchKey(); + + /** + * Returns the {@link MetaData} for this switch. + */ + protected abstract MetaData getMetaData(); + + /** + * 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); + + /** + * 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); + } + } + + /** + * Notify registered observers that checked state was updated and attempt to sync changes. + */ + public void notifyCheckedChanged(Context context) { + notifyChanged(context, METHOD_IS_CHECKED); + } + + void setAuthority(String authority) { + mAuthority = authority; + } + + Bundle getBundle() { + final MetaData metaData = getMetaData(); + if (metaData == null) { + throw new IllegalArgumentException("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, getSwitchKey()); + bundle.putString(META_DATA_PREFERENCE_SWITCH_URI, uriString); + 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); + } + return bundle; + } + + private void notifyChanged(Context context, String method) { + final Uri uri = TileUtils.buildUri(mAuthority, method, getSwitchKey()); + 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; + + /** + * @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; + } + + private 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); + } + return bundle; + } + } +} diff --git a/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/SwitchesProvider.java b/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/SwitchesProvider.java new file mode 100644 index 000000000000..a05c7d5d3b51 --- /dev/null +++ b/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/SwitchesProvider.java @@ -0,0 +1,186 @@ +/* + * 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 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 switches to Settings. + */ +public abstract class SwitchesProvider extends ContentProvider { + private static final String TAG = "SwitchesProvider"; + + public static final String METHOD_GET_SWITCH_DATA = "getSwitchData"; + 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"; + + public static final String EXTRA_SWITCH_DATA = "switch_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"; + + private String mAuthority; + private final Map<String, SwitchController> mControllerMap = new LinkedHashMap<>(); + private final List<Bundle> mSwitchList = new ArrayList<>(); + + /** + * Get a list of {@link SwitchController} for this provider. + */ + protected abstract List<SwitchController> createSwitchControllers(); + + @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<SwitchController> controllers = createSwitchControllers(); + if (controllers == null || controllers.isEmpty()) { + throw new IllegalArgumentException(); + } + + controllers.forEach(controller -> { + final String key = controller.getSwitchKey(); + if (TextUtils.isEmpty(key)) { + throw new NullPointerException("Switch key cannot be null: " + + controller.getClass().getSimpleName()); + } else if (mControllerMap.containsKey(key)) { + throw new IllegalArgumentException("Switch key " + key + " is duplicated by: " + + controller.getClass().getSimpleName()); + } + + controller.setAuthority(mAuthority); + mControllerMap.put(key, controller); + mSwitchList.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)) { + if (METHOD_GET_SWITCH_DATA.equals(method)) { + bundle.putParcelableList(EXTRA_SWITCH_DATA, mSwitchList); + return bundle; + } + return null; + } + + final SwitchController controller = mControllerMap.get(key); + if (controller == null) { + return null; + } + + switch (method) { + case METHOD_GET_SWITCH_DATA: + return controller.getBundle(); + 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: + bundle.putBoolean(EXTRA_SWITCH_CHECKED_STATE, controller.isChecked()); + return bundle; + case METHOD_ON_CHECKED_CHANGED: + return onCheckedChanged(extras.getBoolean(EXTRA_SWITCH_CHECKED_STATE), controller); + } + return null; + } + + private Bundle onCheckedChanged(boolean checked, SwitchController controller) { + final boolean success = controller.onCheckedChanged(checked); + final Bundle bundle = new Bundle(); + bundle.putBoolean(EXTRA_SWITCH_SET_CHECKED_ERROR, !success); + if (success) { + if (controller instanceof DynamicSummary) { + controller.notifySummaryChanged(getContext()); + } + } else { + bundle.putString(EXTRA_SWITCH_SET_CHECKED_ERROR_MESSAGE, + controller.getErrorMessage(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(); + } +} diff --git a/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/Tile.java b/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/Tile.java index 722f734b8021..1e4c7cac4404 100644 --- a/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/Tile.java +++ b/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/Tile.java @@ -23,6 +23,7 @@ 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_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; @@ -79,6 +80,7 @@ public abstract class Tile implements Parcelable { } Tile(Parcel in) { + final boolean isProviderTile = in.readBoolean(); mComponentPackage = in.readString(); mComponentName = in.readString(); mIntent = new Intent().setClassName(mComponentPackage, mComponentName); @@ -97,6 +99,7 @@ public abstract class Tile implements Parcelable { @Override public void writeToParcel(Parcel dest, int flags) { + dest.writeBoolean(this instanceof ProviderTile); dest.writeString(mComponentPackage); dest.writeString(mComponentName); final int size = userHandle.size(); @@ -118,6 +121,12 @@ public abstract class Tile implements Parcelable { */ 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; } @@ -164,6 +173,13 @@ public abstract class Tile implements Parcelable { } /** + * 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) { @@ -195,8 +211,6 @@ public abstract class Tile implements Parcelable { return title; } - protected abstract CharSequence getComponentLabel(Context context); - /** * Overrides the summary. This can happen when injected tile wants to provide dynamic summary. */ @@ -293,7 +307,7 @@ public abstract class Tile implements Parcelable { // 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 = componentInfo.icon; + iconResId = getComponentIcon(componentInfo); } } if (iconResId != 0) { @@ -345,11 +359,12 @@ public abstract class Tile implements Parcelable { } } - protected abstract ComponentInfo getComponentInfo(Context context); - public static final Creator<Tile> CREATOR = new Creator<Tile>() { public Tile createFromParcel(Parcel source) { - return new ActivityTile(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) { @@ -358,7 +373,7 @@ public abstract class Tile implements Parcelable { }; /** - * Check whether title is only have primary profile + * Check whether tile only has primary profile. */ public boolean isPrimaryProfileOnly() { String profile = mMetaData != null diff --git a/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/TileUtils.java b/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/TileUtils.java index b9daf7fcab4e..71ffff780b21 100644 --- a/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/TileUtils.java +++ b/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/TileUtils.java @@ -16,12 +16,14 @@ 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; @@ -30,6 +32,7 @@ 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; @@ -96,13 +99,12 @@ public class TileUtils { * {@link #EXTRA_SETTINGS_ACTION} * The value must be from {@link CategoryKey}. */ - private static final String EXTRA_CATEGORY_KEY = "com.android.settings.category"; + 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. */ - private static final String EXTRA_PREFERENCE_ICON_PACKAGE = - "com.android.settings.icon_package"; + 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 @@ -189,6 +191,17 @@ public class TileUtils { "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"; + + /** * Value for {@link #META_DATA_KEY_PROFILE}. When the device has a managed profile, * the app will always be run in the primary profile. * @@ -279,6 +292,7 @@ public class TileUtils { intent.setPackage(SETTING_PKG); } getActivityTiles(context, user, addedCache, defaultCategory, outTiles, intent); + getProviderTiles(context, user, addedCache, defaultCategory, outTiles, intent); } private static void getActivityTiles(Context context, @@ -298,6 +312,30 @@ public class TileUtils { } } + private static void getProviderTiles(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> switchData = getSwitchDataFromProvider(context, + providerInfo.authority); + if (switchData == null || switchData.isEmpty()) { + continue; + } + for (Bundle metaData : switchData) { + getTile(user, addedCache, defaultCategory, outTiles, intent, metaData, + providerInfo); + } + } + } + private static void getTile(UserHandle user, Map<Pair<String, String>, Tile> addedCache, String defaultCategory, List<Tile> outTiles, Intent intent, Bundle metaData, ComponentInfo componentInfo) { @@ -313,10 +351,16 @@ public class TileUtils { categoryKey = metaData.getString(EXTRA_CATEGORY_KEY); } - final Pair<String, String> key = new Pair<>(componentInfo.packageName, componentInfo.name); + 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 = new ActivityTile(componentInfo, categoryKey); + tile = isProvider + ? new ProviderTile(componentInfo, categoryKey, metaData) + : new ActivityTile(componentInfo, categoryKey); addedCache.put(key, tile); } else { tile.setMetaData(metaData); @@ -330,20 +374,75 @@ public class TileUtils { } } + /** Returns the switch data of the key specified from the provider */ + // TODO(b/144732809): rearrange methods by access level modifiers + static Bundle getSwitchDataFromProvider(Context context, String authority, String key) { + final Map<String, IContentProvider> providerMap = new ArrayMap<>(); + final Uri uri = buildUri(authority, SwitchesProvider.METHOD_GET_SWITCH_DATA, key); + return getBundleFromUri(context, uri, providerMap, null /* bundle */); + } + + /** Returns all switch data from the provider */ + private static List<Bundle> getSwitchDataFromProvider(Context context, String authority) { + final Map<String, IContentProvider> providerMap = new ArrayMap<>(); + final Uri uri = buildUri(authority, SwitchesProvider.METHOD_GET_SWITCH_DATA); + final Bundle result = getBundleFromUri(context, uri, providerMap, null /* bundle */); + return result != null + ? result.getParcelableArrayList(SwitchesProvider.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) { + public static Uri getCompleteUri(Tile tile, String metaDataKey, String defaultMethod) { final String uriString = tile.getMetaData().getString(metaDataKey); if (TextUtils.isEmpty(uriString)) { return null; } - return Uri.parse(uriString); + 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(); } /** @@ -357,7 +456,7 @@ public class TileUtils { */ public static Pair<String, Integer> getIconFromUri(Context context, String packageName, Uri uri, Map<String, IContentProvider> providerMap) { - final Bundle bundle = getBundleFromUri(context, uri, providerMap, null); + final Bundle bundle = getBundleFromUri(context, uri, providerMap, null /* bundle */); if (bundle == null) { return null; } @@ -388,16 +487,50 @@ public class TileUtils { */ public static String getTextFromUri(Context context, Uri uri, Map<String, IContentProvider> providerMap, String key) { - final Bundle bundle = getBundleFromUri(context, uri, providerMap, null); + 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 putBooleanToUri(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) { - if (uri == null) { + final Pair<String, String> args = getMethodAndKey(uri); + if (args == null) { return null; } - final String method = getMethodFromUri(uri); + final String method = args.first; + final String key = args.second; if (TextUtils.isEmpty(method)) { return null; } @@ -405,6 +538,12 @@ public class TileUtils { 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.getPackageName(), context.getFeatureId(), uri.getAuthority(), method, uri.toString(), bundle); @@ -428,15 +567,17 @@ public class TileUtils { return providerMap.get(authority); } - /** Returns the first path segment of the uri if it exists as the method, otherwise null. */ - private static String getMethodFromUri(Uri uri) { + /** Returns method and key of the complete uri. */ + private static Pair<String, String> getMethodAndKey(Uri uri) { if (uri == null) { return null; } - List<String> pathSegments = uri.getPathSegments(); - if ((pathSegments == null) || pathSegments.isEmpty()) { + final List<String> pathSegments = uri.getPathSegments(); + if (pathSegments == null || pathSegments.isEmpty()) { return null; } - return pathSegments.get(0); + final String method = pathSegments.get(0); + final String key = pathSegments.size() > 1 ? pathSegments.get(1) : null; + return Pair.create(method, key); } } diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/drawer/TileTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/drawer/ActivityTileTest.java index 2664ecd17dd3..4f8ecf8f8823 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/drawer/TileTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/drawer/ActivityTileTest.java @@ -1,3 +1,18 @@ +/* + * 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; @@ -25,7 +40,7 @@ import org.robolectric.shadow.api.Shadow; import org.robolectric.shadows.ShadowPackageManager; @RunWith(RobolectricTestRunner.class) -public class TileTest { +public class ActivityTileTest { private Context mContext; private ActivityInfo mActivityInfo; diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/drawer/ProviderTileTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/drawer/ProviderTileTest.java new file mode 100644 index 000000000000..abfb407d749e --- /dev/null +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/drawer/ProviderTileTest.java @@ -0,0 +1,191 @@ +/* + * 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_KEYHINT; +import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_TITLE; +import static com.android.settingslib.drawer.TileUtils.PROFILE_ALL; +import static com.android.settingslib.drawer.TileUtils.PROFILE_PRIMARY; + +import static com.google.common.truth.Truth.assertThat; + +import android.content.Context; +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.ProviderInfo; +import android.content.pm.ResolveInfo; +import android.os.Bundle; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; +import org.robolectric.annotation.Implementation; +import org.robolectric.annotation.Implements; +import org.robolectric.shadow.api.Shadow; +import org.robolectric.shadows.ShadowPackageManager; + +@RunWith(RobolectricTestRunner.class) +public class ProviderTileTest { + + @Rule + public final ExpectedException thrown = ExpectedException.none(); + + private Context mContext; + private ProviderInfo mProviderInfo; + private Bundle mMetaData; + private Tile mTile; + + @Before + public void setUp() { + mContext = RuntimeEnvironment.application; + mProviderInfo = new ProviderInfo(); + mProviderInfo.applicationInfo = new ApplicationInfo(); + mProviderInfo.packageName = mContext.getPackageName(); + mProviderInfo.name = "abc"; + mProviderInfo.authority = "authority"; + mMetaData = new Bundle(); + mMetaData.putString(META_DATA_PREFERENCE_KEYHINT, "key"); + mMetaData.putString(META_DATA_PREFERENCE_TITLE, "title"); + mMetaData.putInt(META_DATA_PREFERENCE_ICON, com.android.internal.R.drawable.ic_plus); + mTile = new ProviderTile(mProviderInfo, "category", mMetaData); + } + + @Test + public void isPrimaryProfileOnly_profilePrimary_shouldReturnTrue() { + mMetaData.putString(META_DATA_KEY_PROFILE, PROFILE_PRIMARY); + assertThat(mTile.isPrimaryProfileOnly()).isTrue(); + } + + @Test + public void isPrimaryProfileOnly_profileAll_shouldReturnFalse() { + mMetaData.putString(META_DATA_KEY_PROFILE, PROFILE_ALL); + assertThat(mTile.isPrimaryProfileOnly()).isFalse(); + } + + @Test + public void isPrimaryProfileOnly_noExplicitValue_shouldReturnFalse() { + assertThat(mTile.isPrimaryProfileOnly()).isFalse(); + } + + @Test + public void getIcon_noContextOrMetadata_shouldThrowNullPointerException() { + thrown.expect(NullPointerException.class); + + final Tile tile = new ProviderTile(mProviderInfo, "category", null); + } + + @Test + public void getIcon_hasIconMetadata_returnIcon() { + mMetaData.putInt(META_DATA_PREFERENCE_ICON, android.R.drawable.ic_info); + + assertThat(mTile.getIcon(RuntimeEnvironment.application).getResId()) + .isEqualTo(android.R.drawable.ic_info); + } + + @Test + public void isIconTintable_hasMetadata_shouldReturnIconTintableMetadata() { + final Tile tile = new ProviderTile(mProviderInfo, "category", mMetaData); + + mMetaData.putBoolean(TileUtils.META_DATA_PREFERENCE_ICON_TINTABLE, false); + assertThat(tile.isIconTintable(RuntimeEnvironment.application)).isFalse(); + + mMetaData.putBoolean(TileUtils.META_DATA_PREFERENCE_ICON_TINTABLE, true); + assertThat(tile.isIconTintable(RuntimeEnvironment.application)).isTrue(); + } + + @Test + public void isIconTintable_noIcon_shouldReturnFalse() { + final Tile tile = new ProviderTile(mProviderInfo, "category", mMetaData); + + assertThat(tile.isIconTintable(RuntimeEnvironment.application)).isFalse(); + } + + @Test + public void isIconTintable_noTintableMetadata_shouldReturnFalse() { + final Tile tile = new ProviderTile(mProviderInfo, "category", mMetaData); + mMetaData.putInt(META_DATA_PREFERENCE_ICON, android.R.drawable.ic_info); + + assertThat(tile.isIconTintable(RuntimeEnvironment.application)).isFalse(); + } + + @Test + public void getPriority_noMetadata_return0() { + final Tile tile = new ProviderTile(mProviderInfo, "category", mMetaData); + + assertThat(tile.getOrder()).isEqualTo(0); + } + + @Test + public void getPriority_badMetadata_return0() { + mMetaData.putString(META_DATA_KEY_ORDER, "1"); + + final Tile tile = new ProviderTile(mProviderInfo, "category", mMetaData); + + assertThat(tile.getOrder()).isEqualTo(0); + } + + @Test + public void getPriority_validMetadata_returnMetadataValue() { + mMetaData.putInt(META_DATA_KEY_ORDER, 1); + + final Tile tile = new ProviderTile(mProviderInfo, "category", mMetaData); + + assertThat(tile.getOrder()).isEqualTo(1); + } + + @Test + @Config(shadows = ShadowTileUtils.class) + public void getTitle_shouldEnsureMetadataNotStale() { + final ResolveInfo info = new ResolveInfo(); + info.providerInfo = mProviderInfo; + final ShadowPackageManager spm = Shadow.extract(mContext.getPackageManager()); + spm.addResolveInfoForIntent( + new Intent().setClassName(mProviderInfo.packageName, mProviderInfo.name), info); + ShadowTileUtils.setMetaData(mMetaData); + + final Tile tile = new ProviderTile(mProviderInfo, "category", mMetaData); + final long staleTimeStamp = -10000; + tile.mLastUpdateTime = staleTimeStamp; + + tile.getTitle(RuntimeEnvironment.application); + + assertThat(tile.mLastUpdateTime).isNotEqualTo(staleTimeStamp); + } + + @Implements(TileUtils.class) + private static class ShadowTileUtils { + + private static Bundle sMetaData; + + @Implementation + protected static Bundle getSwitchDataFromProvider(Context context, String authority, + String key) { + return sMetaData; + } + + private static void setMetaData(Bundle metaData) { + sMetaData = metaData; + } + } +} diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/drawer/SwitchesProviderTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/drawer/SwitchesProviderTest.java new file mode 100644 index 000000000000..27b3697f54ea --- /dev/null +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/drawer/SwitchesProviderTest.java @@ -0,0 +1,389 @@ +/* + * 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.SwitchesProvider.EXTRA_SWITCH_CHECKED_STATE; +import static com.android.settingslib.drawer.SwitchesProvider.EXTRA_SWITCH_DATA; +import static com.android.settingslib.drawer.SwitchesProvider.EXTRA_SWITCH_SET_CHECKED_ERROR; +import static com.android.settingslib.drawer.SwitchesProvider.EXTRA_SWITCH_SET_CHECKED_ERROR_MESSAGE; +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.SwitchesProvider.METHOD_GET_PROVIDER_ICON; +import static com.android.settingslib.drawer.SwitchesProvider.METHOD_GET_SWITCH_DATA; +import static com.android.settingslib.drawer.SwitchesProvider.METHOD_IS_CHECKED; +import static com.android.settingslib.drawer.SwitchesProvider.METHOD_ON_CHECKED_CHANGED; +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 static com.google.common.truth.Truth.assertThat; + +import android.content.Context; +import android.content.pm.ProviderInfo; +import android.os.Bundle; + +import com.android.settingslib.drawer.SwitchController.MetaData; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; + +import java.util.ArrayList; +import java.util.List; + +@RunWith(RobolectricTestRunner.class) +public class SwitchesProviderTest { + + @Rule + public final ExpectedException thrown = ExpectedException.none(); + + private Context mContext; + private ProviderInfo mProviderInfo; + + private TestSwitchesProvider mSwitchesProvider; + + @Before + public void setUp() { + mContext = RuntimeEnvironment.application; + mSwitchesProvider = new TestSwitchesProvider(); + mProviderInfo = new ProviderInfo(); + mProviderInfo.authority = "auth"; + } + + @Test + public void attachInfo_noController_shouldThrowIllegalArgumentException() { + thrown.expect(IllegalArgumentException.class); + + mSwitchesProvider.attachInfo(mContext, mProviderInfo); + } + + @Test + public void attachInfo_NoSwitchKeyInController_shouldThrowNullPointerException() { + thrown.expect(NullPointerException.class); + final TestSwitchController controller = new TestSwitchController(); + mSwitchesProvider.addSwitchController(controller); + + mSwitchesProvider.attachInfo(mContext, mProviderInfo); + } + + @Test + public void attachInfo_NoMetaDataInController_shouldThrowIllegalArgumentException() { + thrown.expect(IllegalArgumentException.class); + final TestSwitchController controller = new TestSwitchController(); + controller.setKey("123"); + mSwitchesProvider.addSwitchController(controller); + + mSwitchesProvider.attachInfo(mContext, mProviderInfo); + } + + @Test + public void attachInfo_duplicateSwitchKey_shouldThrowIllegalArgumentException() { + thrown.expect(IllegalArgumentException.class); + final TestSwitchController controller1 = new TestSwitchController(); + final TestSwitchController controller2 = new TestSwitchController(); + controller1.setKey("123"); + controller2.setKey("123"); + controller1.setMetaData(new MetaData("category")); + controller2.setMetaData(new MetaData("category")); + mSwitchesProvider.addSwitchController(controller1); + mSwitchesProvider.addSwitchController(controller2); + + mSwitchesProvider.attachInfo(mContext, mProviderInfo); + } + + @Test + public void attachInfo_hasDifferentControllers_shouldNotThrowException() { + final TestSwitchController controller1 = new TestSwitchController(); + final TestSwitchController controller2 = new TestSwitchController(); + controller1.setKey("123"); + controller2.setKey("456"); + controller1.setMetaData(new MetaData("category")); + controller2.setMetaData(new MetaData("category")); + mSwitchesProvider.addSwitchController(controller1); + mSwitchesProvider.addSwitchController(controller2); + + mSwitchesProvider.attachInfo(mContext, mProviderInfo); + } + + @Test + public void getSwitchData_shouldReturnDataList() { + final TestSwitchController controller = new TestSwitchController(); + controller.setKey("123"); + controller.setMetaData(new MetaData("category")); + mSwitchesProvider.addSwitchController(controller); + mSwitchesProvider.attachInfo(mContext, mProviderInfo); + + final Bundle switchData = mSwitchesProvider.call(METHOD_GET_SWITCH_DATA, "uri" , + null /* extras*/); + + final ArrayList<Bundle> dataList = switchData.getParcelableArrayList(EXTRA_SWITCH_DATA); + assertThat(dataList).hasSize(1); + assertThat(dataList.get(0).getString(META_DATA_PREFERENCE_KEYHINT)).isEqualTo("123"); + } + + @Test + public void getSwitchDataByKey_shouldReturnData() { + final Bundle extras = new Bundle(); + extras.putString(META_DATA_PREFERENCE_KEYHINT, "123"); + final TestSwitchController controller = new TestSwitchController(); + controller.setKey("123"); + controller.setMetaData(new MetaData("category")); + mSwitchesProvider.addSwitchController(controller); + mSwitchesProvider.attachInfo(mContext, mProviderInfo); + + final Bundle switchData = mSwitchesProvider.call(METHOD_GET_SWITCH_DATA, "uri" , extras); + + assertThat(switchData.getString(META_DATA_PREFERENCE_KEYHINT)).isEqualTo("123"); + } + + @Test + public void isChecked_shouldReturnCheckedState() { + final Bundle extras = new Bundle(); + extras.putString(META_DATA_PREFERENCE_KEYHINT, "123"); + final TestSwitchController controller = new TestSwitchController(); + controller.setKey("123"); + controller.setMetaData(new MetaData("category")); + mSwitchesProvider.addSwitchController(controller); + mSwitchesProvider.attachInfo(mContext, mProviderInfo); + + controller.setChecked(true); + Bundle result = mSwitchesProvider.call(METHOD_IS_CHECKED, "uri" , extras); + + assertThat(result.getBoolean(EXTRA_SWITCH_CHECKED_STATE)).isTrue(); + + controller.setChecked(false); + result = mSwitchesProvider.call(METHOD_IS_CHECKED, "uri" , extras); + + assertThat(result.getBoolean(EXTRA_SWITCH_CHECKED_STATE)).isFalse(); + } + + @Test + public void getProviderIcon_noImplementInterface_shouldReturnNull() { + final Bundle extras = new Bundle(); + extras.putString(META_DATA_PREFERENCE_KEYHINT, "123"); + final TestSwitchController controller = new TestSwitchController(); + controller.setKey("123"); + controller.setMetaData(new MetaData("category")); + mSwitchesProvider.addSwitchController(controller); + mSwitchesProvider.attachInfo(mContext, mProviderInfo); + + final Bundle iconBundle = mSwitchesProvider.call(METHOD_GET_PROVIDER_ICON, "uri" , extras); + + assertThat(iconBundle).isNull(); + } + + @Test + public void getProviderIcon_implementInterface_shouldReturnIcon() { + final Bundle extras = new Bundle(); + extras.putString(META_DATA_PREFERENCE_KEYHINT, "123"); + final TestSwitchController controller = new TestDynamicSwitchController(); + controller.setKey("123"); + controller.setMetaData(new MetaData("category")); + mSwitchesProvider.addSwitchController(controller); + mSwitchesProvider.attachInfo(mContext, mProviderInfo); + + final Bundle iconBundle = mSwitchesProvider.call(METHOD_GET_PROVIDER_ICON, "uri" , extras); + + assertThat(iconBundle).isEqualTo(TestDynamicSwitchController.ICON_BUNDLE); + } + + @Test + public void getDynamicTitle_noImplementInterface_shouldReturnNull() { + final Bundle extras = new Bundle(); + extras.putString(META_DATA_PREFERENCE_KEYHINT, "123"); + final TestSwitchController controller = new TestSwitchController(); + controller.setKey("123"); + controller.setMetaData(new MetaData("category")); + mSwitchesProvider.addSwitchController(controller); + mSwitchesProvider.attachInfo(mContext, mProviderInfo); + + final Bundle result = mSwitchesProvider.call(METHOD_GET_DYNAMIC_TITLE, "uri" , extras); + + assertThat(result).isNull(); + } + + @Test + public void getDynamicTitle_implementInterface_shouldReturnTitle() { + final Bundle extras = new Bundle(); + extras.putString(META_DATA_PREFERENCE_KEYHINT, "123"); + final TestSwitchController controller = new TestDynamicSwitchController(); + controller.setKey("123"); + controller.setMetaData(new MetaData("category")); + mSwitchesProvider.addSwitchController(controller); + mSwitchesProvider.attachInfo(mContext, mProviderInfo); + + final Bundle result = mSwitchesProvider.call(METHOD_GET_DYNAMIC_TITLE, "uri" , extras); + + assertThat(result.getString(META_DATA_PREFERENCE_TITLE)) + .isEqualTo(TestDynamicSwitchController.TITLE); + } + + @Test + public void getDynamicSummary_noImplementInterface_shouldReturnNull() { + final Bundle extras = new Bundle(); + extras.putString(META_DATA_PREFERENCE_KEYHINT, "123"); + final TestSwitchController controller = new TestSwitchController(); + controller.setKey("123"); + controller.setMetaData(new MetaData("category")); + mSwitchesProvider.addSwitchController(controller); + mSwitchesProvider.attachInfo(mContext, mProviderInfo); + + final Bundle result = mSwitchesProvider.call(METHOD_GET_DYNAMIC_SUMMARY, "uri" , extras); + + assertThat(result).isNull(); + } + + @Test + public void getDynamicSummary_implementInterface_shouldReturnSummary() { + final Bundle extras = new Bundle(); + extras.putString(META_DATA_PREFERENCE_KEYHINT, "123"); + final TestSwitchController controller = new TestDynamicSwitchController(); + controller.setKey("123"); + controller.setMetaData(new MetaData("category")); + mSwitchesProvider.addSwitchController(controller); + mSwitchesProvider.attachInfo(mContext, mProviderInfo); + + final Bundle result = mSwitchesProvider.call(METHOD_GET_DYNAMIC_SUMMARY, "uri" , extras); + + assertThat(result.getString(META_DATA_PREFERENCE_SUMMARY)) + .isEqualTo(TestDynamicSwitchController.SUMMARY); + } + + @Test + public void onCheckedChangedSuccess_shouldReturnNoError() { + final Bundle extras = new Bundle(); + extras.putString(META_DATA_PREFERENCE_KEYHINT, "123"); + final TestSwitchController controller = new TestSwitchController(); + controller.setKey("123"); + controller.setMetaData(new MetaData("category")); + mSwitchesProvider.addSwitchController(controller); + mSwitchesProvider.attachInfo(mContext, mProviderInfo); + + final Bundle result = mSwitchesProvider.call(METHOD_ON_CHECKED_CHANGED, "uri" , extras); + + assertThat(result.getBoolean(EXTRA_SWITCH_SET_CHECKED_ERROR)).isFalse(); + } + + @Test + public void onCheckedChangedFailed_shouldReturnErrorMessage() { + final Bundle extras = new Bundle(); + extras.putString(META_DATA_PREFERENCE_KEYHINT, "123"); + final TestSwitchController controller = new TestSwitchController(); + controller.setKey("123"); + controller.setMetaData(new MetaData("category")); + controller.setErrorMessage("error"); + mSwitchesProvider.addSwitchController(controller); + mSwitchesProvider.attachInfo(mContext, mProviderInfo); + + final Bundle result = mSwitchesProvider.call(METHOD_ON_CHECKED_CHANGED, "uri" , extras); + + assertThat(result.getBoolean(EXTRA_SWITCH_SET_CHECKED_ERROR)).isTrue(); + assertThat(result.getString(EXTRA_SWITCH_SET_CHECKED_ERROR_MESSAGE)).isEqualTo("error"); + } + + private class TestSwitchesProvider extends SwitchesProvider { + + private List<SwitchController> mControllers; + + @Override + protected List<SwitchController> createSwitchControllers() { + return mControllers; + } + + void addSwitchController(SwitchController controller) { + if (mControllers == null) { + mControllers = new ArrayList<>(); + } + mControllers.add(controller); + } + } + + private static class TestSwitchController extends SwitchController { + + private String mKey; + private MetaData mMetaData; + private boolean mChecked; + private String mErrorMsg; + + @Override + public String getSwitchKey() { + return mKey; + } + + @Override + protected MetaData getMetaData() { + return mMetaData; + } + + @Override + protected boolean isChecked() { + return mChecked; + } + + @Override + protected boolean onCheckedChanged(boolean checked) { + return mErrorMsg == null ? true : false; + } + + @Override + protected String getErrorMessage(boolean attemptedChecked) { + return mErrorMsg; + } + + void setKey(String key) { + mKey = key; + } + + void setMetaData(MetaData metaData) { + mMetaData = metaData; + } + + void setChecked(boolean checked) { + mChecked = checked; + } + + void setErrorMessage(String errorMsg) { + mErrorMsg = errorMsg; + } + } + + private static class TestDynamicSwitchController extends TestSwitchController + implements ProviderIcon, DynamicTitle, DynamicSummary { + + static final String TITLE = "title"; + static final String SUMMARY = "summary"; + static final Bundle ICON_BUNDLE = new Bundle(); + + @Override + public Bundle getProviderIcon() { + return ICON_BUNDLE; + } + + @Override + public String getDynamicTitle() { + return TITLE; + } + + @Override + public String getDynamicSummary() { + return SUMMARY; + } + } +} diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/drawer/TileUtilsTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/drawer/TileUtilsTest.java index aa1ac4ecd334..b36eb4950b11 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/drawer/TileUtilsTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/drawer/TileUtilsTest.java @@ -44,6 +44,7 @@ import android.content.pm.ActivityInfo; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; +import android.content.pm.ProviderInfo; import android.content.pm.ResolveInfo; import android.content.res.Resources; import android.os.Bundle; @@ -60,12 +61,17 @@ import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; +import org.robolectric.annotation.Implementation; +import org.robolectric.annotation.Implements; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Map; @RunWith(RobolectricTestRunner.class) +@Config(shadows = TileUtilsTest.ShadowTileUtils.class) public class TileUtilsTest { private Context mContext; @@ -104,12 +110,15 @@ public class TileUtilsTest { info.add(newInfo(true, testCategory)); when(mPackageManager.queryIntentActivitiesAsUser(any(Intent.class), anyInt(), anyInt())) .thenReturn(info); + when(mPackageManager.queryIntentContentProvidersAsUser(any(Intent.class), anyInt(), + anyInt())).thenReturn(info); TileUtils.getTilesForAction(mContext, UserHandle.CURRENT, IA_SETTINGS_ACTION, addedCache, null /* defaultCategory */, outTiles, false /* usePriority */); - assertThat(outTiles.size()).isEqualTo(1); + assertThat(outTiles).hasSize(2); assertThat(outTiles.get(0).getCategory()).isEqualTo(testCategory); + assertThat(outTiles.get(1).getCategory()).isEqualTo(testCategory); } @Test @@ -123,12 +132,15 @@ public class TileUtilsTest { when(mPackageManager.queryIntentActivitiesAsUser(any(Intent.class), anyInt(), anyInt())) .thenReturn(info); + when(mPackageManager.queryIntentContentProvidersAsUser(any(Intent.class), anyInt(), + anyInt())).thenReturn(info); TileUtils.getTilesForAction(mContext, UserHandle.CURRENT, IA_SETTINGS_ACTION, addedCache, null /* defaultCategory */, outTiles, false /* requiresSettings */); - assertThat(outTiles).hasSize(1); + assertThat(outTiles).hasSize(2); assertThat(outTiles.get(0).getKey(mContext)).isEqualTo(keyHint); + assertThat(outTiles.get(1).getKey(mContext)).isEqualTo(keyHint); } @Test @@ -141,6 +153,8 @@ public class TileUtilsTest { when(mPackageManager.queryIntentActivitiesAsUser(any(Intent.class), anyInt(), anyInt())) .thenReturn(info); + when(mPackageManager.queryIntentContentProvidersAsUser(any(Intent.class), anyInt(), + anyInt())).thenReturn(info); TileUtils.getTilesForAction(mContext, UserHandle.CURRENT, IA_SETTINGS_ACTION, addedCache, null /* defaultCategory */, outTiles, false /* requiresSettings */); @@ -162,6 +176,8 @@ public class TileUtilsTest { TileUtils.getCategories(mContext, cache); verify(mPackageManager, atLeastOnce()).queryIntentActivitiesAsUser( intentCaptor.capture(), anyInt(), anyInt()); + verify(mPackageManager, atLeastOnce()).queryIntentContentProvidersAsUser( + intentCaptor.capture(), anyInt(), anyInt()); assertThat(intentCaptor.getAllValues().get(0).getPackage()) .isEqualTo(TileUtils.SETTING_PKG); @@ -178,12 +194,15 @@ public class TileUtilsTest { when(mPackageManager.queryIntentActivitiesAsUser(any(Intent.class), anyInt(), anyInt())) .thenReturn(info); + when(mPackageManager.queryIntentContentProvidersAsUser(any(Intent.class), anyInt(), + anyInt())).thenReturn(info); TileUtils.getTilesForAction(mContext, UserHandle.CURRENT, IA_SETTINGS_ACTION, addedCache, null /* defaultCategory */, outTiles, false /* usePriority */); - assertThat(outTiles.size()).isEqualTo(1); + assertThat(outTiles).hasSize(2); assertThat(outTiles.get(0).getTitle(mContext)).isEqualTo("my title"); + assertThat(outTiles.get(1).getTitle(mContext)).isEqualTo("my title"); } @Test @@ -197,14 +216,17 @@ public class TileUtilsTest { when(mPackageManager.queryIntentActivitiesAsUser(any(Intent.class), anyInt(), anyInt())) .thenReturn(info); + when(mPackageManager.queryIntentContentProvidersAsUser(any(Intent.class), anyInt(), + anyInt())).thenReturn(info); when(mResources.getString(eq(123))) .thenReturn("my localized title"); TileUtils.getTilesForAction(mContext, UserHandle.CURRENT, IA_SETTINGS_ACTION, addedCache, null /* defaultCategory */, outTiles, false /* usePriority */); - assertThat(outTiles.size()).isEqualTo(1); + assertThat(outTiles).hasSize(2); assertThat(outTiles.get(0).getTitle(mContext)).isEqualTo("my localized title"); + assertThat(outTiles.get(1).getTitle(mContext)).isEqualTo("my localized title"); } @Test @@ -220,11 +242,14 @@ public class TileUtilsTest { when(mPackageManager.queryIntentActivitiesAsUser(any(Intent.class), anyInt(), anyInt())) .thenReturn(info); + when(mPackageManager.queryIntentContentProvidersAsUser(any(Intent.class), anyInt(), + anyInt())).thenReturn(info); TileUtils.getTilesForAction(mContext, UserHandle.CURRENT, IA_SETTINGS_ACTION, addedCache, null /* defaultCategory */, outTiles, false /* usePriority */); assertThat(outTiles.get(0).isIconTintable(mContext)).isFalse(); + assertThat(outTiles.get(1).isIconTintable(mContext)).isFalse(); } @Test @@ -259,7 +284,6 @@ public class TileUtilsTest { assertThat(newMetaData).isNotSameAs(oldMetadata); } - @Test public void getTilesForIntent_shouldMarkIconTintableIfMetadataSet() { Map<Pair<String, String>, Tile> addedCache = new ArrayMap<>(); @@ -273,11 +297,14 @@ public class TileUtilsTest { when(mPackageManager.queryIntentActivitiesAsUser(any(Intent.class), anyInt(), anyInt())) .thenReturn(info); + when(mPackageManager.queryIntentContentProvidersAsUser(any(Intent.class), anyInt(), + anyInt())).thenReturn(info); TileUtils.getTilesForAction(mContext, UserHandle.CURRENT, IA_SETTINGS_ACTION, addedCache, null /* defaultCategory */, outTiles, false /* usePriority */); assertThat(outTiles.get(0).isIconTintable(mContext)).isTrue(); + assertThat(outTiles.get(1).isIconTintable(mContext)).isTrue(); } @Test @@ -291,11 +318,13 @@ public class TileUtilsTest { when(mPackageManager.queryIntentActivitiesAsUser(any(Intent.class), anyInt(), anyInt())) .thenReturn(info); + when(mPackageManager.queryIntentContentProvidersAsUser(any(Intent.class), anyInt(), + anyInt())).thenReturn(info); TileUtils.getTilesForAction(mContext, UserHandle.CURRENT, IA_SETTINGS_ACTION, addedCache, null /* defaultCategory */, outTiles, false /* usePriority */); - assertThat(outTiles.size()).isEqualTo(1); + assertThat(outTiles).hasSize(2); } public static ResolveInfo newInfo(boolean systemApp, String category) { @@ -314,33 +343,66 @@ public class TileUtilsTest { private static ResolveInfo newInfo(boolean systemApp, String category, String keyHint, String iconUri, String summaryUri, String title, int titleResId) { - ResolveInfo info = new ResolveInfo(); + final Bundle metaData = newMetaData(category, keyHint, iconUri, summaryUri, title, + titleResId); + final ResolveInfo info = new ResolveInfo(); info.system = systemApp; + info.activityInfo = new ActivityInfo(); info.activityInfo.packageName = "abc"; info.activityInfo.name = "123"; - info.activityInfo.metaData = new Bundle(); - info.activityInfo.metaData.putString("com.android.settings.category", category); - info.activityInfo.metaData.putInt(META_DATA_PREFERENCE_ICON, 314159); - info.activityInfo.metaData.putString(META_DATA_PREFERENCE_SUMMARY, "static-summary"); + info.activityInfo.metaData = metaData; + info.activityInfo.applicationInfo = new ApplicationInfo(); + + info.providerInfo = new ProviderInfo(); + info.providerInfo.packageName = "abc"; + info.providerInfo.name = "456"; + info.providerInfo.authority = "auth"; + ShadowTileUtils.setMetaData(metaData); + info.providerInfo.applicationInfo = new ApplicationInfo(); + + if (systemApp) { + info.activityInfo.applicationInfo.flags |= ApplicationInfo.FLAG_SYSTEM; + info.providerInfo.applicationInfo.flags |= ApplicationInfo.FLAG_SYSTEM; + } + return info; + } + + private static Bundle newMetaData(String category, String keyHint, String iconUri, + String summaryUri, String title, int titleResId) { + final Bundle metaData = new Bundle(); + metaData.putString("com.android.settings.category", category); + metaData.putInt(META_DATA_PREFERENCE_ICON, 314159); + metaData.putString(META_DATA_PREFERENCE_SUMMARY, "static-summary"); if (keyHint != null) { - info.activityInfo.metaData.putString(META_DATA_PREFERENCE_KEYHINT, keyHint); + metaData.putString(META_DATA_PREFERENCE_KEYHINT, keyHint); } if (iconUri != null) { - info.activityInfo.metaData.putString(META_DATA_PREFERENCE_ICON_URI, iconUri); + metaData.putString(META_DATA_PREFERENCE_ICON_URI, iconUri); } if (summaryUri != null) { - info.activityInfo.metaData.putString(META_DATA_PREFERENCE_SUMMARY_URI, summaryUri); + metaData.putString(META_DATA_PREFERENCE_SUMMARY_URI, summaryUri); } if (titleResId != 0) { - info.activityInfo.metaData.putInt(TileUtils.META_DATA_PREFERENCE_TITLE, titleResId); + metaData.putInt(TileUtils.META_DATA_PREFERENCE_TITLE, titleResId); } else if (title != null) { - info.activityInfo.metaData.putString(TileUtils.META_DATA_PREFERENCE_TITLE, title); + metaData.putString(TileUtils.META_DATA_PREFERENCE_TITLE, title); } - info.activityInfo.applicationInfo = new ApplicationInfo(); - if (systemApp) { - info.activityInfo.applicationInfo.flags |= ApplicationInfo.FLAG_SYSTEM; + return metaData; + } + + @Implements(TileUtils.class) + static class ShadowTileUtils { + + private static Bundle sMetaData; + + @Implementation + protected static List<Bundle> getSwitchDataFromProvider(Context context, String authority) { + return Arrays.asList(sMetaData); + } + + private static void setMetaData(Bundle metaData) { + sMetaData = metaData; } - return info; } } |