From 26a1deffe6947c28cd4c53981a2035d14f511428 Mon Sep 17 00:00:00 2001 From: Fan Zhang Date: Wed, 26 Jul 2017 10:58:32 -0700 Subject: [PATCH] Support dynamic summary and icon for injected tiles Bug: 63758074 Test: robotests Change-Id: I2fa27812800c12606a613896ea2aa69dda393944 --- .../DashboardFeatureProviderImpl.java | 66 ++++++++++++++++--- .../settings/dashboard/DashboardFragment.java | 2 +- .../security/SecurityFeatureProviderImpl.java | 50 ++++++-------- .../settings/SecuritySettingsTest.java | 36 +++------- .../DashboardFeatureProviderImplTest.java | 63 +++++++++++++++--- .../SecurityFeatureProviderImplTest.java | 44 ++++--------- .../testutils/shadow/ShadowThreadUtils.java | 36 ++++++++++ .../testutils/shadow/ShadowTileUtils.java | 50 ++++++++++++++ 8 files changed, 239 insertions(+), 108 deletions(-) create mode 100644 tests/robotests/src/com/android/settings/testutils/shadow/ShadowThreadUtils.java create mode 100644 tests/robotests/src/com/android/settings/testutils/shadow/ShadowTileUtils.java diff --git a/src/com/android/settings/dashboard/DashboardFeatureProviderImpl.java b/src/com/android/settings/dashboard/DashboardFeatureProviderImpl.java index 88cf6664297..c0ce3b963ad 100644 --- a/src/com/android/settings/dashboard/DashboardFeatureProviderImpl.java +++ b/src/com/android/settings/dashboard/DashboardFeatureProviderImpl.java @@ -16,16 +16,25 @@ package com.android.settings.dashboard; +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 android.app.Activity; import android.content.ComponentName; import android.content.Context; +import android.content.IContentProvider; import android.content.Intent; import android.content.pm.PackageManager; +import android.graphics.drawable.Icon; import android.os.Bundle; import android.provider.Settings; +import android.support.annotation.VisibleForTesting; import android.support.v7.preference.Preference; import android.text.TextUtils; +import android.util.ArrayMap; import android.util.Log; +import android.util.Pair; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.settings.R; @@ -37,9 +46,12 @@ import com.android.settingslib.drawer.DashboardCategory; import com.android.settingslib.drawer.ProfileSelectDialog; import com.android.settingslib.drawer.SettingsDrawerActivity; import com.android.settingslib.drawer.Tile; +import com.android.settingslib.drawer.TileUtils; +import com.android.settingslib.utils.ThreadUtils; import java.util.ArrayList; import java.util.List; +import java.util.Map; /** * Impl for {@code DashboardFeatureProvider}. @@ -125,14 +137,8 @@ public class DashboardFeatureProviderImpl implements DashboardFeatureProvider { } else { pref.setKey(getDashboardKeyForTile(tile)); } - if (tile.summary != null) { - pref.setSummary(tile.summary); - } else { - pref.setSummary(R.string.summary_placeholder); - } - if (tile.icon != null) { - pref.setIcon(tile.icon.loadDrawable(activity)); - } + bindSummary(pref, tile); + bindIcon(pref, tile); final Bundle metadata = tile.metaData; String clsName = null; String action = null; @@ -206,6 +212,50 @@ public class DashboardFeatureProviderImpl implements DashboardFeatureProvider { launchIntentOrSelectProfile(activity, tile, intent, MetricsEvent.DASHBOARD_SUMMARY); } + private void bindSummary(Preference preference, Tile tile) { + if (tile.summary != null) { + preference.setSummary(tile.summary); + } else if (tile.metaData != null + && tile.metaData.containsKey(META_DATA_PREFERENCE_SUMMARY_URI)) { + ThreadUtils.postOnBackgroundThread(() -> { + final Map providerMap = new ArrayMap<>(); + final String uri = tile.metaData.getString(META_DATA_PREFERENCE_SUMMARY_URI); + final String summary = TileUtils.getTextFromUri( + mContext, uri, providerMap, META_DATA_PREFERENCE_SUMMARY); + ThreadUtils.postOnMainThread(() -> preference.setSummary(summary)); + }); + } else { + preference.setSummary(R.string.summary_placeholder); + } + } + + @VisibleForTesting + void bindIcon(Preference preference, Tile tile) { + if (tile.icon != null) { + preference.setIcon(tile.icon.loadDrawable(preference.getContext())); + } else if (tile.metaData != null + && tile.metaData.containsKey(META_DATA_PREFERENCE_ICON_URI)) + ThreadUtils.postOnBackgroundThread(() -> { + String packageName = null; + if (tile.intent != null) { + Intent intent = tile.intent; + if (!TextUtils.isEmpty(intent.getPackage())) { + packageName = intent.getPackage(); + } else if (intent.getComponent() != null) { + packageName = intent.getComponent().getPackageName(); + } + } + final Map providerMap = new ArrayMap<>(); + final String uri = tile.metaData.getString(META_DATA_PREFERENCE_ICON_URI); + final Pair iconInfo = TileUtils.getIconFromUri( + mContext, packageName, uri, providerMap); + tile.icon = Icon.createWithResource(iconInfo.first, iconInfo.second); + ThreadUtils.postOnMainThread(() -> + preference.setIcon(tile.icon.loadDrawable(preference.getContext())) + ); + }); + } + private void launchIntentOrSelectProfile(Activity activity, Tile tile, Intent intent, int sourceMetricCategory) { if (!isIntentResolvable(intent)) { diff --git a/src/com/android/settings/dashboard/DashboardFragment.java b/src/com/android/settings/dashboard/DashboardFragment.java index e8970ecd697..b920850d485 100644 --- a/src/com/android/settings/dashboard/DashboardFragment.java +++ b/src/com/android/settings/dashboard/DashboardFragment.java @@ -201,7 +201,7 @@ public abstract class DashboardFragment extends SettingsPreferenceFragment /** * Returns the CategoryKey for loading {@link DashboardCategory} for this fragment. */ - @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + @VisibleForTesting public String getCategoryKey() { return DashboardFragmentRegistry.PARENT_TO_CATEGORY_KEY_MAP.get(getClass().getName()); } diff --git a/src/com/android/settings/security/SecurityFeatureProviderImpl.java b/src/com/android/settings/security/SecurityFeatureProviderImpl.java index 47f7a1b821c..b8cf689797f 100644 --- a/src/com/android/settings/security/SecurityFeatureProviderImpl.java +++ b/src/com/android/settings/security/SecurityFeatureProviderImpl.java @@ -22,8 +22,6 @@ import android.content.Intent; import android.content.pm.PackageManager; import android.content.res.Resources; import android.graphics.drawable.Drawable; -import android.os.Handler; -import android.os.Looper; import android.support.annotation.VisibleForTesting; import android.support.v7.preference.Preference; import android.support.v7.preference.PreferenceScreen; @@ -37,10 +35,10 @@ import com.android.settings.trustagent.TrustAgentManagerImpl; import com.android.settingslib.drawer.DashboardCategory; import com.android.settingslib.drawer.Tile; import com.android.settingslib.drawer.TileUtils; +import com.android.settingslib.utils.ThreadUtils; import java.util.Map; import java.util.TreeMap; -import java.util.concurrent.Executors; /** Implementation for {@code SecurityFeatureProvider}. */ public class SecurityFeatureProviderImpl implements SecurityFeatureProvider { @@ -71,12 +69,8 @@ public class SecurityFeatureProviderImpl implements SecurityFeatureProvider { // Fetching the summary and icon from the provider introduces latency, so do this on a // separate thread. - Executors.newSingleThreadExecutor().execute(new Runnable() { - @Override - public void run() { - updatePreferencesToRunOnWorkerThread(context, preferenceScreen, dashboardCategory); - } - }); + ThreadUtils.postOnBackgroundThread(() -> + updatePreferencesToRunOnWorkerThread(context, preferenceScreen, dashboardCategory)); } @VisibleForTesting @@ -162,19 +156,16 @@ public class SecurityFeatureProviderImpl implements SecurityFeatureProvider { sIconCache.put(iconUri, icon); // Icon is only returned if the icon belongs to Settings or the target app. // setIcon must be called on the UI thread. - new Handler(Looper.getMainLooper()).post(new Runnable() { - @Override - public void run() { - try { - matchingPref.setIcon(context.getPackageManager() - .getResourcesForApplication(icon.first /* package name */) - .getDrawable(icon.second /* res id */, - context.getTheme())); - } catch (PackageManager.NameNotFoundException - | Resources.NotFoundException e) { - // Intentionally ignored. If icon resources cannot be found, do not - // update. - } + ThreadUtils.postOnMainThread(() -> { + try { + matchingPref.setIcon(context.getPackageManager() + .getResourcesForApplication(icon.first /* package name */) + .getDrawable(icon.second /* res id */, + context.getTheme())); + } catch (PackageManager.NameNotFoundException + | Resources.NotFoundException e) { + // Intentionally ignored. If icon resources cannot be found, do not + // update. } }); } @@ -184,17 +175,14 @@ public class SecurityFeatureProviderImpl implements SecurityFeatureProvider { TileUtils.META_DATA_PREFERENCE_SUMMARY); sSummaryCache.put(summaryUri, summary); // setSummary must be called on UI thread. - new Handler(Looper.getMainLooper()).post(new Runnable() { - @Override - public void run() { - // Only update the summary if it has actually changed. - if (summary == null) { - if (matchingPref.getSummary() != null) { - matchingPref.setSummary(summary); - } - } else if (!summary.equals(matchingPref.getSummary())) { + ThreadUtils.postOnMainThread(() -> { + // Only update the summary if it has actually changed. + if (summary == null) { + if (matchingPref.getSummary() != null) { matchingPref.setSummary(summary); } + } else if (!summary.equals(matchingPref.getSummary())) { + matchingPref.setSummary(summary); } }); } diff --git a/tests/robotests/src/com/android/settings/SecuritySettingsTest.java b/tests/robotests/src/com/android/settings/SecuritySettingsTest.java index 5838642b635..9600936cb22 100644 --- a/tests/robotests/src/com/android/settings/SecuritySettingsTest.java +++ b/tests/robotests/src/com/android/settings/SecuritySettingsTest.java @@ -16,9 +16,17 @@ package com.android.settings; +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Matchers.anyInt; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + import android.app.Activity; import android.content.Context; -import android.content.IContentProvider; import android.content.pm.PackageManager; import android.hardware.fingerprint.FingerprintManager; import android.os.UserManager; @@ -33,7 +41,7 @@ import com.android.settings.testutils.FakeFeatureFactory; import com.android.settings.testutils.SettingsRobolectricTestRunner; import com.android.settings.testutils.XmlTestUtils; import com.android.settings.testutils.shadow.ShadowLockPatternUtils; -import com.android.settingslib.drawer.DashboardCategory; + import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -42,47 +50,23 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Config; -import org.robolectric.annotation.Implementation; -import org.robolectric.annotation.Implements; import org.robolectric.shadows.ShadowApplication; import org.robolectric.util.ReflectionHelpers; import java.util.List; -import java.util.Map; - -import static com.google.common.truth.Truth.assertThat; -import static org.mockito.Matchers.anyInt; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoMoreInteractions; -import static org.mockito.Mockito.when; @RunWith(SettingsRobolectricTestRunner.class) @Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION) public class SecuritySettingsTest { - private static final String MOCK_SUMMARY = "summary"; @Mock(answer = Answers.RETURNS_DEEP_STUBS) private Context mContext; @Mock - private DashboardCategory mDashboardCategory; - @Mock private SummaryLoader mSummaryLoader; private SecuritySettings.SummaryProvider mSummaryProvider; - @Implements(com.android.settingslib.drawer.TileUtils.class) - public static class ShadowTileUtils { - @Implementation - public static String getTextFromUri(Context context, String uriString, - Map providerMap, String key) { - return MOCK_SUMMARY; - } - } - @Before public void setUp() { MockitoAnnotations.initMocks(this); diff --git a/tests/robotests/src/com/android/settings/dashboard/DashboardFeatureProviderImplTest.java b/tests/robotests/src/com/android/settings/dashboard/DashboardFeatureProviderImplTest.java index 2791da3f6a7..ef907542714 100644 --- a/tests/robotests/src/com/android/settings/dashboard/DashboardFeatureProviderImplTest.java +++ b/tests/robotests/src/com/android/settings/dashboard/DashboardFeatureProviderImplTest.java @@ -16,6 +16,15 @@ package com.android.settings.dashboard; +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyInt; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.robolectric.Shadows.shadowOf; + import android.app.Activity; import android.content.ComponentName; import android.content.Context; @@ -31,14 +40,17 @@ import android.support.v7.preference.Preference; import com.android.internal.logging.nano.MetricsProto; import com.android.settings.R; import com.android.settings.SettingsActivity; -import com.android.settings.testutils.SettingsRobolectricTestRunner; import com.android.settings.TestConfig; import com.android.settings.testutils.FakeFeatureFactory; +import com.android.settings.testutils.SettingsRobolectricTestRunner; +import com.android.settings.testutils.shadow.ShadowThreadUtils; +import com.android.settings.testutils.shadow.ShadowTileUtils; import com.android.settings.testutils.shadow.ShadowUserManager; import com.android.settingslib.drawer.CategoryKey; import com.android.settingslib.drawer.CategoryManager; import com.android.settingslib.drawer.DashboardCategory; import com.android.settingslib.drawer.Tile; +import com.android.settingslib.drawer.TileUtils; import org.junit.Before; import org.junit.Test; @@ -55,15 +67,6 @@ import org.robolectric.util.ReflectionHelpers; import java.util.ArrayList; -import static com.google.common.truth.Truth.assertThat; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.anyInt; -import static org.mockito.Matchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; -import static org.robolectric.Shadows.shadowOf; - @RunWith(SettingsRobolectricTestRunner.class) @Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION, @@ -245,6 +248,26 @@ public class DashboardFeatureProviderImplTest { assertThat(preference.getSummary()).isEqualTo(tile.summary); } + @Test + @Config(shadows = { + ShadowTileUtils.class, + ShadowThreadUtils.class + }) + public void bindPreference_hasSummaryUri_shouldLoadSummaryFromContentProvider() { + final Preference preference = new Preference(RuntimeEnvironment.application); + final Tile tile = new Tile(); + tile.intent = new Intent(); + tile.intent.setComponent(new ComponentName("pkg", "class")); + tile.metaData = new Bundle(); + tile.metaData.putString(TileUtils.META_DATA_PREFERENCE_SUMMARY_URI, + "content://com.android.settings/tile_summary"); + + mImpl.bindPreferenceToTile(mActivity, MetricsProto.MetricsEvent.VIEW_UNKNOWN, + preference, tile, null /*key */, Preference.DEFAULT_ORDER); + + assertThat(preference.getSummary()).isEqualTo(ShadowTileUtils.MOCK_SUMMARY); + } + @Test public void bindPreference_withNullKeyTileKey_shouldUseTileKey() { final Preference preference = new Preference(RuntimeEnvironment.application); @@ -258,6 +281,26 @@ public class DashboardFeatureProviderImplTest { assertThat(preference.getKey()).isEqualTo(tile.key); } + @Test + @Config(shadows = { + ShadowTileUtils.class, + ShadowThreadUtils.class + }) + public void bindPreference_withIconUri_shouldLoadIconFromContentProvider() { + final Preference preference = new Preference(RuntimeEnvironment.application); + final Tile tile = new Tile(); + tile.key = "key"; + tile.intent = new Intent(); + tile.intent.setComponent( + new ComponentName(RuntimeEnvironment.application.getPackageName(), "class")); + tile.metaData = new Bundle(); + tile.metaData.putString(TileUtils.META_DATA_PREFERENCE_ICON_URI, + "content://com.android.settings/tile_icon"); + mImpl.bindIcon(preference, tile); + + assertThat(tile.icon).isNotNull(); + } + @Test public void bindPreference_withBaseOrder_shouldOffsetPriority() { final int baseOrder = 100; diff --git a/tests/robotests/src/com/android/settings/security/SecurityFeatureProviderImplTest.java b/tests/robotests/src/com/android/settings/security/SecurityFeatureProviderImplTest.java index 8c5e8e59756..627ecf507dc 100644 --- a/tests/robotests/src/com/android/settings/security/SecurityFeatureProviderImplTest.java +++ b/tests/robotests/src/com/android/settings/security/SecurityFeatureProviderImplTest.java @@ -16,8 +16,17 @@ package com.android.settings.security; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyInt; +import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + import android.content.Context; -import android.content.IContentProvider; import android.content.Intent; import android.content.pm.PackageManager; import android.content.res.Resources; @@ -25,11 +34,11 @@ import android.graphics.drawable.Drawable; import android.os.Bundle; import android.support.v7.preference.Preference; import android.support.v7.preference.PreferenceScreen; -import android.util.Pair; -import com.android.settings.testutils.SettingsRobolectricTestRunner; import com.android.settings.R; import com.android.settings.TestConfig; +import com.android.settings.testutils.SettingsRobolectricTestRunner; +import com.android.settings.testutils.shadow.ShadowTileUtils; import com.android.settingslib.drawer.DashboardCategory; import com.android.settingslib.drawer.Tile; import com.android.settingslib.drawer.TileUtils; @@ -41,22 +50,8 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Config; -import org.robolectric.annotation.Implementation; -import org.robolectric.annotation.Implements; import org.robolectric.shadows.ShadowLooper; -import java.util.Map; - -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.anyInt; -import static org.mockito.Matchers.anyString; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoMoreInteractions; -import static org.mockito.Mockito.when; - @RunWith(SettingsRobolectricTestRunner.class) @Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION) public class SecurityFeatureProviderImplTest { @@ -77,21 +72,6 @@ public class SecurityFeatureProviderImplTest { private SecurityFeatureProviderImpl mImpl; - @Implements(com.android.settingslib.drawer.TileUtils.class) - public static class ShadowTileUtils { - @Implementation - public static Pair getIconFromUri(Context context, String packageName, String uriString, - Map providerMap) { - return Pair.create("package", 161803); - } - - @Implementation - public static String getTextFromUri(Context context, String uriString, - Map providerMap, String key) { - return MOCK_SUMMARY; - } - } - @Before public void setUp() throws PackageManager.NameNotFoundException { MockitoAnnotations.initMocks(this); diff --git a/tests/robotests/src/com/android/settings/testutils/shadow/ShadowThreadUtils.java b/tests/robotests/src/com/android/settings/testutils/shadow/ShadowThreadUtils.java new file mode 100644 index 00000000000..6b0411e9ab0 --- /dev/null +++ b/tests/robotests/src/com/android/settings/testutils/shadow/ShadowThreadUtils.java @@ -0,0 +1,36 @@ +/* + * 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. + */ + +package com.android.settings.testutils.shadow; + +import com.android.settingslib.utils.ThreadUtils; + +import org.robolectric.annotation.Implementation; +import org.robolectric.annotation.Implements; + +@Implements(ThreadUtils.class) +public class ShadowThreadUtils { + + @Implementation + public static void postOnBackgroundThread(Runnable runnable) { + runnable.run(); + } + + @Implementation + public static void postOnMainThread(Runnable runnable) { + runnable.run(); + } +} diff --git a/tests/robotests/src/com/android/settings/testutils/shadow/ShadowTileUtils.java b/tests/robotests/src/com/android/settings/testutils/shadow/ShadowTileUtils.java new file mode 100644 index 00000000000..1409daa23dc --- /dev/null +++ b/tests/robotests/src/com/android/settings/testutils/shadow/ShadowTileUtils.java @@ -0,0 +1,50 @@ +/* + * 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. + */ + +package com.android.settings.testutils.shadow; + +import android.content.Context; +import android.content.IContentProvider; +import android.util.Pair; + +import com.android.settings.R; +import com.android.settingslib.drawer.TileUtils; + +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Implementation; +import org.robolectric.annotation.Implements; + +import java.util.Map; + +@Implements(TileUtils.class) +public class ShadowTileUtils { + + public static final int ICON_RES_ID = R.drawable.ic_settings; + public static final String MOCK_SUMMARY = "summary"; + + @Implementation + public static String getTextFromUri(Context context, String uriString, + Map providerMap, String key) { + return MOCK_SUMMARY; + } + + @Implementation + public static Pair getIconFromUri(Context context, String packageName, + String uriString, Map providerMap) { + return Pair.create(RuntimeEnvironment.application.getPackageName(), ICON_RES_ID); + } + +}