Add suport for additional injected metadata
To support security patch injection, adding support for additional
injection metadata including keyhint, icon_tintable, summary_uri, and
icon_uri.
Bug: 172352420
Test: manual, atest CarSettingsUnitTests, make -j50 RunCarSettingsRoboTests ROBOTEST_FILTER=com.android.car.settings.common
Change-Id: I6a2f55817679ef1f370cd72925392dcdee2a0831
diff --git a/res/drawable/ic_find_device_disabled.xml b/res/drawable/ic_find_device_disabled.xml
new file mode 100644
index 0000000..631d3e8
--- /dev/null
+++ b/res/drawable/ic_find_device_disabled.xml
@@ -0,0 +1,25 @@
+<!--
+ Copyright (C) 2017 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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="@dimen/icon_size"
+ android:height="@dimen/icon_size"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+
+ <path
+ android:fillColor="#FFF44336"
+ android:pathData="M12,2C8.13,2 5,5.13 5,9c0,5.25 7,13 7,13s7,-7.75 7,-13C19,5.13 15.87,2 12,2zM7,9c0,-2.76 2.24,-5 5,-5s5,2.24 5,5c0,2.88 -2.88,7.19 -5,9.88C9.92,16.21 7,11.85 7,9zM11,13h2v2h-2V13zM13,6h-2v5h2V6z"/>
+</vector>
diff --git a/res/drawable/ic_find_device_enabled.xml b/res/drawable/ic_find_device_enabled.xml
new file mode 100644
index 0000000..d676a70
--- /dev/null
+++ b/res/drawable/ic_find_device_enabled.xml
@@ -0,0 +1,25 @@
+<!--
+ Copyright (C) 2017 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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="@dimen/icon_size"
+ android:height="@dimen/icon_size"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+
+ <path
+ android:fillColor="#FF43A047"
+ android:pathData="M12,2C8.13,2 5,5.13 5,9c0,5.25 7,13 7,13s7,-7.75 7,-13C19,5.13 15.87,2 12,2zM7,9c0,-2.76 2.24,-5 5,-5s5,2.24 5,5c0,2.88 -2.88,7.19 -5,9.88C9.92,16.21 7,11.85 7,9zM14.5,9c0,1.38 -1.12,2.5 -2.5,2.5S9.5,10.38 9.5,9s1.12,-2.5 2.5,-2.5S14.5,7.62 14.5,9z"/>
+</vector>
diff --git a/res/drawable/ic_ota_update_current.xml b/res/drawable/ic_ota_update_current.xml
new file mode 100644
index 0000000..d21f68a
--- /dev/null
+++ b/res/drawable/ic_ota_update_current.xml
@@ -0,0 +1,25 @@
+<!--
+ Copyright (C) 2017 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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="@dimen/icon_size"
+ android:height="@dimen/icon_size"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+
+ <path
+ android:fillColor="#FF43A047"
+ android:pathData="M17,1.01L7,1C5.9,1 5,1.9 5,3v18c0,1.1 0.9,2 2,2h10c1.1,0 2,-0.9 2,-2V3C19,1.9 18.1,1.01 17,1.01zM17,21H7l0,-1h10V21zM17,18H7V6h10V18zM7,4V3h10v1H7zM11.14,16l-3.84,-3.84l1.41,-1.42l2.43,2.42l4.16,-4.16l1.42,1.41L11.14,16z"/>
+</vector>
diff --git a/res/drawable/ic_ota_update_none.xml b/res/drawable/ic_ota_update_none.xml
new file mode 100644
index 0000000..df6c8fe
--- /dev/null
+++ b/res/drawable/ic_ota_update_none.xml
@@ -0,0 +1,25 @@
+<!--
+ Copyright (C) 2017 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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="@dimen/icon_size"
+ android:height="@dimen/icon_size"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+
+ <path
+ android:fillColor="#FFF44336"
+ android:pathData="M17,1.01L7,1C5.9,1 5,1.9 5,3v18c0,1.1 0.9,2 2,2h10c1.1,0 2,-0.9 2,-2V3C19,1.9 18.1,1.01 17,1.01zM17,21H7l0,-1h10V21zM17,18H7V6h10V18zM7,4V3h10v1H7zM11,15h2v2h-2V15zM13,8h-2v5h2V8z"/>
+</vector>
diff --git a/res/drawable/ic_ota_update_stale.xml b/res/drawable/ic_ota_update_stale.xml
new file mode 100644
index 0000000..df6c8fe
--- /dev/null
+++ b/res/drawable/ic_ota_update_stale.xml
@@ -0,0 +1,25 @@
+<!--
+ Copyright (C) 2017 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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="@dimen/icon_size"
+ android:height="@dimen/icon_size"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+
+ <path
+ android:fillColor="#FFF44336"
+ android:pathData="M17,1.01L7,1C5.9,1 5,1.9 5,3v18c0,1.1 0.9,2 2,2h10c1.1,0 2,-0.9 2,-2V3C19,1.9 18.1,1.01 17,1.01zM17,21H7l0,-1h10V21zM17,18H7V6h10V18zM7,4V3h10v1H7zM11,15h2v2h-2V15zM13,8h-2v5h2V8z"/>
+</vector>
diff --git a/res/drawable/ic_package_verifier_disabled.xml b/res/drawable/ic_package_verifier_disabled.xml
new file mode 100644
index 0000000..8ddc04e
--- /dev/null
+++ b/res/drawable/ic_package_verifier_disabled.xml
@@ -0,0 +1,25 @@
+<!--
+ Copyright (C) 2017 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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="@dimen/icon_size"
+ android:height="@dimen/icon_size"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+
+ <path
+ android:fillColor="#FFEF6C00"
+ android:pathData="M12,4.24l6,3v4.1c0,3.9 -2.55,7.5 -6,8.59c-3.45,-1.09 -6,-4.7 -6,-8.59v-4.1L12,4.24M12,2L4,6v5.33c0,4.93 3.41,9.55 8,10.67c4.59,-1.12 8,-5.73 8,-10.67V6L12,2L12,2zM11,15h2v2h-2V15zM13,8h-2v5h2V8z"/>
+</vector>
diff --git a/res/drawable/ic_package_verifier_enabled.xml b/res/drawable/ic_package_verifier_enabled.xml
new file mode 100644
index 0000000..b46c5ea
--- /dev/null
+++ b/res/drawable/ic_package_verifier_enabled.xml
@@ -0,0 +1,25 @@
+<!--
+ Copyright (C) 2017 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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="@dimen/icon_size"
+ android:height="@dimen/icon_size"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+
+ <path
+ android:fillColor="#FF43A047"
+ android:pathData="M11.14,16l-3.84,-3.84l1.41,-1.42l2.43,2.42l4.16,-4.16l1.42,1.41L11.14,16zM12,4.24l6,3v4.1c0,3.9 -2.55,7.5 -6,8.59c-3.45,-1.09 -6,-4.7 -6,-8.59v-4.1L12,4.24M12,2L4,6v5.33c0,4.93 3.41,9.55 8,10.67c4.59,-1.12 8,-5.73 8,-10.67V6L12,2L12,2z"/>
+</vector>
diff --git a/res/drawable/ic_package_verifier_removed.xml b/res/drawable/ic_package_verifier_removed.xml
new file mode 100644
index 0000000..a8902ae
--- /dev/null
+++ b/res/drawable/ic_package_verifier_removed.xml
@@ -0,0 +1,24 @@
+<!--
+ Copyright (C) 2017 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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="@dimen/icon_size"
+ android:height="@dimen/icon_size"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="#FFF44336"
+ android:pathData="M15.5,9.91L14.09,8.5L12,10.59L9.91,8.5L8.5,9.91L10.59,12L8.5,14.09l1.41,1.41L12,13.42l2.09,2.08l1.41,-1.41L13.42,12L15.5,9.91zM12,4.24l6,3v4.1c0,3.9 -2.55,7.5 -6,8.59c-3.45,-1.09 -6,-4.7 -6,-8.59v-4.1L12,4.24M12,2L4,6v5.33c0,4.93 3.41,9.55 8,10.67c4.59,-1.12 8,-5.73 8,-10.67V6L12,2L12,2z"/>
+</vector>
diff --git a/res/drawable/ic_placeholder.xml b/res/drawable/ic_placeholder.xml
new file mode 100644
index 0000000..cc3801e
--- /dev/null
+++ b/res/drawable/ic_placeholder.xml
@@ -0,0 +1,26 @@
+<!--
+ ~ Copyright (C) 2020 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.
+ -->
+<!-- Transparent icon to be used as a placeholder for URI-based icons that take time to load. -->
+<vector
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="@dimen/icon_size"
+ android:height="@dimen/icon_size"
+ android:viewportHeight="24.0"
+ android:viewportWidth="24.0">
+ <path
+ android:fillColor="@android:color/transparent"
+ android:pathData="M 0 0 1 0 1 1 0 1 z"/>
+</vector>
\ No newline at end of file
diff --git a/res/values/strings.xml b/res/values/strings.xml
index c6bfc4b..247699f 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -34,6 +34,8 @@
<string name="keywords_display_night_display">dim screen, night, tint</string>
<!-- Label for night mode toggle tile in quick setting [CHAR LIMIT=20] -->
<string name="night_mode_tile_label">Night mode</string>
+ <!-- Empty placeholder string to be used while waiting for a string to be loaded asynchronously. [CHAR LIMIT=NONE] -->
+ <string name="empty_placeholder" translatable="false"> </string>
<!-- Network and internet settings [CHAR LIMIT=40] -->
<string name="network_and_internet">Network & internet</string>
diff --git a/src/com/android/car/settings/common/ExtraSettingsLoader.java b/src/com/android/car/settings/common/ExtraSettingsLoader.java
index ef59b3d..a0e387a 100644
--- a/src/com/android/car/settings/common/ExtraSettingsLoader.java
+++ b/src/com/android/car/settings/common/ExtraSettingsLoader.java
@@ -18,8 +18,12 @@
import static com.android.settingslib.drawer.CategoryKey.CATEGORY_DEVICE;
import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_ICON;
+import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_ICON_URI;
+import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_KEYHINT;
import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_SUMMARY;
+import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_SUMMARY_URI;
import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_TITLE;
+import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_TITLE_URI;
import android.app.ActivityManager;
import android.content.Context;
@@ -32,6 +36,7 @@
import android.os.Bundle;
import android.text.TextUtils;
+import androidx.annotation.VisibleForTesting;
import androidx.preference.Preference;
import com.android.car.apps.common.util.Themes;
@@ -49,14 +54,21 @@
public class ExtraSettingsLoader {
private static final Logger LOG = new Logger(ExtraSettingsLoader.class);
private static final String META_DATA_PREFERENCE_CATEGORY = "com.android.settings.category";
- private Map<Preference, Bundle> mPreferenceBundleMap;
private final Context mContext;
+ private Map<Preference, Bundle> mPreferenceBundleMap;
+ private PackageManager mPm;
public ExtraSettingsLoader(Context context) {
mContext = context;
+ mPm = context.getPackageManager();
mPreferenceBundleMap = new HashMap<>();
}
+ @VisibleForTesting
+ void setPackageManager(PackageManager pm) {
+ mPm = pm;
+ }
+
/**
* Returns a map of {@link Preference} and {@link Bundle} representing settings injected from
* system apps and their metadata. The given intent must specify the action to use for
@@ -66,8 +78,7 @@
* @param intent intent specifying the extra settings category to load
*/
public Map<Preference, Bundle> loadPreferences(Intent intent) {
- PackageManager pm = mContext.getPackageManager();
- List<ResolveInfo> results = pm.queryIntentActivitiesAsUser(intent,
+ List<ResolveInfo> results = mPm.queryIntentActivitiesAsUser(intent,
PackageManager.GET_META_DATA, ActivityManager.getCurrentUser());
String extraCategory = intent.getStringExtra(META_DATA_PREFERENCE_CATEGORY);
@@ -76,40 +87,32 @@
// Do not allow any app to be added to settings, only system ones.
continue;
}
+ String key = null;
String title = null;
String summary = null;
String category = null;
ActivityInfo activityInfo = resolved.activityInfo;
Bundle metaData = activityInfo.metaData;
try {
- Resources res = pm.getResourcesForApplication(activityInfo.packageName);
- if (metaData.containsKey(META_DATA_PREFERENCE_TITLE)) {
- if (metaData.get(META_DATA_PREFERENCE_TITLE) instanceof Integer) {
- title = res.getString(metaData.getInt(META_DATA_PREFERENCE_TITLE));
- } else {
- title = metaData.getString(META_DATA_PREFERENCE_TITLE);
+ Resources res = mPm.getResourcesForApplication(activityInfo.packageName);
+ if (metaData.containsKey(META_DATA_PREFERENCE_KEYHINT)) {
+ key = extractMetaDataString(metaData, META_DATA_PREFERENCE_KEYHINT, res);
+ }
+ if (!metaData.containsKey(META_DATA_PREFERENCE_TITLE_URI)) {
+ title = extractMetaDataString(metaData, META_DATA_PREFERENCE_TITLE, res);
+ if (TextUtils.isEmpty(title)) {
+ LOG.d("no title.");
+ title = activityInfo.loadLabel(mPm).toString();
}
}
- if (TextUtils.isEmpty(title)) {
- LOG.d("no title.");
- title = activityInfo.loadLabel(pm).toString();
- }
- if (metaData.containsKey(META_DATA_PREFERENCE_SUMMARY)) {
- if (metaData.get(META_DATA_PREFERENCE_SUMMARY) instanceof Integer) {
- summary = res.getString(metaData.getInt(META_DATA_PREFERENCE_SUMMARY));
- } else {
- summary = metaData.getString(META_DATA_PREFERENCE_SUMMARY);
+ if (!metaData.containsKey(META_DATA_PREFERENCE_SUMMARY_URI)) {
+ summary = extractMetaDataString(metaData, META_DATA_PREFERENCE_SUMMARY, res);
+ if (TextUtils.isEmpty(summary)) {
+ LOG.d("no description.");
}
- } else {
- LOG.d("no description.");
}
- if (metaData.containsKey(META_DATA_PREFERENCE_CATEGORY)) {
- if (metaData.get(META_DATA_PREFERENCE_CATEGORY) instanceof Integer) {
- category = res.getString(metaData.getInt(META_DATA_PREFERENCE_CATEGORY));
- } else {
- category = metaData.getString(META_DATA_PREFERENCE_CATEGORY);
- }
- } else {
+ category = extractMetaDataString(metaData, META_DATA_PREFERENCE_CATEGORY, res);
+ if (TextUtils.isEmpty(category)) {
LOG.d("no category.");
}
} catch (PackageManager.NameNotFoundException | Resources.NotFoundException e) {
@@ -119,7 +122,7 @@
if (metaData.containsKey(META_DATA_PREFERENCE_ICON)) {
int iconRes = metaData.getInt(META_DATA_PREFERENCE_ICON);
icon = Icon.createWithResource(activityInfo.packageName, iconRes);
- } else {
+ } else if (!metaData.containsKey(META_DATA_PREFERENCE_ICON_URI)) {
icon = Icon.createWithResource(mContext, R.drawable.ic_settings_gear);
LOG.d("use default icon.");
}
@@ -136,14 +139,33 @@
CarUiPreference preference = new CarUiPreference(mContext);
preference.setTitle(title);
preference.setSummary(summary);
+ if (key != null) {
+ preference.setKey(key);
+ }
if (icon != null) {
preference.setIcon(icon.loadDrawable(mContext));
- preference.getIcon().setTintList(
- Themes.getAttrColorStateList(mContext, R.attr.iconColor));
+ if (ExtraSettingsUtil.isIconTintable(metaData)) {
+ preference.getIcon().setTintList(
+ Themes.getAttrColorStateList(mContext, R.attr.iconColor));
+ }
}
preference.setIntent(extraSettingIntent);
mPreferenceBundleMap.put(preference, metaData);
}
return mPreferenceBundleMap;
}
+
+ /**
+ * Extracts the value in the metadata specified by the key.
+ * If it is resource, resolve the string and return. Otherwise, return the string itself.
+ */
+ private String extractMetaDataString(Bundle metaData, String key, Resources res) {
+ if (metaData.containsKey(key)) {
+ if (metaData.get(key) instanceof Integer) {
+ return res.getString(metaData.getInt(key));
+ }
+ return metaData.getString(key);
+ }
+ return null;
+ }
}
diff --git a/src/com/android/car/settings/common/ExtraSettingsPreferenceController.java b/src/com/android/car/settings/common/ExtraSettingsPreferenceController.java
index 82e222c..480c22b 100644
--- a/src/com/android/car/settings/common/ExtraSettingsPreferenceController.java
+++ b/src/com/android/car/settings/common/ExtraSettingsPreferenceController.java
@@ -16,15 +16,42 @@
package com.android.car.settings.common;
+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.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_SUMMARY;
+import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_SUMMARY_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.car.drivingstate.CarUxRestrictions;
+import android.content.ContentResolver;
import android.content.Context;
+import android.content.IContentProvider;
import android.content.Intent;
+import android.database.ContentObserver;
+import android.graphics.drawable.Icon;
+import android.net.Uri;
import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.text.TextUtils;
+import android.util.ArrayMap;
+import android.util.Pair;
import androidx.annotation.VisibleForTesting;
import androidx.preference.Preference;
import androidx.preference.PreferenceGroup;
+import com.android.car.apps.common.util.Themes;
+import com.android.car.settings.R;
+import com.android.settingslib.drawer.TileUtils;
+import com.android.settingslib.utils.ThreadUtils;
+
+import java.util.ArrayList;
+import java.util.List;
import java.util.Map;
/**
@@ -50,16 +77,23 @@
*/
// TODO: investigate using SettingsLib Tiles.
public class ExtraSettingsPreferenceController extends PreferenceController<PreferenceGroup> {
+ private static final Logger LOG = new Logger(ExtraSettingsPreferenceController.class);
@VisibleForTesting
static final String META_DATA_DISTRACTION_OPTIMIZED = "distractionOptimized";
+ private Context mContext;
+ private ContentResolver mContentResolver;
private ExtraSettingsLoader mExtraSettingsLoader;
private boolean mSettingsLoaded;
+ @VisibleForTesting
+ List<DynamicDataObserver> mObservers = new ArrayList<>();
public ExtraSettingsPreferenceController(Context context, String preferenceKey,
FragmentController fragmentController, CarUxRestrictions restrictionInfo) {
super(context, preferenceKey, fragmentController, restrictionInfo);
+ mContext = context;
+ mContentResolver = context.getContentResolver();
mExtraSettingsLoader = new ExtraSettingsLoader(context);
}
@@ -109,6 +143,20 @@
preference.setVisible(preference.getPreferenceCount() > 0);
}
+ @Override
+ protected void onStartInternal() {
+ mObservers.forEach(observer -> {
+ observer.register(mContentResolver, /* register= */ true);
+ });
+ }
+
+ @Override
+ protected void onStopInternal() {
+ mObservers.forEach(observer -> {
+ observer.register(mContentResolver, /* register= */ false);
+ });
+ }
+
/**
* Adds the extra settings from the system based on the intent that is passed in the preference
* group. All the preferences that resolve these intents will be added in the preference group.
@@ -125,7 +173,148 @@
metaData.getBoolean(META_DATA_DISTRACTION_OPTIMIZED);
}
setting.getExtras().putBoolean(META_DATA_DISTRACTION_OPTIMIZED, distractionOptimized);
+ setting.getExtras().putBoolean(META_DATA_PREFERENCE_ICON_TINTABLE,
+ ExtraSettingsUtil.isIconTintable(metaData));
+ getDynamicData(setting, metaData);
getPreference().addPreference(setting);
}
}
+
+ /**
+ * Retrieve dynamic injected preference data and create observers for updates.
+ */
+ protected void getDynamicData(Preference preference, Bundle metaData) {
+ if (metaData.containsKey(META_DATA_PREFERENCE_TITLE_URI)) {
+ // Set a placeholder title before starting to fetch real title to prevent vertical
+ // preference shift.
+ preference.setTitle(R.string.empty_placeholder);
+ Uri uri = ExtraSettingsUtil.getCompleteUri(metaData, META_DATA_PREFERENCE_TITLE_URI,
+ METHOD_GET_DYNAMIC_TITLE);
+ refreshTitle(uri, preference);
+ mObservers.add(new DynamicDataObserver(METHOD_GET_DYNAMIC_TITLE, uri, preference));
+ }
+ if (metaData.containsKey(META_DATA_PREFERENCE_SUMMARY_URI)) {
+ // Set a placeholder summary before starting to fetch real summary to prevent vertical
+ // preference shift.
+ preference.setSummary(R.string.empty_placeholder);
+ Uri uri = ExtraSettingsUtil.getCompleteUri(metaData, META_DATA_PREFERENCE_SUMMARY_URI,
+ METHOD_GET_DYNAMIC_SUMMARY);
+ refreshSummary(uri, preference);
+ mObservers.add(new DynamicDataObserver(METHOD_GET_DYNAMIC_SUMMARY, uri, preference));
+ }
+ if (metaData.containsKey(META_DATA_PREFERENCE_ICON_URI)) {
+ // Set a placeholder icon before starting to fetch real icon to prevent horizontal
+ // preference shift.
+ preference.setIcon(R.drawable.ic_placeholder);
+ Uri uri = ExtraSettingsUtil.getCompleteUri(metaData, META_DATA_PREFERENCE_ICON_URI,
+ METHOD_GET_PROVIDER_ICON);
+ refreshIcon(uri, preference);
+ mObservers.add(new DynamicDataObserver(METHOD_GET_PROVIDER_ICON, uri, preference));
+ }
+ }
+
+ @VisibleForTesting
+ void executeBackgroundTask(Runnable r) {
+ ThreadUtils.postOnBackgroundThread(r);
+ }
+
+ @VisibleForTesting
+ void executeUiTask(Runnable r) {
+ ThreadUtils.postOnMainThread(r);
+ }
+
+ private void refreshTitle(Uri uri, Preference preference) {
+ executeBackgroundTask(() -> {
+ Map<String, IContentProvider> providerMap = new ArrayMap<>();
+ String titleFromUri = TileUtils.getTextFromUri(
+ mContext, uri, providerMap, META_DATA_PREFERENCE_TITLE);
+ if (!TextUtils.equals(titleFromUri, preference.getTitle())) {
+ executeUiTask(() -> preference.setTitle(titleFromUri));
+ }
+ });
+ }
+
+ private void refreshSummary(Uri uri, Preference preference) {
+ executeBackgroundTask(() -> {
+ Map<String, IContentProvider> providerMap = new ArrayMap<>();
+ String summaryFromUri = TileUtils.getTextFromUri(
+ mContext, uri, providerMap, META_DATA_PREFERENCE_SUMMARY);
+ if (!TextUtils.equals(summaryFromUri, preference.getSummary())) {
+ executeUiTask(() -> preference.setSummary(summaryFromUri));
+ }
+ });
+ }
+
+ private void refreshIcon(Uri uri, Preference preference) {
+ executeBackgroundTask(() -> {
+ Intent intent = preference.getIntent();
+ String packageName = null;
+ if (!TextUtils.isEmpty(intent.getPackage())) {
+ packageName = intent.getPackage();
+ } else if (intent.getComponent() != null) {
+ packageName = intent.getComponent().getPackageName();
+ }
+ Map<String, IContentProvider> providerMap = new ArrayMap<>();
+ Pair<String, Integer> iconInfo = TileUtils.getIconFromUri(
+ mContext, packageName, uri, providerMap);
+ Icon icon;
+ if (iconInfo != null) {
+ icon = Icon.createWithResource(iconInfo.first, iconInfo.second);
+ } else {
+ LOG.w("Failed to get icon from uri " + uri);
+ icon = Icon.createWithResource(mContext, R.drawable.ic_settings_gear);
+ LOG.d("use default icon.");
+ }
+ if (icon != null) {
+ executeUiTask(() -> {
+ preference.setIcon(icon.loadDrawable(mContext));
+ if (preference.getExtras().getBoolean(META_DATA_PREFERENCE_ICON_TINTABLE)) {
+ preference.getIcon().setTintList(
+ Themes.getAttrColorStateList(mContext, R.attr.iconColor));
+ }
+ });
+ }
+ });
+ }
+
+ /**
+ * Observer for updating injected dynamic data.
+ */
+ private class DynamicDataObserver extends ContentObserver {
+ private final String mMethod;
+ private final Uri mUri;
+ private final Preference mPreference;
+
+ DynamicDataObserver(String method, Uri uri, Preference preference) {
+ super(new Handler(Looper.getMainLooper()));
+ mMethod = method;
+ mUri = uri;
+ mPreference = preference;
+ }
+
+ /** Registers or unregisters this observer to the given content resolver. */
+ void register(ContentResolver cr, boolean register) {
+ if (register) {
+ cr.registerContentObserver(mUri, /* notifyForDescendants= */ false,
+ /* observer= */ this);
+ } else {
+ cr.unregisterContentObserver(this);
+ }
+ }
+
+ @Override
+ public void onChange(boolean selfChange) {
+ switch (mMethod) {
+ case METHOD_GET_DYNAMIC_TITLE:
+ refreshTitle(mUri, mPreference);
+ break;
+ case METHOD_GET_DYNAMIC_SUMMARY:
+ refreshSummary(mUri, mPreference);
+ break;
+ case METHOD_GET_PROVIDER_ICON:
+ refreshIcon(mUri, mPreference);
+ break;
+ }
+ }
+ }
}
diff --git a/src/com/android/car/settings/common/ExtraSettingsUtil.java b/src/com/android/car/settings/common/ExtraSettingsUtil.java
new file mode 100644
index 0000000..7937fdf
--- /dev/null
+++ b/src/com/android/car/settings/common/ExtraSettingsUtil.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2020 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.car.settings.common;
+
+import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_ICON_TINTABLE;
+import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_KEYHINT;
+
+import android.content.ContentResolver;
+import android.net.Uri;
+import android.os.Bundle;
+import android.text.TextUtils;
+
+import java.util.List;
+
+/** Contains utility functions for injected settings. */
+public class ExtraSettingsUtil {
+ private static final Logger LOG = new Logger(ExtraSettingsUtil.class);
+
+ /**
+ * Returns whether or not an icon is tintable given the injected setting metadata.
+ */
+ public static boolean isIconTintable(Bundle metaData) {
+ if (metaData.containsKey(META_DATA_PREFERENCE_ICON_TINTABLE)) {
+ return metaData.getBoolean(META_DATA_PREFERENCE_ICON_TINTABLE);
+ }
+ return false;
+ }
+
+ /**
+ * Returns the complete uri from the meta data key of the injected setting metadata.
+ *
+ * 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.
+ */
+ public static Uri getCompleteUri(Bundle metaData, String metaDataKey, String defaultMethod) {
+ String uriString = metaData.getString(metaDataKey);
+ if (TextUtils.isEmpty(uriString)) {
+ return null;
+ }
+
+ Uri uri = Uri.parse(uriString);
+ List<String> pathSegments = uri.getPathSegments();
+ if (pathSegments != null && !pathSegments.isEmpty()) {
+ return uri;
+ }
+
+ String key = metaData.getString(META_DATA_PREFERENCE_KEYHINT);
+ if (TextUtils.isEmpty(key)) {
+ LOG.w("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);
+ }
+
+ private 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();
+ }
+}
diff --git a/tests/unit/AndroidManifest.xml b/tests/unit/AndroidManifest.xml
index 86ffe81..d240ed1 100644
--- a/tests/unit/AndroidManifest.xml
+++ b/tests/unit/AndroidManifest.xml
@@ -22,6 +22,12 @@
<application android:debuggable="true">
<uses-library android:name="android.test.runner" />
+
+ <provider
+ android:name="com.android.car.settings.testutils.TestContentProvider"
+ android:authorities="com.android.car.settings.testutils.TestContentProvider"
+ android:exported="true">
+ </provider>
</application>
<instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
diff --git a/tests/unit/res/drawable/test_icon.xml b/tests/unit/res/drawable/test_icon.xml
new file mode 100644
index 0000000..93ab1a7
--- /dev/null
+++ b/tests/unit/res/drawable/test_icon.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2020 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.
+ -->
+
+<vector
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="44dp"
+ android:height="44dp"
+ android:viewportHeight="24.0"
+ android:viewportWidth="24.0">
+ <path
+ android:fillColor="#ffffff"
+ android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z"/>
+</vector>
\ No newline at end of file
diff --git a/tests/unit/src/com/android/car/settings/common/ExtraSettingsLoaderTest.java b/tests/unit/src/com/android/car/settings/common/ExtraSettingsLoaderTest.java
new file mode 100644
index 0000000..6f6703a
--- /dev/null
+++ b/tests/unit/src/com/android/car/settings/common/ExtraSettingsLoaderTest.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2020 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.car.settings.common;
+
+import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_ICON_URI;
+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_TITLE;
+import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_TITLE_URI;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.os.Bundle;
+
+import androidx.preference.Preference;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.Collections;
+import java.util.Map;
+
+@RunWith(AndroidJUnit4.class)
+public class ExtraSettingsLoaderTest {
+ private static final String META_DATA_PREFERENCE_CATEGORY = "com.android.settings.category";
+ private static final String FAKE_CATEGORY = "fake_category";
+ private static final String FAKE_TITLE = "fake_title";
+ private static final String FAKE_SUMMARY = "fake_summary";
+ private static final String FAKE_CONTENT_PROVIDER = "content://android.content.FakeProvider";
+
+ private Context mContext = ApplicationProvider.getApplicationContext();
+ private ExtraSettingsLoader mExtraSettingsLoader;
+
+ @Mock
+ private PackageManager mPm;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ mExtraSettingsLoader = new ExtraSettingsLoader(mContext);
+ mExtraSettingsLoader.setPackageManager(mPm);
+ }
+
+ @Test
+ public void testLoadPreference_uriResources_shouldNotLoadStaticResources() {
+ Intent intent = new Intent();
+ intent.putExtra(META_DATA_PREFERENCE_CATEGORY, FAKE_CATEGORY);
+ Bundle bundle = new Bundle();
+ bundle.putString(META_DATA_PREFERENCE_TITLE, FAKE_TITLE);
+ bundle.putString(META_DATA_PREFERENCE_SUMMARY, FAKE_SUMMARY);
+ bundle.putString(META_DATA_PREFERENCE_CATEGORY, FAKE_CATEGORY);
+ bundle.putString(META_DATA_PREFERENCE_TITLE_URI, FAKE_CONTENT_PROVIDER);
+ bundle.putString(META_DATA_PREFERENCE_SUMMARY_URI, FAKE_CONTENT_PROVIDER);
+ bundle.putString(META_DATA_PREFERENCE_ICON_URI, FAKE_CONTENT_PROVIDER);
+
+ ActivityInfo activityInfo = new ActivityInfo();
+ activityInfo.metaData = bundle;
+ activityInfo.packageName = "package_name";
+ activityInfo.name = "class_name";
+
+ ResolveInfo resolveInfoSystem = new ResolveInfo();
+ resolveInfoSystem.system = true;
+ resolveInfoSystem.activityInfo = activityInfo;
+
+ when(mPm.queryIntentActivitiesAsUser(eq(intent), eq(PackageManager.GET_META_DATA),
+ anyInt())).thenReturn(Collections.singletonList(resolveInfoSystem));
+ Map<Preference, Bundle> preferenceToBundleMap = mExtraSettingsLoader.loadPreferences(
+ intent);
+
+ assertThat(preferenceToBundleMap).hasSize(1);
+
+ for (Preference p : preferenceToBundleMap.keySet()) {
+ assertThat(p.getTitle()).isNull();
+ assertThat(p.getSummary()).isNull();
+ assertThat(p.getIcon()).isNull();
+ }
+ }
+}
diff --git a/tests/unit/src/com/android/car/settings/common/ExtraSettingsPreferenceControllerTest.java b/tests/unit/src/com/android/car/settings/common/ExtraSettingsPreferenceControllerTest.java
index 62ded8b..6c7d36b 100644
--- a/tests/unit/src/com/android/car/settings/common/ExtraSettingsPreferenceControllerTest.java
+++ b/tests/unit/src/com/android/car/settings/common/ExtraSettingsPreferenceControllerTest.java
@@ -17,7 +17,14 @@
package com.android.car.settings.common;
import static com.android.car.settings.common.ExtraSettingsPreferenceController.META_DATA_DISTRACTION_OPTIMIZED;
+import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_ICON_URI;
+import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_SUMMARY_URI;
+import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_TITLE_URI;
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -25,6 +32,7 @@
import android.car.drivingstate.CarUxRestrictions;
import android.content.Context;
import android.content.Intent;
+import android.graphics.drawable.Drawable;
import android.os.Bundle;
import androidx.lifecycle.LifecycleOwner;
@@ -35,7 +43,9 @@
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
+import com.android.car.settings.R;
import com.android.car.settings.testutils.ResourceTestUtils;
+import com.android.car.settings.testutils.TestContentProvider;
import com.android.car.ui.preference.CarUiPreference;
import com.android.car.ui.preference.DisabledPreferenceCallback;
import com.android.settingslib.core.lifecycle.Lifecycle;
@@ -43,6 +53,7 @@
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.mockito.InOrder;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
@@ -65,6 +76,12 @@
new CarUxRestrictions.Builder(/* reqOpt= */ false,
CarUxRestrictions.UX_RESTRICTIONS_BASELINE, /* timestamp= */ 0).build();
+ // Fake provider that won't actually resolve to anything
+ private static final String FAKE_PROVIDER = "content://android.content.FakeProvider";
+ // Real test provider
+ private static final String TEST_PROVIDER =
+ "content://com.android.car.settings.testutils.TestContentProvider";
+
private LifecycleOwner mLifecycleOwner;
private Lifecycle mLifecycle;
@@ -73,7 +90,8 @@
private PreferenceScreen mScreen;
private FakeExtraSettingsPreferenceController mPreferenceController;
private CarUiPreference mPreference;
- private Map<Preference, Bundle> mPreferenceBundleMap = new HashMap<>();
+ private Map<Preference, Bundle> mPreferenceBundleMap;
+ private Bundle mMetaData;
@Mock
private FragmentController mFragmentController;
@@ -97,18 +115,20 @@
mScreen.setIntent(FAKE_INTENT);
mPreferenceController.setPreference(mScreen);
mPreference = spy(new CarUiPreference(mContext));
+ mPreference.setIntent(new Intent().setPackage("com.android.car.settings"));
- Bundle bundle = new Bundle();
- bundle.putBoolean(META_DATA_DISTRACTION_OPTIMIZED, false);
mPreferenceBundleMap = new HashMap<>();
- mPreferenceBundleMap.put(mPreference, bundle);
- when(mExtraSettingsLoaderMock.loadPreferences(FAKE_INTENT)).thenReturn(
- mPreferenceBundleMap);
- mPreferenceController.setExtraSettingsLoader(mExtraSettingsLoaderMock);
+ mMetaData = new Bundle();
}
@Test
public void onUxRestrictionsChanged_restricted_restrictedMessageSet() {
+ mMetaData.putBoolean(META_DATA_DISTRACTION_OPTIMIZED, false);
+ mPreferenceBundleMap.put(mPreference, mMetaData);
+ when(mExtraSettingsLoaderMock.loadPreferences(FAKE_INTENT)).thenReturn(
+ mPreferenceBundleMap);
+ mPreferenceController.setExtraSettingsLoader(mExtraSettingsLoaderMock);
+
mPreferenceController.onCreate(mLifecycleOwner);
Mockito.reset(mPreference);
@@ -121,6 +141,12 @@
@Test
public void onUxRestrictionsChanged_unrestricted_restrictedMessageUnset() {
+ mMetaData.putBoolean(META_DATA_DISTRACTION_OPTIMIZED, false);
+ mPreferenceBundleMap.put(mPreference, mMetaData);
+ when(mExtraSettingsLoaderMock.loadPreferences(FAKE_INTENT)).thenReturn(
+ mPreferenceBundleMap);
+ mPreferenceController.setExtraSettingsLoader(mExtraSettingsLoaderMock);
+
mPreferenceController.onCreate(mLifecycleOwner);
Mockito.reset(mPreference);
@@ -132,6 +158,12 @@
@Test
public void onUxRestrictionsChanged_restricted_viewOnly_restrictedMessageUnset() {
+ mMetaData.putBoolean(META_DATA_DISTRACTION_OPTIMIZED, false);
+ mPreferenceBundleMap.put(mPreference, mMetaData);
+ when(mExtraSettingsLoaderMock.loadPreferences(FAKE_INTENT)).thenReturn(
+ mPreferenceBundleMap);
+ mPreferenceController.setExtraSettingsLoader(mExtraSettingsLoaderMock);
+
mPreferenceController.setAvailabilityStatus(PreferenceController.AVAILABLE_FOR_VIEWING);
mPreferenceController.onCreate(mLifecycleOwner);
@@ -142,6 +174,135 @@
.setMessageToShowWhenDisabledPreferenceClicked("");
}
+ @Test
+ public void onCreate_hasDynamicTitleData_placeholderAdded() {
+ mMetaData.putString(META_DATA_PREFERENCE_TITLE_URI, FAKE_PROVIDER);
+ mPreferenceBundleMap.put(mPreference, mMetaData);
+ when(mExtraSettingsLoaderMock.loadPreferences(FAKE_INTENT)).thenReturn(
+ mPreferenceBundleMap);
+ mPreferenceController.setExtraSettingsLoader(mExtraSettingsLoaderMock);
+
+ mPreferenceController.onCreate(mLifecycleOwner);
+
+ verify(mPreference).setTitle(
+ ResourceTestUtils.getString(mContext, "empty_placeholder"));
+ }
+
+ @Test
+ public void onCreate_hasDynamicSummaryData_placeholderAdded() {
+ mMetaData.putString(META_DATA_PREFERENCE_SUMMARY_URI, FAKE_PROVIDER);
+ mPreferenceBundleMap.put(mPreference, mMetaData);
+ when(mExtraSettingsLoaderMock.loadPreferences(FAKE_INTENT)).thenReturn(
+ mPreferenceBundleMap);
+ mPreferenceController.setExtraSettingsLoader(mExtraSettingsLoaderMock);
+
+ mPreferenceController.onCreate(mLifecycleOwner);
+
+ verify(mPreference).setSummary(
+ ResourceTestUtils.getString(mContext, "empty_placeholder"));
+ }
+
+ @Test
+ public void onCreate_hasDynamicIconData_placeholderAdded() {
+ mMetaData.putString(META_DATA_PREFERENCE_ICON_URI, FAKE_PROVIDER);
+ mPreferenceBundleMap.put(mPreference, mMetaData);
+ when(mExtraSettingsLoaderMock.loadPreferences(FAKE_INTENT)).thenReturn(
+ mPreferenceBundleMap);
+ mPreferenceController.setExtraSettingsLoader(mExtraSettingsLoaderMock);
+
+ mPreferenceController.onCreate(mLifecycleOwner);
+
+ verify(mPreference).setIcon(R.drawable.ic_placeholder);
+ }
+
+ @Test
+ public void onCreate_hasDynamicTitleData_TitleSet() {
+ mMetaData.putString(META_DATA_PREFERENCE_TITLE_URI,
+ TEST_PROVIDER + "/getText/textKey");
+
+ mPreferenceBundleMap.put(mPreference, mMetaData);
+ when(mExtraSettingsLoaderMock.loadPreferences(FAKE_INTENT)).thenReturn(
+ mPreferenceBundleMap);
+ mPreferenceController.setExtraSettingsLoader(mExtraSettingsLoaderMock);
+
+ mPreferenceController.onCreate(mLifecycleOwner);
+
+ assertThat(mPreference.getTitle()).isEqualTo(TestContentProvider.TEST_TEXT_CONTENT);
+ }
+
+ @Test
+ public void onCreate_hasDynamicSummaryData_summarySet() {
+ mMetaData.putString(META_DATA_PREFERENCE_SUMMARY_URI,
+ TEST_PROVIDER + "/getText/textKey");
+ mPreferenceBundleMap.put(mPreference, mMetaData);
+ when(mExtraSettingsLoaderMock.loadPreferences(FAKE_INTENT)).thenReturn(
+ mPreferenceBundleMap);
+ mPreferenceController.setExtraSettingsLoader(mExtraSettingsLoaderMock);
+
+ mPreferenceController.onCreate(mLifecycleOwner);
+
+ assertThat(mPreference.getSummary()).isEqualTo(TestContentProvider.TEST_TEXT_CONTENT);
+ }
+
+ @Test
+ public void onCreate_hasDynamicIconData_iconSet() {
+ mMetaData.putString(META_DATA_PREFERENCE_ICON_URI,
+ TEST_PROVIDER + "/getIcon/iconKey");
+
+ mPreferenceBundleMap.put(mPreference, mMetaData);
+ when(mExtraSettingsLoaderMock.loadPreferences(FAKE_INTENT)).thenReturn(
+ mPreferenceBundleMap);
+ mPreferenceController.setExtraSettingsLoader(mExtraSettingsLoaderMock);
+
+ mPreferenceController.onCreate(mLifecycleOwner);
+
+ InOrder inOrder = inOrder(mPreference);
+ inOrder.verify(mPreference).setIcon(R.drawable.ic_placeholder);
+ inOrder.verify(mPreference).setIcon(any(Drawable.class));
+ }
+
+ @Test
+ public void onStart_hasDynamicTitleData_observerAdded() {
+ mMetaData.putString(META_DATA_PREFERENCE_TITLE_URI, FAKE_PROVIDER);
+ mPreferenceBundleMap.put(mPreference, mMetaData);
+ when(mExtraSettingsLoaderMock.loadPreferences(FAKE_INTENT)).thenReturn(
+ mPreferenceBundleMap);
+ mPreferenceController.setExtraSettingsLoader(mExtraSettingsLoaderMock);
+
+ mPreferenceController.onCreate(mLifecycleOwner);
+ mPreferenceController.onStart(mLifecycleOwner);
+
+ assertThat(mPreferenceController.mObservers.size()).isEqualTo(1);
+ }
+
+ @Test
+ public void onStart_hasDynamicSummaryData_observerAdded() {
+ mMetaData.putString(META_DATA_PREFERENCE_SUMMARY_URI, FAKE_PROVIDER);
+ mPreferenceBundleMap.put(mPreference, mMetaData);
+ when(mExtraSettingsLoaderMock.loadPreferences(FAKE_INTENT)).thenReturn(
+ mPreferenceBundleMap);
+ mPreferenceController.setExtraSettingsLoader(mExtraSettingsLoaderMock);
+
+ mPreferenceController.onCreate(mLifecycleOwner);
+ mPreferenceController.onStart(mLifecycleOwner);
+
+ assertThat(mPreferenceController.mObservers.size()).isEqualTo(1);
+ }
+
+ @Test
+ public void onStart_hasDynamicIconData_observerAdded() {
+ mMetaData.putString(META_DATA_PREFERENCE_ICON_URI, FAKE_PROVIDER);
+ mPreferenceBundleMap.put(mPreference, mMetaData);
+ when(mExtraSettingsLoaderMock.loadPreferences(FAKE_INTENT)).thenReturn(
+ mPreferenceBundleMap);
+ mPreferenceController.setExtraSettingsLoader(mExtraSettingsLoaderMock);
+
+ mPreferenceController.onCreate(mLifecycleOwner);
+ mPreferenceController.onStart(mLifecycleOwner);
+
+ assertThat(mPreferenceController.mObservers.size()).isEqualTo(1);
+ }
+
private static class FakeExtraSettingsPreferenceController extends
ExtraSettingsPreferenceController {
@@ -154,6 +315,18 @@
}
@Override
+ void executeBackgroundTask(Runnable r) {
+ // run task immediately on main thread
+ r.run();
+ }
+
+ @Override
+ void executeUiTask(Runnable r) {
+ // run task immediately on main thread
+ r.run();
+ }
+
+ @Override
protected int getAvailabilityStatus() {
return mAvailabilityStatus;
}
diff --git a/tests/unit/src/com/android/car/settings/testutils/TestContentProvider.java b/tests/unit/src/com/android/car/settings/testutils/TestContentProvider.java
new file mode 100644
index 0000000..0e7f994
--- /dev/null
+++ b/tests/unit/src/com/android/car/settings/testutils/TestContentProvider.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 2020 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.car.settings.testutils;
+
+import android.content.ContentProvider;
+import android.content.ContentValues;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.text.TextUtils;
+
+import java.util.List;
+
+/**
+ * A simple content provider for tests. This provider runs in the same process as the test.
+ */
+public class TestContentProvider extends ContentProvider {
+
+ public static final String TEST_TEXT_CONTENT = "TestContentProviderText";
+
+ private static final String SCHEME = "content";
+ private static final String AUTHORITY =
+ "com.android.car.settings.testutils.TestContentProvider";
+ private static final String METHOD_GET_TEXT = "getText";
+ private static final String METHOD_GET_ICON = "getIcon";
+ private static final String KEY_PREFERENCE_TITLE = "com.android.settings.title";
+ private static final String KEY_PREFERENCE_SUMMARY = "com.android.settings.summary";
+ private static final String KEY_PREFERENCE_ICON = "com.android.settings.icon";
+ private static final String KEY_PREFERENCE_ICON_PACKAGE = "com.android.settings.icon_package";
+ private static final String TEXT_KEY = "textKey";
+ private static final String ICON_KEY = "iconKey";
+
+
+ @Override
+ public boolean onCreate() {
+ return true;
+ }
+
+ @Override
+ public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
+ String sortOrder) {
+ return null;
+ }
+
+ @Override
+ public String getType(Uri uri) {
+ return null;
+ }
+
+ @Override
+ public Uri insert(Uri uri, ContentValues values) {
+ return null;
+ }
+
+ @Override
+ public int delete(Uri uri, String selection, String[] selectionArgs) {
+ return 0;
+ }
+
+ @Override
+ public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
+ return 0;
+ }
+
+ @Override
+ public Bundle call(String methodName, String uriString, Bundle extras) {
+ if (TextUtils.isEmpty(methodName)) {
+ return null;
+ }
+ if (TextUtils.isEmpty(uriString)) {
+ return null;
+ }
+ Uri uri = Uri.parse(uriString);
+ if (uri == null) {
+ return null;
+ }
+ // Only process URIs with valid scheme, authority, and no assigned port.
+ if (!(SCHEME.equals(uri.getScheme())
+ && AUTHORITY.equals(uri.getAuthority())
+ && (uri.getPort() == -1))) {
+ return null;
+ }
+ List<String> pathSegments = uri.getPathSegments();
+ // Path segments should consist of the method name and the argument. If the number of path
+ // segments is not exactly two, the content provider is not being called as intended.
+ if ((pathSegments == null) || (pathSegments.size() != 2)) {
+ return null;
+ }
+ // The first path segment needs to match the methodName.
+ if (!methodName.equals(pathSegments.get(0))) {
+ return null;
+ }
+ String key = pathSegments.get(1);
+ if (METHOD_GET_TEXT.equals(methodName)) {
+ if (TEXT_KEY.equals(key)) {
+ Bundle bundle = new Bundle();
+ bundle.putString(KEY_PREFERENCE_TITLE, TEST_TEXT_CONTENT);
+ bundle.putString(KEY_PREFERENCE_SUMMARY, TEST_TEXT_CONTENT);
+ return bundle;
+ }
+ } else if (METHOD_GET_ICON.equals(methodName)) {
+ if (ICON_KEY.equals(key)) {
+ try {
+ String packageName = getContext().getPackageName();
+ PackageManager manager = getContext().getPackageManager();
+ Resources resources = manager.getResourcesForApplication(packageName);
+ int iconRes = resources.getIdentifier("test_icon", "drawable",
+ packageName);
+ if (iconRes == 0) {
+ return null;
+ }
+
+ Bundle bundle = new Bundle();
+ bundle.putInt(KEY_PREFERENCE_ICON, iconRes);
+ bundle.putString(KEY_PREFERENCE_ICON_PACKAGE, packageName);
+ return bundle;
+
+ } catch (PackageManager.NameNotFoundException e) {
+ return null;
+ }
+ }
+ }
+ return null;
+ }
+}