diff --git a/src/com/android/settings/SecuritySettings.java b/src/com/android/settings/SecuritySettings.java index 46e7fe57aad..b0928573c27 100644 --- a/src/com/android/settings/SecuritySettings.java +++ b/src/com/android/settings/SecuritySettings.java @@ -62,10 +62,13 @@ import com.android.settings.search.BaseSearchIndexProvider; import com.android.settings.search.Index; import com.android.settings.search.Indexable; import com.android.settings.search.SearchIndexableRaw; +import com.android.settings.security.SecurityFeatureProvider; import com.android.settingslib.RestrictedLockUtils; import com.android.settingslib.RestrictedPreference; import com.android.settingslib.RestrictedSwitchPreference; import com.android.settingslib.drawer.CategoryKey; +import com.android.settingslib.drawer.DashboardCategory; +import com.android.settingslib.drawer.Tile; import java.util.ArrayList; import java.util.List; @@ -130,6 +133,7 @@ public class SecuritySettings extends SettingsPreferenceFragment private DashboardFeatureProvider mDashboardFeatureProvider; private DevicePolicyManager mDPM; + private SecurityFeatureProvider mSecurityFeatureProvider; private SubscriptionManager mSubscriptionManager; private UserManager mUm; @@ -183,6 +187,8 @@ public class SecuritySettings extends SettingsPreferenceFragment mDashboardFeatureProvider = FeatureFactory.getFactory(activity) .getDashboardFeatureProvider(activity); + mSecurityFeatureProvider = FeatureFactory.getFactory(activity).getSecurityFeatureProvider(); + if (savedInstanceState != null && savedInstanceState.containsKey(TRUST_AGENT_CLICK_INTENT)) { mTrustAgentClickIntent = savedInstanceState.getParcelable(TRUST_AGENT_CLICK_INTENT); @@ -416,6 +422,11 @@ public class SecuritySettings extends SettingsPreferenceFragment } } + // Update preference data with tile data. Security feature provider only updates the data + // if it actually needs to be changed. + mSecurityFeatureProvider.updatePreferences(getActivity(), root, + mDashboardFeatureProvider.getTilesForCategory(CategoryKey.CATEGORY_SECURITY)); + for (int i = 0; i < SWITCH_PREFERENCE_KEYS.length; i++) { final Preference pref = findPreference(SWITCH_PREFERENCE_KEYS[i]); if (pref != null) pref.setOnPreferenceChangeListener(this); diff --git a/src/com/android/settings/overlay/FeatureFactory.java b/src/com/android/settings/overlay/FeatureFactory.java index 0b8ee8e1a98..8b6c3b172c2 100644 --- a/src/com/android/settings/overlay/FeatureFactory.java +++ b/src/com/android/settings/overlay/FeatureFactory.java @@ -27,6 +27,7 @@ import com.android.settings.dashboard.DashboardFeatureProvider; import com.android.settings.enterprise.EnterprisePrivacyFeatureProvider; import com.android.settings.fuelgauge.PowerUsageFeatureProvider; import com.android.settings.localepicker.LocaleFeatureProvider; +import com.android.settings.security.SecurityFeatureProvider; import com.android.settings.search2.SearchFeatureProvider; /** @@ -85,6 +86,8 @@ public abstract class FeatureFactory { public abstract SurveyFeatureProvider getSurveyFeatureProvider(Context context); + public abstract SecurityFeatureProvider getSecurityFeatureProvider(); + public static final class FactoryNotFoundException extends RuntimeException { public FactoryNotFoundException(Throwable throwable) { super("Unable to create factory. Did you misconfigure Proguard?", throwable); diff --git a/src/com/android/settings/overlay/FeatureFactoryImpl.java b/src/com/android/settings/overlay/FeatureFactoryImpl.java index c2d5d791cad..eb5d0657036 100644 --- a/src/com/android/settings/overlay/FeatureFactoryImpl.java +++ b/src/com/android/settings/overlay/FeatureFactoryImpl.java @@ -33,6 +33,8 @@ import com.android.settings.enterprise.EnterprisePrivacyFeatureProviderImpl; import com.android.settings.fuelgauge.PowerUsageFeatureProvider; import com.android.settings.localepicker.LocaleFeatureProvider; import com.android.settings.localepicker.LocaleFeatureProviderImpl; +import com.android.settings.security.SecurityFeatureProvider; +import com.android.settings.security.SecurityFeatureProviderImpl; import com.android.settings.search2.SearchFeatureProvider; import com.android.settings.search2.SearchFeatureProviderImpl; @@ -48,6 +50,7 @@ public class FeatureFactoryImpl extends FeatureFactory { private LocaleFeatureProvider mLocaleFeatureProvider; private EnterprisePrivacyFeatureProvider mEnterprisePrivacyFeatureProvider; private SearchFeatureProvider mSearchFeatureProvider; + private SecurityFeatureProvider mSecurityFeatureProvider; @Override public SupportFeatureProvider getSupportFeatureProvider(Context context) { @@ -115,4 +118,12 @@ public class FeatureFactoryImpl extends FeatureFactory { public SurveyFeatureProvider getSurveyFeatureProvider(Context context) { return null; } + + @Override + public SecurityFeatureProvider getSecurityFeatureProvider() { + if (mSecurityFeatureProvider == null) { + mSecurityFeatureProvider = new SecurityFeatureProviderImpl(); + } + return mSecurityFeatureProvider; + } } diff --git a/src/com/android/settings/security/SecurityFeatureProvider.java b/src/com/android/settings/security/SecurityFeatureProvider.java new file mode 100644 index 00000000000..5cf6fc99eb6 --- /dev/null +++ b/src/com/android/settings/security/SecurityFeatureProvider.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2016 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.security; + +import android.content.Context; +import android.support.v7.preference.PreferenceScreen; + +import com.android.settingslib.drawer.DashboardCategory; + + +/** FeatureProvider for security. */ +public interface SecurityFeatureProvider { + + /** Update preferences with data from associated tiles. */ + void updatePreferences(Context context, PreferenceScreen preferenceScreen, + DashboardCategory dashboardCategory); +} diff --git a/src/com/android/settings/security/SecurityFeatureProviderImpl.java b/src/com/android/settings/security/SecurityFeatureProviderImpl.java new file mode 100644 index 00000000000..91659fd275d --- /dev/null +++ b/src/com/android/settings/security/SecurityFeatureProviderImpl.java @@ -0,0 +1,117 @@ +/* + * Copyright (C) 2016 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.security; + +import android.content.Context; +import android.content.Intent; +import android.content.IContentProvider; +import android.content.pm.PackageManager; +import android.content.res.Resources; +import android.graphics.drawable.Drawable; +import com.android.settingslib.drawer.DashboardCategory; +import android.support.v4.content.ContextCompat; +import android.support.v7.preference.Preference; +import android.support.v7.preference.PreferenceScreen; +import android.text.TextUtils; +import android.util.ArrayMap; + +import com.android.settingslib.drawer.Tile; +import com.android.settingslib.drawer.TileUtils; + +import java.util.Map; + +/** Implementation for {@code SecurityFeatureProvider}. */ +public class SecurityFeatureProviderImpl implements SecurityFeatureProvider { + + /** Update preferences with data from associated tiles. */ + public void updatePreferences(Context context, PreferenceScreen preferenceScreen, + DashboardCategory dashboardCategory) { + if (preferenceScreen == null) { + return; + } + int tilesCount = (dashboardCategory != null) ? dashboardCategory.getTilesCount() : 0; + if (tilesCount == 0) { + return; + } + Map providerMap = new ArrayMap<>(); + for (int i = 0; i < tilesCount; i++) { + Tile tile = dashboardCategory.getTile(i); + // If the tile does not have a key or appropriate meta data, skip it. + if (TextUtils.isEmpty(tile.key) || (tile.metaData == null)) { + continue; + } + Preference matchingPref = preferenceScreen.findPreference(tile.key); + // If the tile does not have a matching preference, skip it. + if (matchingPref == null) { + continue; + } + // Check if the tile has content providers for dynamically updatable content. + String iconUri = tile.metaData.getString(TileUtils.META_DATA_PREFERENCE_ICON_URI, null); + String summaryUri = + tile.metaData.getString(TileUtils.META_DATA_PREFERENCE_SUMMARY_URI, null); + if (!TextUtils.isEmpty(iconUri)) { + int icon = TileUtils.getIconFromUri(context, iconUri, providerMap); + boolean updateIcon = true; + String packageName = null; + // Dynamic icon has to come from the same package that the preference launches. + 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(); + } + } + if (TextUtils.isEmpty(packageName)) { + updateIcon = false; + } else { + if (tile.icon == null) { + // If the tile does not have an icon already, only update if the suggested + // icon is non-zero. + updateIcon = (icon != 0); + } else { + // If the existing icon has the same resource package and resource id, the + // icon does not need to be updated. + updateIcon = !(packageName.equals(tile.icon.getResPackage()) + && (icon == tile.icon.getResId())); + } + } + if (updateIcon) { + try { + matchingPref.setIcon(context.getPackageManager() + .getResourcesForApplication(packageName) + .getDrawable(icon, context.getTheme())); + } catch (PackageManager.NameNotFoundException | Resources.NotFoundException e) { + // Intentionally ignored. If icon resources cannot be found, do not update. + } + } + } + if (!TextUtils.isEmpty(summaryUri)) { + String summary = TileUtils.getTextFromUri(context, summaryUri, providerMap, + TileUtils.META_DATA_PREFERENCE_SUMMARY); + // 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/security/SecurityFeatureProviderImplTest.java b/tests/robotests/src/com/android/settings/security/SecurityFeatureProviderImplTest.java new file mode 100644 index 00000000000..7df9af7fa9c --- /dev/null +++ b/tests/robotests/src/com/android/settings/security/SecurityFeatureProviderImplTest.java @@ -0,0 +1,186 @@ +/* + * Copyright (C) 2016 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.security; + +import android.content.IContentProvider; +import android.content.Intent; +import android.content.Context; +import android.content.pm.PackageManager; +import android.content.res.Resources; +import android.graphics.Color; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.PaintDrawable; +import android.os.Bundle; +import android.support.v7.preference.Preference; +import android.support.v7.preference.PreferenceScreen; + +import com.android.settings.SettingsRobolectricTestRunner; +import com.android.settings.TestConfig; +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; +import org.junit.runner.RunWith; +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 java.util.Collections; +import java.util.List; +import java.util.Map; + +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.anyString; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +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 { + + private static final String MOCK_KEY = "key"; + private static final String MOCK_SUMMARY = "summary"; + private static final String URI_GET_SUMMARY = "content://package/text/summary"; + private static final String URI_GET_ICON = "content://package/icon/my_icon"; + + private static final Drawable MOCK_DRAWABLE = new PaintDrawable(Color.BLUE); + + @Mock + private Context mContext; + @Mock + private PackageManager mPackageManager; + @Mock + private Resources mResources; + + private SecurityFeatureProviderImpl mImpl; + + @Implements(com.android.settingslib.drawer.TileUtils.class) + public static class TileUtilsMock { + @Implementation + public static int getIconFromUri(Context context, String uriString, + Map providerMap) { + return 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); + mContext = spy(RuntimeEnvironment.application); + mImpl = new SecurityFeatureProviderImpl(); + when(mContext.getPackageManager()).thenReturn(mPackageManager); + when(mPackageManager.getResourcesForApplication(anyString())).thenReturn(mResources); + when(mResources.getDrawable(anyInt(), any())).thenReturn(MOCK_DRAWABLE); + } + + @Test + public void updateTilesData_shouldNotProcessEmptyScreenOrTiles() { + mImpl.updatePreferences(mContext, null, null); + mImpl.updatePreferences(mContext, new PreferenceScreen(mContext, null), null); + verifyNoMoreInteractions(mPackageManager); + } + + @Test + public void updateTilesData_shouldNotProcessNonMatchingPreference() { + DashboardCategory dashboardCategory = new DashboardCategory(); + dashboardCategory.addTile(new Tile()); + mImpl.updatePreferences(mContext, getPreferenceScreen(), dashboardCategory); + verifyNoMoreInteractions(mPackageManager); + } + + @Test + public void updateTilesData_shouldNotProcessMatchingPreferenceWithNoData() { + mImpl.updatePreferences(mContext, getPreferenceScreen(), getDashboardCategory()); + verifyNoMoreInteractions(mPackageManager); + } + + @Test + @Config(shadows = { + TileUtilsMock.class, + }) + public void updateTilesData_shouldUpdateMatchingPreference() { + Bundle bundle = new Bundle(); + bundle.putString(TileUtils.META_DATA_PREFERENCE_ICON_URI, URI_GET_ICON); + bundle.putString(TileUtils.META_DATA_PREFERENCE_SUMMARY_URI, URI_GET_SUMMARY); + + PreferenceScreen screen = getPreferenceScreen(); + DashboardCategory dashboardCategory = getDashboardCategory(); + dashboardCategory.getTile(0).intent = new Intent().setPackage("package"); + dashboardCategory.getTile(0).metaData = bundle; + + mImpl.updatePreferences(mContext, getPreferenceScreen(), dashboardCategory); + assertThat(screen.findPreference(MOCK_KEY).getIcon()).isEqualTo(MOCK_DRAWABLE); + assertThat(screen.findPreference(MOCK_KEY).getSummary()).isEqualTo(MOCK_SUMMARY); + } + + @Test + @Config(shadows = { + TileUtilsMock.class, + }) + public void updateTilesData_shouldNotUpdateAlreadyUpdatedPreference() { + Bundle bundle = new Bundle(); + bundle.putString(TileUtils.META_DATA_PREFERENCE_ICON_URI, URI_GET_ICON); + bundle.putString(TileUtils.META_DATA_PREFERENCE_SUMMARY_URI, URI_GET_SUMMARY); + + PreferenceScreen screen = getPreferenceScreen(); + screen.findPreference(MOCK_KEY).setIcon(MOCK_DRAWABLE); + screen.findPreference(MOCK_KEY).setSummary(MOCK_SUMMARY); + + DashboardCategory dashboardCategory = getDashboardCategory(); + dashboardCategory.getTile(0).intent = new Intent().setPackage("package"); + dashboardCategory.getTile(0).metaData = bundle; + + mImpl.updatePreferences(mContext, screen, dashboardCategory); + verify(screen.findPreference(MOCK_KEY), times(0)).setIcon(any()); + verify(screen.findPreference(MOCK_KEY), times(0)).setSummary(anyString()); + assertThat(screen.findPreference(MOCK_KEY).getIcon()).isEqualTo(MOCK_DRAWABLE); + assertThat(screen.findPreference(MOCK_KEY).getSummary()).isEqualTo(MOCK_SUMMARY); + } + + private PreferenceScreen getPreferenceScreen() { + PreferenceScreen screen = new PreferenceScreen(mContext, null); + Preference preference = spy(new Preference(mContext)); + preference.setKey(MOCK_KEY); + screen.addPreference(preference); + return screen; + } + + private static DashboardCategory getDashboardCategory() { + DashboardCategory dashboardCategory = new DashboardCategory(); + Tile tile = new Tile(); + tile.key = MOCK_KEY; + dashboardCategory.addTile(tile); + return dashboardCategory; + } +} diff --git a/tests/robotests/src/com/android/settings/testutils/FakeFeatureFactory.java b/tests/robotests/src/com/android/settings/testutils/FakeFeatureFactory.java index 1625f352223..bc0894c4d9b 100644 --- a/tests/robotests/src/com/android/settings/testutils/FakeFeatureFactory.java +++ b/tests/robotests/src/com/android/settings/testutils/FakeFeatureFactory.java @@ -25,6 +25,7 @@ import com.android.settings.fuelgauge.PowerUsageFeatureProvider; import com.android.settings.localepicker.LocaleFeatureProvider; import com.android.settings.overlay.FeatureFactory; import com.android.settings.overlay.SupportFeatureProvider; +import com.android.settings.security.SecurityFeatureProvider; import com.android.settings.search2.SearchFeatureProvider; import com.android.settings.overlay.SurveyFeatureProvider; @@ -47,6 +48,7 @@ public class FakeFeatureFactory extends FeatureFactory { public final EnterprisePrivacyFeatureProvider enterprisePrivacyFeatureProvider; public final SearchFeatureProvider searchFeatureProvider; public final SurveyFeatureProvider surveyFeatureProvider; + public final SecurityFeatureProvider securityFeatureProvider; /** * Call this in {@code @Before} method of the test class to use fake factory. @@ -78,6 +80,7 @@ public class FakeFeatureFactory extends FeatureFactory { enterprisePrivacyFeatureProvider = mock(EnterprisePrivacyFeatureProvider.class); searchFeatureProvider = mock(SearchFeatureProvider.class); surveyFeatureProvider = mock(SurveyFeatureProvider.class); + securityFeatureProvider = mock(SecurityFeatureProvider.class); } @Override @@ -124,4 +127,9 @@ public class FakeFeatureFactory extends FeatureFactory { public SurveyFeatureProvider getSurveyFeatureProvider(Context context) { return surveyFeatureProvider; } + + @Override + public SecurityFeatureProvider getSecurityFeatureProvider() { + return securityFeatureProvider; + } }