From 8ffcbccbce2dea0e5ba23137d9098fb6dd938e59 Mon Sep 17 00:00:00 2001 From: Stephen Chen Date: Fri, 1 Sep 2017 11:18:36 -0700 Subject: [PATCH 01/17] Make auto Wi-Fi summary text consistent with feature availability. "Turn Wi-Fi on automatically" feature is disabled when airplane mode or battery saver mode is enabled, when Wi-Fi scanning is disabled, or when network recommendations are disabled / scorer unset. This change captures these negative cases in the summary text. Bug: 65085700 Test: make ROBOTEST_FILTER=WifiWakeupPreferenceControllerTest RunSettingsRoboTests -j40 Change-Id: I2f2d22b5bef3ad03a28d34e79a27e6545cac557f --- .../settings/wifi/ConfigureWifiSettings.java | 2 +- .../android/settings/wifi/WifiSettings.java | 20 ++++++++++++++++--- .../wifi/WifiWakeupPreferenceController.java | 11 +++------- .../WifiWakeupPreferenceControllerTest.java | 18 ++++++----------- 4 files changed, 27 insertions(+), 24 deletions(-) diff --git a/src/com/android/settings/wifi/ConfigureWifiSettings.java b/src/com/android/settings/wifi/ConfigureWifiSettings.java index aeee6ffa011..43f25f11977 100644 --- a/src/com/android/settings/wifi/ConfigureWifiSettings.java +++ b/src/com/android/settings/wifi/ConfigureWifiSettings.java @@ -78,7 +78,7 @@ public class ConfigureWifiSettings extends DashboardFragment { final NetworkScoreManagerWrapper networkScoreManagerWrapper = new NetworkScoreManagerWrapper(context.getSystemService(NetworkScoreManager.class)); mWifiWakeupPreferenceController = new WifiWakeupPreferenceController( - context, getLifecycle(), networkScoreManagerWrapper); + context, getLifecycle()); mUseOpenWifiPreferenceController = new UseOpenWifiPreferenceController(context, this, networkScoreManagerWrapper, getLifecycle()); final WifiManager wifiManager = (WifiManager) getSystemService(WIFI_SERVICE); diff --git a/src/com/android/settings/wifi/WifiSettings.java b/src/com/android/settings/wifi/WifiSettings.java index f0457e65d44..f99d13fc00e 100644 --- a/src/com/android/settings/wifi/WifiSettings.java +++ b/src/com/android/settings/wifi/WifiSettings.java @@ -38,6 +38,7 @@ import android.net.wifi.WpsInfo; import android.nfc.NfcAdapter; import android.os.Bundle; import android.os.HandlerThread; +import android.os.PowerManager; import android.os.Process; import android.provider.Settings; import android.support.annotation.VisibleForTesting; @@ -910,9 +911,8 @@ public class WifiSettings extends RestrictedSettingsFragment getContentResolver(), Settings.Global.WIFI_WAKEUP_AVAILABLE, defaultWakeupAvailable) == 1; if (wifiWakeupAvailable) { - boolean wifiWakeupEnabled = Settings.Global.getInt( - getContentResolver(), Settings.Global.WIFI_WAKEUP_ENABLED, 0) == 1; - mConfigureWifiSettingsPreference.setSummary(getString(wifiWakeupEnabled + mConfigureWifiSettingsPreference.setSummary(getString( + isWifiWakeupEnabled() ? R.string.wifi_configure_settings_preference_summary_wakeup_on : R.string.wifi_configure_settings_preference_summary_wakeup_off)); } @@ -927,6 +927,20 @@ public class WifiSettings extends RestrictedSettingsFragment } } + private boolean isWifiWakeupEnabled() { + PowerManager powerManager = (PowerManager) getSystemService(Context.POWER_SERVICE); + ContentResolver contentResolver = getContentResolver(); + return Settings.Global.getInt(contentResolver, + Settings.Global.WIFI_WAKEUP_ENABLED, 0) == 1 + && Settings.Global.getInt(contentResolver, + Settings.Global.WIFI_SCAN_ALWAYS_AVAILABLE, 0) == 1 + && Settings.Global.getInt(contentResolver, + Settings.Global.AIRPLANE_MODE_ON, 0) == 0 + && Settings.Global.getInt(contentResolver, + Settings.Global.NETWORK_RECOMMENDATIONS_ENABLED, 0) == 1 + && !powerManager.isPowerSaveMode(); + } + private void setOffMessage() { final CharSequence title = getText(R.string.wifi_empty_list_wifi_off); // Don't use WifiManager.isScanAlwaysAvailable() to check the Wi-Fi scanning mode. Instead, diff --git a/src/com/android/settings/wifi/WifiWakeupPreferenceController.java b/src/com/android/settings/wifi/WifiWakeupPreferenceController.java index 0017a5d7c4a..efb8aa6f84a 100644 --- a/src/com/android/settings/wifi/WifiWakeupPreferenceController.java +++ b/src/com/android/settings/wifi/WifiWakeupPreferenceController.java @@ -44,13 +44,10 @@ public class WifiWakeupPreferenceController extends AbstractPreferenceController implements PreferenceControllerMixin, LifecycleObserver, OnResume, OnPause { private static final String KEY_ENABLE_WIFI_WAKEUP = "enable_wifi_wakeup"; - private final NetworkScoreManagerWrapper mNetworkScoreManager; private SettingObserver mSettingObserver; - public WifiWakeupPreferenceController( - Context context, Lifecycle lifecycle, NetworkScoreManagerWrapper networkScoreManager) { + public WifiWakeupPreferenceController(Context context, Lifecycle lifecycle) { super(context); - mNetworkScoreManager = networkScoreManager; lifecycle.addObserver(this); } @@ -116,11 +113,9 @@ public class WifiWakeupPreferenceController extends AbstractPreferenceController boolean networkRecommendationsEnabled = Settings.Global.getInt( mContext.getContentResolver(), Settings.Global.NETWORK_RECOMMENDATIONS_ENABLED, 0) == 1; - boolean activeScorerSet = mNetworkScoreManager.getActiveScorerPackage() != null; - enableWifiWakeup.setEnabled( - networkRecommendationsEnabled && wifiScanningEnabled && activeScorerSet); + enableWifiWakeup.setEnabled(networkRecommendationsEnabled && wifiScanningEnabled); - if (!activeScorerSet) { + if (!networkRecommendationsEnabled) { enableWifiWakeup.setSummary(R.string.wifi_wakeup_summary_scoring_disabled); } else if (!wifiScanningEnabled) { enableWifiWakeup.setSummary(R.string.wifi_wakeup_summary_scanning_disabled); diff --git a/tests/robotests/src/com/android/settings/wifi/WifiWakeupPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/wifi/WifiWakeupPreferenceControllerTest.java index 8be686e64ea..306b297c69f 100644 --- a/tests/robotests/src/com/android/settings/wifi/WifiWakeupPreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/wifi/WifiWakeupPreferenceControllerTest.java @@ -56,8 +56,6 @@ public class WifiWakeupPreferenceControllerTest { private static final String TEST_SCORER_PACKAGE_NAME = "Test Scorer"; private Context mContext; - @Mock - private NetworkScoreManagerWrapper mNetworkScorer; private WifiWakeupPreferenceController mController; @Before @@ -65,11 +63,11 @@ public class WifiWakeupPreferenceControllerTest { MockitoAnnotations.initMocks(this); mContext = RuntimeEnvironment.application; mController = new WifiWakeupPreferenceController( - mContext, mock(Lifecycle.class), mNetworkScorer); + mContext, mock(Lifecycle.class)); Settings.System.putInt(mContext.getContentResolver(), WIFI_SCAN_ALWAYS_AVAILABLE, 1); + Settings.System.putInt(mContext.getContentResolver(), NETWORK_RECOMMENDATIONS_ENABLED, 1); SettingsShadowResources.overrideResource( com.android.internal.R.integer.config_wifi_wakeup_available, 0); - when(mNetworkScorer.getActiveScorerPackage()).thenReturn(TEST_SCORER_PACKAGE_NAME); } @After @@ -116,9 +114,8 @@ public class WifiWakeupPreferenceControllerTest { } @Test - public void updateState_preferenceSetCheckedAndSetEnabledWhenSettingsAreEnabled() { + public void updateState_preferenceSetCheckedAndSetEnabledWhenWakeupSettingEnabled() { final SwitchPreference preference = mock(SwitchPreference.class); - Settings.System.putInt(mContext.getContentResolver(), NETWORK_RECOMMENDATIONS_ENABLED, 1); Settings.System.putInt(mContext.getContentResolver(), WIFI_WAKEUP_ENABLED, 1); mController.updateState(preference); @@ -129,22 +126,20 @@ public class WifiWakeupPreferenceControllerTest { } @Test - public void updateState_preferenceSetCheckedAndSetEnabledWhenSettingsAreDisabled() { + public void updateState_preferenceSetUncheckedAndSetEnabledWhenWakeupSettingDisabled() { final SwitchPreference preference = mock(SwitchPreference.class); - Settings.System.putInt(mContext.getContentResolver(), NETWORK_RECOMMENDATIONS_ENABLED, 0); Settings.System.putInt(mContext.getContentResolver(), WIFI_WAKEUP_ENABLED, 0); mController.updateState(preference); verify(preference).setChecked(false); - verify(preference).setEnabled(false); + verify(preference).setEnabled(true); verify(preference).setSummary(R.string.wifi_wakeup_summary); } @Test public void updateState_preferenceSetUncheckedAndSetDisabledWhenWifiScanningDisabled() { final SwitchPreference preference = mock(SwitchPreference.class); - Settings.System.putInt(mContext.getContentResolver(), NETWORK_RECOMMENDATIONS_ENABLED, 1); Settings.System.putInt(mContext.getContentResolver(), WIFI_WAKEUP_ENABLED, 1); Settings.System.putInt(mContext.getContentResolver(), WIFI_SCAN_ALWAYS_AVAILABLE, 0); @@ -158,9 +153,8 @@ public class WifiWakeupPreferenceControllerTest { @Test public void updateState_preferenceSetUncheckedAndSetDisabledWhenScoringDisabled() { final SwitchPreference preference = mock(SwitchPreference.class); - Settings.System.putInt(mContext.getContentResolver(), NETWORK_RECOMMENDATIONS_ENABLED, 1); Settings.System.putInt(mContext.getContentResolver(), WIFI_WAKEUP_ENABLED, 1); - when(mNetworkScorer.getActiveScorerPackage()).thenReturn(null); + Settings.System.putInt(mContext.getContentResolver(), NETWORK_RECOMMENDATIONS_ENABLED, 0); mController.updateState(preference); From fb302de0d649eae3d86aa842680d69125239502f Mon Sep 17 00:00:00 2001 From: Daniel Nishi Date: Thu, 10 Aug 2017 14:09:19 -0700 Subject: [PATCH 02/17] Cache storage values for fast loading. If the user moves away from the storage fragment and returns, this allows us to use cached data from the previous calculation. If the data is > 1 minute old, we consider it stale. Otherwise, we can bypass the loading screen. Fixes: 37923463 Test: Settings Robotest Change-Id: I7650d4d742852f8d447878c077b9190bc0a0bb22 --- .../deviceinfo/StorageDashboardFragment.java | 51 ++- .../storage/CachedStorageValuesHelper.java | 172 ++++++++++ .../StorageDashboardFragmentTest.java | 46 +++ .../CachedStorageValuesHelperTest.java | 295 ++++++++++++++++++ 4 files changed, 563 insertions(+), 1 deletion(-) create mode 100644 src/com/android/settings/deviceinfo/storage/CachedStorageValuesHelper.java create mode 100644 tests/robotests/src/com/android/settings/deviceinfo/storage/CachedStorageValuesHelperTest.java diff --git a/src/com/android/settings/deviceinfo/StorageDashboardFragment.java b/src/com/android/settings/deviceinfo/StorageDashboardFragment.java index 33d7d36b85c..dd0db9a2335 100644 --- a/src/com/android/settings/deviceinfo/StorageDashboardFragment.java +++ b/src/com/android/settings/deviceinfo/StorageDashboardFragment.java @@ -40,6 +40,7 @@ import com.android.settings.applications.UserManagerWrapper; import com.android.settings.applications.UserManagerWrapperImpl; import com.android.settings.dashboard.DashboardFragment; import com.android.settings.deviceinfo.storage.AutomaticStorageManagementSwitchPreferenceController; +import com.android.settings.deviceinfo.storage.CachedStorageValuesHelper; import com.android.settings.deviceinfo.storage.SecondaryUserController; import com.android.settings.deviceinfo.storage.StorageAsyncLoader; import com.android.settings.deviceinfo.storage.StorageItemPreferenceController; @@ -68,6 +69,7 @@ public class StorageDashboardFragment extends DashboardFragment private VolumeInfo mVolume; private PrivateStorageInfo mStorageInfo; private SparseArray mAppsResult; + private CachedStorageValuesHelper mCachedStorageValuesHelper; private StorageSummaryDonutPreferenceController mSummaryController; private StorageItemPreferenceController mPreferenceController; @@ -102,7 +104,10 @@ public class StorageDashboardFragment extends DashboardFragment @Override public void onViewCreated(View v, Bundle savedInstanceState) { super.onViewCreated(v, savedInstanceState); - setLoading(true, false); + initializeCacheProvider(); + if (mAppsResult == null || mStorageInfo == null) { + setLoading(true, false); + } } @Override @@ -249,6 +254,7 @@ public class StorageDashboardFragment extends DashboardFragment public void onLoadFinished(Loader> loader, SparseArray data) { mAppsResult = data; + maybeCacheFreshValues(); onReceivedSizes(); } @@ -256,6 +262,48 @@ public class StorageDashboardFragment extends DashboardFragment public void onLoaderReset(Loader> loader) { } + @VisibleForTesting + public void setCachedStorageValuesHelper(CachedStorageValuesHelper helper) { + mCachedStorageValuesHelper = helper; + } + + @VisibleForTesting + public PrivateStorageInfo getPrivateStorageInfo() { + return mStorageInfo; + } + + @VisibleForTesting + public SparseArray getAppsStorageResult() { + return mAppsResult; + } + + @VisibleForTesting + public void initializeCachedValues() { + PrivateStorageInfo info = mCachedStorageValuesHelper.getCachedPrivateStorageInfo(); + SparseArray loaderResult = + mCachedStorageValuesHelper.getCachedAppsStorageResult(); + if (info == null || loaderResult == null) { + return; + } + + mStorageInfo = info; + mAppsResult = loaderResult; + } + + private void initializeCacheProvider() { + mCachedStorageValuesHelper = + new CachedStorageValuesHelper(getContext(), UserHandle.myUserId()); + initializeCachedValues(); + onReceivedSizes(); + } + + private void maybeCacheFreshValues() { + if (mStorageInfo != null && mAppsResult != null) { + mCachedStorageValuesHelper.cacheResult( + mStorageInfo, mAppsResult.get(UserHandle.myUserId())); + } + } + /** * IconLoaderCallbacks exists because StorageDashboardFragment already implements * LoaderCallbacks for a different type. @@ -308,6 +356,7 @@ public class StorageDashboardFragment extends DashboardFragment } mStorageInfo = privateStorageInfo; + maybeCacheFreshValues(); onReceivedSizes(); } } diff --git a/src/com/android/settings/deviceinfo/storage/CachedStorageValuesHelper.java b/src/com/android/settings/deviceinfo/storage/CachedStorageValuesHelper.java new file mode 100644 index 00000000000..8225db39434 --- /dev/null +++ b/src/com/android/settings/deviceinfo/storage/CachedStorageValuesHelper.java @@ -0,0 +1,172 @@ +/* + * 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.deviceinfo.storage; + +import android.content.Context; +import android.content.SharedPreferences; +import android.provider.Settings; +import android.support.annotation.VisibleForTesting; +import android.util.SparseArray; + +import com.android.settingslib.applications.StorageStatsSource; +import com.android.settingslib.deviceinfo.PrivateStorageInfo; + +import java.util.concurrent.TimeUnit; + +public class CachedStorageValuesHelper { + + @VisibleForTesting public static final String SHARED_PREFERENCES_NAME = "CachedStorageValues"; + public static final String TIMESTAMP_KEY = "last_query_timestamp"; + public static final String FREE_BYTES_KEY = "free_bytes"; + public static final String TOTAL_BYTES_KEY = "total_bytes"; + public static final String GAME_APPS_SIZE_KEY = "game_apps_size"; + public static final String MUSIC_APPS_SIZE_KEY = "music_apps_size"; + public static final String VIDEO_APPS_SIZE_KEY = "video_apps_size"; + public static final String PHOTO_APPS_SIZE_KEY = "photo_apps_size"; + public static final String OTHER_APPS_SIZE_KEY = "other_apps_size"; + public static final String CACHE_APPS_SIZE_KEY = "cache_apps_size"; + public static final String EXTERNAL_TOTAL_BYTES = "external_total_bytes"; + public static final String EXTERNAL_AUDIO_BYTES = "external_audio_bytes"; + public static final String EXTERNAL_VIDEO_BYTES = "external_video_bytes"; + public static final String EXTERNAL_IMAGE_BYTES = "external_image_bytes"; + public static final String EXTERNAL_APP_BYTES = "external_apps_bytes"; + public static final String USER_ID_KEY = "user_id"; + private final Long mClobberThreshold; + private final SharedPreferences mSharedPreferences; + private final int mUserId; + // This clock is used to provide the time. By default, it uses the system clock, but can be + // replaced for test purposes. + protected Clock mClock; + + public CachedStorageValuesHelper(Context context, int userId) { + mSharedPreferences = + context.getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE); + mClock = new Clock(); + mUserId = userId; + mClobberThreshold = + Settings.Global.getLong( + context.getContentResolver(), + Settings.Global.STORAGE_SETTINGS_CLOBBER_THRESHOLD, + TimeUnit.MINUTES.toMillis(5)); + } + + public PrivateStorageInfo getCachedPrivateStorageInfo() { + if (!isDataValid()) { + return null; + } + final long freeBytes = mSharedPreferences.getLong(FREE_BYTES_KEY, -1); + final long totalBytes = mSharedPreferences.getLong(TOTAL_BYTES_KEY, -1); + if (freeBytes < 0 || totalBytes < 0) { + return null; + } + + return new PrivateStorageInfo(freeBytes, totalBytes); + } + + public SparseArray getCachedAppsStorageResult() { + if (!isDataValid()) { + return null; + } + final long gamesSize = mSharedPreferences.getLong(GAME_APPS_SIZE_KEY, -1); + final long musicAppsSize = mSharedPreferences.getLong(MUSIC_APPS_SIZE_KEY, -1); + final long videoAppsSize = mSharedPreferences.getLong(VIDEO_APPS_SIZE_KEY, -1); + final long photoAppSize = mSharedPreferences.getLong(PHOTO_APPS_SIZE_KEY, -1); + final long otherAppsSize = mSharedPreferences.getLong(OTHER_APPS_SIZE_KEY, -1); + final long cacheSize = mSharedPreferences.getLong(CACHE_APPS_SIZE_KEY, -1); + if (gamesSize < 0 + || musicAppsSize < 0 + || videoAppsSize < 0 + || photoAppSize < 0 + || otherAppsSize < 0 + || cacheSize < 0) { + return null; + } + + final long externalTotalBytes = mSharedPreferences.getLong(EXTERNAL_TOTAL_BYTES, -1); + final long externalAudioBytes = mSharedPreferences.getLong(EXTERNAL_AUDIO_BYTES, -1); + final long externalVideoBytes = mSharedPreferences.getLong(EXTERNAL_VIDEO_BYTES, -1); + final long externalImageBytes = mSharedPreferences.getLong(EXTERNAL_IMAGE_BYTES, -1); + final long externalAppBytes = mSharedPreferences.getLong(EXTERNAL_APP_BYTES, -1); + if (externalTotalBytes < 0 + || externalAudioBytes < 0 + || externalVideoBytes < 0 + || externalImageBytes < 0 + || externalAppBytes < 0) { + return null; + } + + final StorageStatsSource.ExternalStorageStats externalStats = + new StorageStatsSource.ExternalStorageStats( + externalTotalBytes, + externalAudioBytes, + externalVideoBytes, + externalImageBytes, + externalAppBytes); + final StorageAsyncLoader.AppsStorageResult result = + new StorageAsyncLoader.AppsStorageResult(); + result.gamesSize = gamesSize; + result.musicAppsSize = musicAppsSize; + result.videoAppsSize = videoAppsSize; + result.photosAppsSize = photoAppSize; + result.otherAppsSize = otherAppsSize; + result.cacheSize = cacheSize; + result.externalStats = externalStats; + final SparseArray resultArray = new SparseArray<>(); + resultArray.append(mUserId, result); + return resultArray; + } + + public void cacheResult( + PrivateStorageInfo storageInfo, StorageAsyncLoader.AppsStorageResult result) { + mSharedPreferences + .edit() + .putLong(FREE_BYTES_KEY, storageInfo.freeBytes) + .putLong(TOTAL_BYTES_KEY, storageInfo.totalBytes) + .putLong(GAME_APPS_SIZE_KEY, result.gamesSize) + .putLong(MUSIC_APPS_SIZE_KEY, result.musicAppsSize) + .putLong(VIDEO_APPS_SIZE_KEY, result.videoAppsSize) + .putLong(PHOTO_APPS_SIZE_KEY, result.photosAppsSize) + .putLong(OTHER_APPS_SIZE_KEY, result.otherAppsSize) + .putLong(CACHE_APPS_SIZE_KEY, result.cacheSize) + .putLong(EXTERNAL_TOTAL_BYTES, result.externalStats.totalBytes) + .putLong(EXTERNAL_AUDIO_BYTES, result.externalStats.audioBytes) + .putLong(EXTERNAL_VIDEO_BYTES, result.externalStats.videoBytes) + .putLong(EXTERNAL_IMAGE_BYTES, result.externalStats.imageBytes) + .putLong(EXTERNAL_APP_BYTES, result.externalStats.appBytes) + .putInt(USER_ID_KEY, mUserId) + .putLong(TIMESTAMP_KEY, mClock.getCurrentTime()) + .apply(); + } + + private boolean isDataValid() { + final int cachedUserId = mSharedPreferences.getInt(USER_ID_KEY, -1); + if (cachedUserId != mUserId) { + return false; + } + + final long lastQueryTime = mSharedPreferences.getLong(TIMESTAMP_KEY, Long.MAX_VALUE); + final long currentTime = mClock.getCurrentTime(); + return currentTime - lastQueryTime < mClobberThreshold; + } + + /** Clock provides the current time. */ + static class Clock { + public long getCurrentTime() { + return System.currentTimeMillis(); + } + } +} diff --git a/tests/robotests/src/com/android/settings/deviceinfo/StorageDashboardFragmentTest.java b/tests/robotests/src/com/android/settings/deviceinfo/StorageDashboardFragmentTest.java index b2d259a1846..a87f563cc9e 100644 --- a/tests/robotests/src/com/android/settings/deviceinfo/StorageDashboardFragmentTest.java +++ b/tests/robotests/src/com/android/settings/deviceinfo/StorageDashboardFragmentTest.java @@ -20,13 +20,18 @@ import static com.google.common.truth.Truth.assertThat; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import android.app.Activity; import android.os.storage.StorageManager; import android.provider.SearchIndexableResource; +import android.util.SparseArray; +import com.android.settings.deviceinfo.storage.CachedStorageValuesHelper; +import com.android.settings.deviceinfo.storage.StorageAsyncLoader; import com.android.settings.testutils.SettingsRobolectricTestRunner; import com.android.settings.TestConfig; +import com.android.settingslib.deviceinfo.PrivateStorageInfo; import com.android.settingslib.drawer.CategoryKey; import org.junit.Before; @@ -68,6 +73,47 @@ public class StorageDashboardFragmentTest { verify(activity).invalidateOptionsMenu(); } + @Test + public void test_cacheProviderProvidesValuesIfBothCached() { + CachedStorageValuesHelper helper = mock(CachedStorageValuesHelper.class); + PrivateStorageInfo info = new PrivateStorageInfo(0, 0); + when(helper.getCachedPrivateStorageInfo()).thenReturn(info); + SparseArray result = new SparseArray<>(); + when(helper.getCachedAppsStorageResult()).thenReturn(result); + + mFragment.setCachedStorageValuesHelper(helper); + mFragment.initializeCachedValues(); + + assertThat(mFragment.getPrivateStorageInfo()).isEqualTo(info); + assertThat(mFragment.getAppsStorageResult()).isEqualTo(result); + } + + @Test + public void test_cacheProviderDoesntProvideValuesIfAppsMissing() { + CachedStorageValuesHelper helper = mock(CachedStorageValuesHelper.class); + PrivateStorageInfo info = new PrivateStorageInfo(0, 0); + when(helper.getCachedPrivateStorageInfo()).thenReturn(info); + + mFragment.setCachedStorageValuesHelper(helper); + mFragment.initializeCachedValues(); + + assertThat(mFragment.getPrivateStorageInfo()).isNull(); + assertThat(mFragment.getAppsStorageResult()).isNull(); + } + + @Test + public void test_cacheProviderDoesntProvideValuesIfVolumeInfoMissing() { + CachedStorageValuesHelper helper = mock(CachedStorageValuesHelper.class); + SparseArray result = new SparseArray<>(); + when(helper.getCachedAppsStorageResult()).thenReturn(result); + + mFragment.setCachedStorageValuesHelper(helper); + mFragment.initializeCachedValues(); + + assertThat(mFragment.getPrivateStorageInfo()).isNull(); + assertThat(mFragment.getAppsStorageResult()).isNull(); + } + @Test public void testSearchIndexProvider_shouldIndexResource() { final List indexRes = diff --git a/tests/robotests/src/com/android/settings/deviceinfo/storage/CachedStorageValuesHelperTest.java b/tests/robotests/src/com/android/settings/deviceinfo/storage/CachedStorageValuesHelperTest.java new file mode 100644 index 00000000000..154a7a1afbf --- /dev/null +++ b/tests/robotests/src/com/android/settings/deviceinfo/storage/CachedStorageValuesHelperTest.java @@ -0,0 +1,295 @@ +/* + * 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.deviceinfo.storage; + +import static com.android.settings.deviceinfo.storage.CachedStorageValuesHelper.CACHE_APPS_SIZE_KEY; +import static com.android.settings.deviceinfo.storage.CachedStorageValuesHelper.EXTERNAL_APP_BYTES; +import static com.android.settings.deviceinfo.storage.CachedStorageValuesHelper.EXTERNAL_AUDIO_BYTES; +import static com.android.settings.deviceinfo.storage.CachedStorageValuesHelper.EXTERNAL_IMAGE_BYTES; +import static com.android.settings.deviceinfo.storage.CachedStorageValuesHelper.EXTERNAL_TOTAL_BYTES; +import static com.android.settings.deviceinfo.storage.CachedStorageValuesHelper.EXTERNAL_VIDEO_BYTES; +import static com.android.settings.deviceinfo.storage.CachedStorageValuesHelper.FREE_BYTES_KEY; +import static com.android.settings.deviceinfo.storage.CachedStorageValuesHelper.GAME_APPS_SIZE_KEY; +import static com.android.settings.deviceinfo.storage.CachedStorageValuesHelper.MUSIC_APPS_SIZE_KEY; +import static com.android.settings.deviceinfo.storage.CachedStorageValuesHelper.OTHER_APPS_SIZE_KEY; +import static com.android.settings.deviceinfo.storage.CachedStorageValuesHelper.PHOTO_APPS_SIZE_KEY; +import static com.android.settings.deviceinfo.storage.CachedStorageValuesHelper.SHARED_PREFERENCES_NAME; +import static com.android.settings.deviceinfo.storage.CachedStorageValuesHelper.TIMESTAMP_KEY; +import static com.android.settings.deviceinfo.storage.CachedStorageValuesHelper.TOTAL_BYTES_KEY; +import static com.android.settings.deviceinfo.storage.CachedStorageValuesHelper.USER_ID_KEY; +import static com.android.settings.deviceinfo.storage.CachedStorageValuesHelper.VIDEO_APPS_SIZE_KEY; + +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.content.SharedPreferences; +import android.util.SparseArray; + +import com.android.settings.TestConfig; +import com.android.settings.testutils.SettingsRobolectricTestRunner; +import com.android.settingslib.applications.StorageStatsSource; +import com.android.settingslib.deviceinfo.PrivateStorageInfo; + +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 static com.google.common.truth.Truth.assertThat; + +@RunWith(SettingsRobolectricTestRunner.class) +@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION) +public class CachedStorageValuesHelperTest { + private Context mContext; + + @Mock private CachedStorageValuesHelper.Clock mMockClock; + private CachedStorageValuesHelper mCachedValuesHelper; + private SharedPreferences mSharedPreferences; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + mContext = RuntimeEnvironment.application.getApplicationContext(); + mSharedPreferences = mContext.getSharedPreferences(SHARED_PREFERENCES_NAME, 0); + mCachedValuesHelper = new CachedStorageValuesHelper(mContext, 0); + mCachedValuesHelper.mClock = mMockClock; + } + + @Test + public void getCachedPrivateStorageInfo_cachedValuesAreLoaded() throws Exception { + when(mMockClock.getCurrentTime()).thenReturn(10001L); + mSharedPreferences + .edit() + .putLong(GAME_APPS_SIZE_KEY, 0) + .putLong(MUSIC_APPS_SIZE_KEY, 10) + .putLong(VIDEO_APPS_SIZE_KEY, 100) + .putLong(PHOTO_APPS_SIZE_KEY, 1000) + .putLong(OTHER_APPS_SIZE_KEY, 10000) + .putLong(CACHE_APPS_SIZE_KEY, 100000) + .putLong(EXTERNAL_TOTAL_BYTES, 2) + .putLong(EXTERNAL_AUDIO_BYTES, 22) + .putLong(EXTERNAL_VIDEO_BYTES, 222) + .putLong(EXTERNAL_IMAGE_BYTES, 2222) + .putLong(EXTERNAL_APP_BYTES, 22222) + .putLong(FREE_BYTES_KEY, 1000L) + .putLong(TOTAL_BYTES_KEY, 6000L) + .putInt(USER_ID_KEY, 0) + .putLong(TIMESTAMP_KEY, 10000L) + .apply(); + + PrivateStorageInfo info = mCachedValuesHelper.getCachedPrivateStorageInfo(); + + assertThat(info.freeBytes).isEqualTo(1000L); + assertThat(info.totalBytes).isEqualTo(6000L); + } + + @Test + public void getCachedAppsStorageResult_cachedValuesAreLoaded() throws Exception { + when(mMockClock.getCurrentTime()).thenReturn(10001L); + mSharedPreferences + .edit() + .putLong(GAME_APPS_SIZE_KEY, 1) + .putLong(MUSIC_APPS_SIZE_KEY, 10) + .putLong(VIDEO_APPS_SIZE_KEY, 100) + .putLong(PHOTO_APPS_SIZE_KEY, 1000) + .putLong(OTHER_APPS_SIZE_KEY, 10000) + .putLong(CACHE_APPS_SIZE_KEY, 100000) + .putLong(EXTERNAL_TOTAL_BYTES, 222222) + .putLong(EXTERNAL_AUDIO_BYTES, 22) + .putLong(EXTERNAL_VIDEO_BYTES, 222) + .putLong(EXTERNAL_IMAGE_BYTES, 2222) + .putLong(EXTERNAL_APP_BYTES, 22222) + .putLong(FREE_BYTES_KEY, 1000L) + .putLong(TOTAL_BYTES_KEY, 5000L) + .putInt(USER_ID_KEY, 0) + .putLong(TIMESTAMP_KEY, 10000L) + .apply(); + + SparseArray result = + mCachedValuesHelper.getCachedAppsStorageResult(); + + StorageAsyncLoader.AppsStorageResult primaryResult = result.get(0); + assertThat(primaryResult.gamesSize).isEqualTo(1L); + assertThat(primaryResult.musicAppsSize).isEqualTo(10L); + assertThat(primaryResult.videoAppsSize).isEqualTo(100L); + assertThat(primaryResult.photosAppsSize).isEqualTo(1000L); + assertThat(primaryResult.otherAppsSize).isEqualTo(10000L); + assertThat(primaryResult.cacheSize).isEqualTo(100000L); + assertThat(primaryResult.externalStats.totalBytes).isEqualTo(222222L); + assertThat(primaryResult.externalStats.audioBytes).isEqualTo(22L); + assertThat(primaryResult.externalStats.videoBytes).isEqualTo(222L); + assertThat(primaryResult.externalStats.imageBytes).isEqualTo(2222L); + assertThat(primaryResult.externalStats.appBytes).isEqualTo(22222L); + } + + @Test + public void getCachedPrivateStorageInfo_nullIfDataIsStale() throws Exception { + when(mMockClock.getCurrentTime()).thenReturn(10000000L); + mSharedPreferences + .edit() + .putLong(GAME_APPS_SIZE_KEY, 0) + .putLong(MUSIC_APPS_SIZE_KEY, 10) + .putLong(VIDEO_APPS_SIZE_KEY, 100) + .putLong(PHOTO_APPS_SIZE_KEY, 1000) + .putLong(OTHER_APPS_SIZE_KEY, 10000) + .putLong(CACHE_APPS_SIZE_KEY, 100000) + .putLong(EXTERNAL_TOTAL_BYTES, 2) + .putLong(EXTERNAL_AUDIO_BYTES, 22) + .putLong(EXTERNAL_VIDEO_BYTES, 222) + .putLong(EXTERNAL_IMAGE_BYTES, 2222) + .putLong(EXTERNAL_APP_BYTES, 22222) + .putLong(FREE_BYTES_KEY, 1000L) + .putLong(TOTAL_BYTES_KEY, 5000L) + .putInt(USER_ID_KEY, 0) + .putLong(TIMESTAMP_KEY, 10000L) + .apply(); + + PrivateStorageInfo info = mCachedValuesHelper.getCachedPrivateStorageInfo(); + assertThat(info).isNull(); + } + + @Test + public void getCachedAppsStorageResult_nullIfDataIsStale() throws Exception { + when(mMockClock.getCurrentTime()).thenReturn(10000000L); + mSharedPreferences + .edit() + .putLong(GAME_APPS_SIZE_KEY, 0) + .putLong(MUSIC_APPS_SIZE_KEY, 10) + .putLong(VIDEO_APPS_SIZE_KEY, 100) + .putLong(PHOTO_APPS_SIZE_KEY, 1000) + .putLong(OTHER_APPS_SIZE_KEY, 10000) + .putLong(CACHE_APPS_SIZE_KEY, 100000) + .putLong(EXTERNAL_TOTAL_BYTES, 2) + .putLong(EXTERNAL_AUDIO_BYTES, 22) + .putLong(EXTERNAL_VIDEO_BYTES, 222) + .putLong(EXTERNAL_IMAGE_BYTES, 2222) + .putLong(EXTERNAL_APP_BYTES, 22222) + .putLong(FREE_BYTES_KEY, 1000L) + .putLong(TOTAL_BYTES_KEY, 5000L) + .putInt(USER_ID_KEY, 0) + .putLong(TIMESTAMP_KEY, 10000L) + .apply(); + + SparseArray result = + mCachedValuesHelper.getCachedAppsStorageResult(); + assertThat(result).isNull(); + } + + @Test + public void getCachedPrivateStorageInfo_nullIfWrongUser() throws Exception { + when(mMockClock.getCurrentTime()).thenReturn(10001L); + mSharedPreferences + .edit() + .putLong(GAME_APPS_SIZE_KEY, 0) + .putLong(MUSIC_APPS_SIZE_KEY, 10) + .putLong(VIDEO_APPS_SIZE_KEY, 100) + .putLong(PHOTO_APPS_SIZE_KEY, 1000) + .putLong(OTHER_APPS_SIZE_KEY, 10000) + .putLong(CACHE_APPS_SIZE_KEY, 100000) + .putLong(EXTERNAL_TOTAL_BYTES, 2) + .putLong(EXTERNAL_AUDIO_BYTES, 22) + .putLong(EXTERNAL_VIDEO_BYTES, 222) + .putLong(EXTERNAL_IMAGE_BYTES, 2222) + .putLong(EXTERNAL_APP_BYTES, 22222) + .putLong(FREE_BYTES_KEY, 1000L) + .putLong(TOTAL_BYTES_KEY, 5000L) + .putInt(USER_ID_KEY, 1) + .putLong(TIMESTAMP_KEY, 10000L) + .apply(); + + PrivateStorageInfo info = mCachedValuesHelper.getCachedPrivateStorageInfo(); + assertThat(info).isNull(); + } + + @Test + public void getCachedAppsStorageResult_nullIfWrongUser() throws Exception { + when(mMockClock.getCurrentTime()).thenReturn(10001L); + mSharedPreferences + .edit() + .putLong(GAME_APPS_SIZE_KEY, 0) + .putLong(MUSIC_APPS_SIZE_KEY, 10) + .putLong(VIDEO_APPS_SIZE_KEY, 100) + .putLong(PHOTO_APPS_SIZE_KEY, 1000) + .putLong(OTHER_APPS_SIZE_KEY, 10000) + .putLong(CACHE_APPS_SIZE_KEY, 100000) + .putLong(EXTERNAL_TOTAL_BYTES, 2) + .putLong(EXTERNAL_AUDIO_BYTES, 22) + .putLong(EXTERNAL_VIDEO_BYTES, 222) + .putLong(EXTERNAL_IMAGE_BYTES, 2222) + .putLong(EXTERNAL_APP_BYTES, 22222) + .putLong(FREE_BYTES_KEY, 1000L) + .putLong(TOTAL_BYTES_KEY, 5000L) + .putInt(USER_ID_KEY, 1) + .putLong(TIMESTAMP_KEY, 10000L) + .apply(); + + SparseArray result = + mCachedValuesHelper.getCachedAppsStorageResult(); + assertThat(result).isNull(); + } + + @Test + public void getCachedPrivateStorageInfo_nullIfEmpty() throws Exception { + PrivateStorageInfo info = mCachedValuesHelper.getCachedPrivateStorageInfo(); + assertThat(info).isNull(); + } + + @Test + public void getCachedAppsStorageResult_nullIfEmpty() throws Exception { + SparseArray result = + mCachedValuesHelper.getCachedAppsStorageResult(); + assertThat(result).isNull(); + } + + @Test + public void cacheResult_succeeds() throws Exception { + when(mMockClock.getCurrentTime()).thenReturn(10000L); + final StorageStatsSource.ExternalStorageStats externalStats = + new StorageStatsSource.ExternalStorageStats(22222l, 2l, 20L, 200L, 2000L); + final StorageAsyncLoader.AppsStorageResult result = + new StorageAsyncLoader.AppsStorageResult(); + result.gamesSize = 1L; + result.musicAppsSize = 10l; + result.videoAppsSize = 100L; + result.photosAppsSize = 1000L; + result.otherAppsSize = 10000L; + result.cacheSize = 100000l; + result.externalStats = externalStats; + final PrivateStorageInfo info = new PrivateStorageInfo(1000L, 6000L); + + mCachedValuesHelper.cacheResult(info, result); + + assertThat(mSharedPreferences.getLong(GAME_APPS_SIZE_KEY, -1)).isEqualTo(1L); + assertThat(mSharedPreferences.getLong(MUSIC_APPS_SIZE_KEY, -1)).isEqualTo(10L); + assertThat(mSharedPreferences.getLong(VIDEO_APPS_SIZE_KEY, -1)).isEqualTo(100L); + assertThat(mSharedPreferences.getLong(PHOTO_APPS_SIZE_KEY, -1)).isEqualTo(1000L); + assertThat(mSharedPreferences.getLong(OTHER_APPS_SIZE_KEY, -1)).isEqualTo(10000L); + assertThat(mSharedPreferences.getLong(CACHE_APPS_SIZE_KEY, -1)).isEqualTo(100000L); + assertThat(mSharedPreferences.getLong(EXTERNAL_TOTAL_BYTES, -1)).isEqualTo(22222L); + assertThat(mSharedPreferences.getLong(EXTERNAL_AUDIO_BYTES, -1)).isEqualTo(2L); + assertThat(mSharedPreferences.getLong(EXTERNAL_VIDEO_BYTES, -1)).isEqualTo(20L); + assertThat(mSharedPreferences.getLong(EXTERNAL_IMAGE_BYTES, -1)).isEqualTo(200L); + assertThat(mSharedPreferences.getLong(EXTERNAL_APP_BYTES, -1)).isEqualTo(2000L); + assertThat(mSharedPreferences.getLong(FREE_BYTES_KEY, -1)).isEqualTo(1000L); + assertThat(mSharedPreferences.getLong(TOTAL_BYTES_KEY, -1)).isEqualTo(6000L); + assertThat(mSharedPreferences.getInt(USER_ID_KEY, -1)).isEqualTo(0); + assertThat(mSharedPreferences.getLong(TIMESTAMP_KEY, -1)).isEqualTo(10000L); + }; +} From b7a11feaa2355f82dcbcc451e417710e65d0f75f Mon Sep 17 00:00:00 2001 From: debesay guadad Date: Fri, 26 Feb 2016 14:49:27 +0800 Subject: [PATCH 03/17] Add ims registration status Display "IMS registration state" in Status menu. Introduce carrier config to enable/disable the feature for customization. Since some carriers require, this feature is necessary. Test: manual Checked "IMS registration state" in Status menu Bug: 28806101 Change-Id: I6c452c512f03cf41704b91331e44141ed3050cf9 --- res/values/strings.xml | 7 +++++ res/xml/device_info_status.xml | 6 ++++ .../android/settings/deviceinfo/Status.java | 28 +++++++++++++++++++ 3 files changed, 41 insertions(+) diff --git a/res/values/strings.xml b/res/values/strings.xml index 6241850e4ec..fcc0b6d36bf 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -9066,4 +9066,11 @@ This feature is not available on this device + + + "IMS registration state" + + "Registered" + + "Not registered" diff --git a/res/xml/device_info_status.xml b/res/xml/device_info_status.xml index 9a57af987bf..6fe43ec87b0 100644 --- a/res/xml/device_info_status.xml +++ b/res/xml/device_info_status.xml @@ -89,4 +89,10 @@ android:title="@string/status_wimax_mac_address" android:summary="@string/device_info_not_available" android:persistent="false" /> + diff --git a/src/com/android/settings/deviceinfo/Status.java b/src/com/android/settings/deviceinfo/Status.java index baddc6cac2b..faa4134a935 100644 --- a/src/com/android/settings/deviceinfo/Status.java +++ b/src/com/android/settings/deviceinfo/Status.java @@ -28,11 +28,15 @@ import android.net.wifi.WifiManager; import android.os.Bundle; import android.os.Handler; import android.os.Message; +import android.os.PersistableBundle; import android.os.SystemClock; import android.os.SystemProperties; import android.os.UserManager; import android.support.v7.preference.Preference; import android.support.v7.preference.PreferenceScreen; +import android.telephony.CarrierConfigManager; +import android.telephony.SubscriptionManager; +import android.telephony.TelephonyManager; import android.text.TextUtils; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; @@ -56,6 +60,7 @@ public class Status extends SettingsPreferenceFragment { private static final String KEY_WIMAX_MAC_ADDRESS = "wimax_mac_address"; private static final String KEY_SIM_STATUS = "sim_status"; private static final String KEY_IMEI_INFO = "imei_info"; + private static final String KEY_IMS_REGISTRATION_STATE = "ims_reg_state"; // Broadcasts to listen to for connectivity changes. private static final String[] CONNECTIVITY_INTENTS = { @@ -85,6 +90,8 @@ public class Status extends SettingsPreferenceFragment { private Preference mIpAddress; private Preference mWifiMacAddress; private Preference mWimaxMacAddress; + private Preference mImsStatus; + private Handler mHandler; private static class MyHandler extends Handler { @@ -162,6 +169,7 @@ public class Status extends SettingsPreferenceFragment { mWifiMacAddress = findPreference(KEY_WIFI_MAC_ADDRESS); mWimaxMacAddress = findPreference(KEY_WIMAX_MAC_ADDRESS); mIpAddress = findPreference(KEY_IP_ADDRESS); + mImsStatus = findPreference(KEY_IMS_REGISTRATION_STATE); mRes = getResources(); mUnavailable = mRes.getString(R.string.status_unavailable); @@ -269,11 +277,31 @@ public class Status extends SettingsPreferenceFragment { } } + private void setImsRegistrationStatus() { + CarrierConfigManager configManager = (CarrierConfigManager) + getSystemService(Context.CARRIER_CONFIG_SERVICE); + int subId = SubscriptionManager.getDefaultDataSubscriptionId(); + PersistableBundle config = null; + if (configManager != null) { + config = configManager.getConfigForSubId(subId); + } + if (config != null && config.getBoolean( + CarrierConfigManager.KEY_SHOW_IMS_REGISTRATION_STATUS_BOOL)) { + TelephonyManager tm = (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE); + mImsStatus.setSummary((tm != null && tm.isImsRegistered(subId)) ? + R.string.ims_reg_status_registered : R.string.ims_reg_status_not_registered); + } else { + removePreferenceFromScreen(KEY_IMS_REGISTRATION_STATE); + mImsStatus = null; + } + } + void updateConnectivity() { setWimaxStatus(); setWifiStatus(); setBtStatus(); setIpAddressStatus(); + setImsRegistrationStatus(); } void updateTimes() { From 9e6d39e71a0d8f502e7aa50c1d63867cca0448ac Mon Sep 17 00:00:00 2001 From: Daniel Nishi Date: Fri, 1 Sep 2017 12:45:56 -0700 Subject: [PATCH 04/17] Turn in the work profile's badge. The badge is no longer part of the preferred work profile UX. By removing the code which badges the preferences for the individual storage items, we can avoid placing the no longer needed tiny badges. Change-Id: I6fd2ec28d3cf55e3a95b877f4c7b9b51c94e12d8 Fixes: 64475406 Test: Manual --- .../StorageItemPreferenceController.java | 23 ++++++++----------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/src/com/android/settings/deviceinfo/storage/StorageItemPreferenceController.java b/src/com/android/settings/deviceinfo/storage/StorageItemPreferenceController.java index ca85f69d0be..163f5b9dff4 100644 --- a/src/com/android/settings/deviceinfo/storage/StorageItemPreferenceController.java +++ b/src/com/android/settings/deviceinfo/storage/StorageItemPreferenceController.java @@ -208,23 +208,18 @@ public class StorageItemPreferenceController extends AbstractPreferenceControlle public void setUserId(UserHandle userHandle) { mUserId = userHandle.getIdentifier(); - PackageManager pm = mContext.getPackageManager(); - badgePreference(pm, userHandle, mPhotoPreference); - badgePreference(pm, userHandle, mMoviesPreference); - badgePreference(pm, userHandle, mAudioPreference); - badgePreference(pm, userHandle, mGamePreference); - badgePreference(pm, userHandle, mAppPreference); - badgePreference(pm, userHandle, mSystemPreference); - badgePreference(pm, userHandle, mFilePreference); + tintPreference(mPhotoPreference); + tintPreference(mMoviesPreference); + tintPreference(mAudioPreference); + tintPreference(mGamePreference); + tintPreference(mAppPreference); + tintPreference(mSystemPreference); + tintPreference(mFilePreference); } - private void badgePreference(PackageManager pm, UserHandle userHandle, Preference preference) { + private void tintPreference(Preference preference) { if (preference != null) { - Drawable currentIcon = preference.getIcon(); - // Sigh... Applying the badge to the icon clobbers the tint on the base drawable. - // For some reason, reapplying it here means the tint remains. - currentIcon = applyTint(mContext, currentIcon); - preference.setIcon(pm.getUserBadgedIcon(currentIcon, userHandle)); + preference.setIcon(applyTint(mContext, preference.getIcon())); } } From d1c3084bd53cd8079f395ffe2a3bc5f45dcc5290 Mon Sep 17 00:00:00 2001 From: Ajay Nadathur Date: Thu, 31 Aug 2017 11:37:32 -0700 Subject: [PATCH 05/17] Set title in remote views if present in bundle - Title set if defined in the Bundle returned by summaryUri's content provider. - summaryUri invoked inline with onBindView call bug: 62713030 Test: Manually tested, verified that title and summary are retrieved when settings app is resumed Change-Id: Id82531eec5ec11ec3492f033fb34ec65a5437a48 --- .../settings/dashboard/DashboardData.java | 11 +++++++++ .../suggestions/SuggestionAdapter.java | 3 +++ .../settings/dashboard/DashboardDataTest.java | 24 +++++++++++++++++++ 3 files changed, 38 insertions(+) diff --git a/src/com/android/settings/dashboard/DashboardData.java b/src/com/android/settings/dashboard/DashboardData.java index 0fac0a2248d..1fd181ff544 100644 --- a/src/com/android/settings/dashboard/DashboardData.java +++ b/src/com/android/settings/dashboard/DashboardData.java @@ -460,6 +460,17 @@ public class DashboardData { // Only check title and summary for dashboard tile return TextUtils.equals(localTile.title, targetTile.title) && TextUtils.equals(localTile.summary, targetTile.summary); + case TYPE_SUGGESTION_CONDITION_CONTAINER: + // If entity is suggestion and contains remote view, force refresh + final List entities = (List) entity; + if (!entities.isEmpty()) { + Object firstEntity = entities.get(0); + if (firstEntity instanceof Tile + && ((Tile) firstEntity).remoteViews != null) { + return false; + } + } + // Otherwise Fall through to default default: return entity == null ? targetItem.entity == null : entity.equals(targetItem.entity); diff --git a/src/com/android/settings/dashboard/suggestions/SuggestionAdapter.java b/src/com/android/settings/dashboard/suggestions/SuggestionAdapter.java index 45396807f87..2c9da4124b1 100644 --- a/src/com/android/settings/dashboard/suggestions/SuggestionAdapter.java +++ b/src/com/android/settings/dashboard/suggestions/SuggestionAdapter.java @@ -29,6 +29,8 @@ import com.android.settings.dashboard.DashboardAdapter.DashboardItemHolder; import com.android.settings.dashboard.DashboardAdapter.IconCache; import com.android.settings.overlay.FeatureFactory; import com.android.settingslib.drawer.Tile; +import com.android.settingslib.drawer.TileUtils; + import java.util.List; import java.util.Objects; @@ -73,6 +75,7 @@ public class SuggestionAdapter extends RecyclerView.Adapter mSuggestionsShownLogged.add(suggestionId); } if (suggestion.remoteViews != null) { + TileUtils.updateTileUsingSummaryUri(mContext, suggestion); final ViewGroup itemView = (ViewGroup) holder.itemView; itemView.removeAllViews(); itemView.addView(suggestion.remoteViews.apply(itemView.getContext(), itemView)); diff --git a/tests/robotests/src/com/android/settings/dashboard/DashboardDataTest.java b/tests/robotests/src/com/android/settings/dashboard/DashboardDataTest.java index 77213f54cfe..68d33545bc1 100644 --- a/tests/robotests/src/com/android/settings/dashboard/DashboardDataTest.java +++ b/tests/robotests/src/com/android/settings/dashboard/DashboardDataTest.java @@ -19,6 +19,7 @@ package com.android.settings.dashboard; import android.support.annotation.NonNull; import android.support.v7.util.DiffUtil; import android.support.v7.util.ListUpdateCallback; +import android.widget.RemoteViews; import com.android.settings.TestConfig; import com.android.settings.dashboard.conditional.AirplaneModeCondition; @@ -35,6 +36,7 @@ import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Objects; @@ -224,6 +226,28 @@ public class DashboardDataTest { testDiffUtil(mDashboardDataWithOneConditions, mDashboardDataWithNoItems, testResultData); } + @Test + public void testDiffUtil_typeSuggestedContainer_ResultDataNothingChanged() { + //Build testResultData + final List testResultData = new ArrayList<>(); + testResultData.add(new ListUpdateResult.ResultData( + ListUpdateResult.ResultData.TYPE_OPERATION_CHANGE, 0, 1)); + Tile tile = new Tile(); + tile.remoteViews = mock(RemoteViews.class); + + DashboardData prevData = new DashboardData.Builder() + .setConditions(null) + .setCategory(null) + .setSuggestions(Arrays.asList(tile)) + .build(); + DashboardData currentData = new DashboardData.Builder() + .setConditions(null) + .setCategory(null) + .setSuggestions(Arrays.asList(tile)) + .build(); + testDiffUtil(prevData, currentData, testResultData); + } + /** * Test when using the * {@link com.android.settings.dashboard.DashboardData.ItemsDataDiffCallback} From be47df7395d1ccf1b35f33f963ca0c69a1036de0 Mon Sep 17 00:00:00 2001 From: daqi Date: Tue, 5 Sep 2017 09:53:05 +0800 Subject: [PATCH 06/17] Fix TrustedCredentialsSettings NPE [Cause of Defect] TrustedCredentialsSettings#mKeyChainConnectionByProfileId is get/set by more than one thread. Main thread would set it in onDestroy method, and AsyncTask would get and set in the doInBackground method. So mKeyChainConnectionByProfileId.get(profileId) would get null after onDestroy method get called. Bug: N/A Test: Debugger to simulate concurrency Change-Id: I0664d1e9b88b079855354ce0e6fe014a98a22327 Signed-off-by: daqi --- .../settings/TrustedCredentialsSettings.java | 162 ++++++++++-------- 1 file changed, 86 insertions(+), 76 deletions(-) diff --git a/src/com/android/settings/TrustedCredentialsSettings.java b/src/com/android/settings/TrustedCredentialsSettings.java index 4696dd317b3..587e814e2f8 100644 --- a/src/com/android/settings/TrustedCredentialsSettings.java +++ b/src/com/android/settings/TrustedCredentialsSettings.java @@ -61,6 +61,7 @@ import android.widget.Switch; import android.widget.TabHost; import android.widget.TextView; +import com.android.internal.annotations.GuardedBy; import com.android.internal.app.UnlaunchableAppActivity; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.internal.widget.LockPatternUtils; @@ -152,6 +153,7 @@ public class TrustedCredentialsSettings extends OptionsMenuFragment private int mConfirmingCredentialUser; private IntConsumer mConfirmingCredentialListener; private Set mAliasLoaders = new ArraySet(2); + @GuardedBy("mKeyChainConnectionByProfileId") private final SparseArray mKeyChainConnectionByProfileId = new SparseArray(); @@ -256,11 +258,13 @@ public class TrustedCredentialsSettings extends OptionsMenuFragment } private void closeKeyChainConnections() { - final int n = mKeyChainConnectionByProfileId.size(); - for (int i = 0; i < n; ++i) { - mKeyChainConnectionByProfileId.valueAt(i).close(); + synchronized (mKeyChainConnectionByProfileId) { + final int n = mKeyChainConnectionByProfileId.size(); + for (int i = 0; i < n; ++i) { + mKeyChainConnectionByProfileId.valueAt(i).close(); + } + mKeyChainConnectionByProfileId.clear(); } - mKeyChainConnectionByProfileId.clear(); } private void addTab(Tab tab) { @@ -684,62 +688,64 @@ public class TrustedCredentialsSettings extends OptionsMenuFragment SparseArray> certHoldersByProfile = new SparseArray>(); try { - List profiles = mUserManager.getUserProfiles(); - final int n = profiles.size(); - // First we get all aliases for all profiles in order to show progress - // correctly. Otherwise this could all be in a single loop. - SparseArray> aliasesByProfileId = new SparseArray< - List>(n); - int max = 0; - int progress = 0; - for (int i = 0; i < n; ++i) { - UserHandle profile = profiles.get(i); - int profileId = profile.getIdentifier(); - if (shouldSkipProfile(profile)) { - continue; + synchronized(mKeyChainConnectionByProfileId) { + List profiles = mUserManager.getUserProfiles(); + final int n = profiles.size(); + // First we get all aliases for all profiles in order to show progress + // correctly. Otherwise this could all be in a single loop. + SparseArray> aliasesByProfileId = new SparseArray< + List>(n); + int max = 0; + int progress = 0; + for (int i = 0; i < n; ++i) { + UserHandle profile = profiles.get(i); + int profileId = profile.getIdentifier(); + if (shouldSkipProfile(profile)) { + continue; + } + KeyChainConnection keyChainConnection = KeyChain.bindAsUser(mContext, + profile); + // Saving the connection for later use on the certificate dialog. + mKeyChainConnectionByProfileId.put(profileId, keyChainConnection); + IKeyChainService service = keyChainConnection.getService(); + List aliases = mTab.getAliases(service); + if (isCancelled()) { + return new SparseArray>(); + } + max += aliases.size(); + aliasesByProfileId.put(profileId, aliases); } - KeyChainConnection keyChainConnection = KeyChain.bindAsUser(mContext, - profile); - // Saving the connection for later use on the certificate dialog. - mKeyChainConnectionByProfileId.put(profileId, keyChainConnection); - IKeyChainService service = keyChainConnection.getService(); - List aliases = mTab.getAliases(service); - if (isCancelled()) { - return new SparseArray>(); + for (int i = 0; i < n; ++i) { + UserHandle profile = profiles.get(i); + int profileId = profile.getIdentifier(); + List aliases = aliasesByProfileId.get(profileId); + if (isCancelled()) { + return new SparseArray>(); + } + KeyChainConnection keyChainConnection = mKeyChainConnectionByProfileId.get( + profileId); + if (shouldSkipProfile(profile) || aliases == null + || keyChainConnection == null) { + certHoldersByProfile.put(profileId, new ArrayList(0)); + continue; + } + IKeyChainService service = keyChainConnection.getService(); + List certHolders = new ArrayList(max); + final int aliasMax = aliases.size(); + for (int j = 0; j < aliasMax; ++j) { + String alias = aliases.get(j); + byte[] encodedCertificate = service.getEncodedCaCertificate(alias, + true); + X509Certificate cert = KeyChain.toCertificate(encodedCertificate); + certHolders.add(new CertHolder(service, mAdapter, + mTab, alias, cert, profileId)); + publishProgress(++progress, max); + } + Collections.sort(certHolders); + certHoldersByProfile.put(profileId, certHolders); } - max += aliases.size(); - aliasesByProfileId.put(profileId, aliases); + return certHoldersByProfile; } - for (int i = 0; i < n; ++i) { - UserHandle profile = profiles.get(i); - int profileId = profile.getIdentifier(); - List aliases = aliasesByProfileId.get(profileId); - if (isCancelled()) { - return new SparseArray>(); - } - KeyChainConnection keyChainConnection = mKeyChainConnectionByProfileId.get( - profileId); - if (shouldSkipProfile(profile) || aliases == null - || keyChainConnection == null) { - certHoldersByProfile.put(profileId, new ArrayList(0)); - continue; - } - IKeyChainService service = keyChainConnection.getService(); - List certHolders = new ArrayList(max); - final int aliasMax = aliases.size(); - for (int j = 0; j < aliasMax; ++j) { - String alias = aliases.get(j); - byte[] encodedCertificate = service.getEncodedCaCertificate(alias, - true); - X509Certificate cert = KeyChain.toCertificate(encodedCertificate); - certHolders.add(new CertHolder(service, mAdapter, - mTab, alias, cert, profileId)); - publishProgress(++progress, max); - } - Collections.sort(certHolders); - certHoldersByProfile.put(profileId, certHolders); - } - return certHoldersByProfile; } catch (RemoteException e) { Log.e(TAG, "Remote exception while loading aliases.", e); return new SparseArray>(); @@ -936,16 +942,18 @@ public class TrustedCredentialsSettings extends OptionsMenuFragment public List getX509CertsFromCertHolder(CertHolder certHolder) { List certificates = null; try { - KeyChainConnection keyChainConnection = mKeyChainConnectionByProfileId.get( - certHolder.mProfileId); - IKeyChainService service = keyChainConnection.getService(); - List chain = service.getCaCertificateChainAliases(certHolder.mAlias, true); - final int n = chain.size(); - certificates = new ArrayList(n); - for (int i = 0; i < n; ++i) { - byte[] encodedCertificate = service.getEncodedCaCertificate(chain.get(i), true); - X509Certificate certificate = KeyChain.toCertificate(encodedCertificate); - certificates.add(certificate); + synchronized (mKeyChainConnectionByProfileId) { + KeyChainConnection keyChainConnection = mKeyChainConnectionByProfileId.get( + certHolder.mProfileId); + IKeyChainService service = keyChainConnection.getService(); + List chain = service.getCaCertificateChainAliases(certHolder.mAlias, true); + final int n = chain.size(); + certificates = new ArrayList(n); + for (int i = 0; i < n; ++i) { + byte[] encodedCertificate = service.getEncodedCaCertificate(chain.get(i), true); + X509Certificate certificate = KeyChain.toCertificate(encodedCertificate); + certificates.add(certificate); + } } } catch (RemoteException ex) { Log.e(TAG, "RemoteException while retrieving certificate chain for root " @@ -985,15 +993,17 @@ public class TrustedCredentialsSettings extends OptionsMenuFragment @Override protected Boolean doInBackground(Void... params) { try { - KeyChainConnection keyChainConnection = mKeyChainConnectionByProfileId.get( - mCertHolder.mProfileId); - IKeyChainService service = keyChainConnection.getService(); - if (mCertHolder.mDeleted) { - byte[] bytes = mCertHolder.mX509Cert.getEncoded(); - service.installCaCertificate(bytes); - return true; - } else { - return service.deleteCaCertificate(mCertHolder.mAlias); + synchronized (mKeyChainConnectionByProfileId) { + KeyChainConnection keyChainConnection = mKeyChainConnectionByProfileId.get( + mCertHolder.mProfileId); + IKeyChainService service = keyChainConnection.getService(); + if (mCertHolder.mDeleted) { + byte[] bytes = mCertHolder.mX509Cert.getEncoded(); + service.installCaCertificate(bytes); + return true; + } else { + return service.deleteCaCertificate(mCertHolder.mAlias); + } } } catch (CertificateEncodingException | SecurityException | IllegalStateException | RemoteException e) { From ab07989b979d03e67aa39a0bdc3cc931b6ec35d1 Mon Sep 17 00:00:00 2001 From: Ajay Nadathur Date: Thu, 31 Aug 2017 11:37:32 -0700 Subject: [PATCH 07/17] Set title in remote views if present in bundle - Title set if defined in the Bundle returned by summaryUri's content provider. - summaryUri invoked inline with onBindView call bug: 62713030 Test: Manually tested, verified that title and summary are retrieved when settings app is resumed Change-Id: Id82531eec5ec11ec3492f033fb34ec65a5437a48 --- .../settings/dashboard/DashboardData.java | 11 +++++++++ .../suggestions/SuggestionAdapter.java | 3 +++ .../settings/dashboard/DashboardDataTest.java | 24 +++++++++++++++++++ 3 files changed, 38 insertions(+) diff --git a/src/com/android/settings/dashboard/DashboardData.java b/src/com/android/settings/dashboard/DashboardData.java index 0fac0a2248d..1fd181ff544 100644 --- a/src/com/android/settings/dashboard/DashboardData.java +++ b/src/com/android/settings/dashboard/DashboardData.java @@ -460,6 +460,17 @@ public class DashboardData { // Only check title and summary for dashboard tile return TextUtils.equals(localTile.title, targetTile.title) && TextUtils.equals(localTile.summary, targetTile.summary); + case TYPE_SUGGESTION_CONDITION_CONTAINER: + // If entity is suggestion and contains remote view, force refresh + final List entities = (List) entity; + if (!entities.isEmpty()) { + Object firstEntity = entities.get(0); + if (firstEntity instanceof Tile + && ((Tile) firstEntity).remoteViews != null) { + return false; + } + } + // Otherwise Fall through to default default: return entity == null ? targetItem.entity == null : entity.equals(targetItem.entity); diff --git a/src/com/android/settings/dashboard/suggestions/SuggestionAdapter.java b/src/com/android/settings/dashboard/suggestions/SuggestionAdapter.java index 45396807f87..2c9da4124b1 100644 --- a/src/com/android/settings/dashboard/suggestions/SuggestionAdapter.java +++ b/src/com/android/settings/dashboard/suggestions/SuggestionAdapter.java @@ -29,6 +29,8 @@ import com.android.settings.dashboard.DashboardAdapter.DashboardItemHolder; import com.android.settings.dashboard.DashboardAdapter.IconCache; import com.android.settings.overlay.FeatureFactory; import com.android.settingslib.drawer.Tile; +import com.android.settingslib.drawer.TileUtils; + import java.util.List; import java.util.Objects; @@ -73,6 +75,7 @@ public class SuggestionAdapter extends RecyclerView.Adapter mSuggestionsShownLogged.add(suggestionId); } if (suggestion.remoteViews != null) { + TileUtils.updateTileUsingSummaryUri(mContext, suggestion); final ViewGroup itemView = (ViewGroup) holder.itemView; itemView.removeAllViews(); itemView.addView(suggestion.remoteViews.apply(itemView.getContext(), itemView)); diff --git a/tests/robotests/src/com/android/settings/dashboard/DashboardDataTest.java b/tests/robotests/src/com/android/settings/dashboard/DashboardDataTest.java index 77213f54cfe..68d33545bc1 100644 --- a/tests/robotests/src/com/android/settings/dashboard/DashboardDataTest.java +++ b/tests/robotests/src/com/android/settings/dashboard/DashboardDataTest.java @@ -19,6 +19,7 @@ package com.android.settings.dashboard; import android.support.annotation.NonNull; import android.support.v7.util.DiffUtil; import android.support.v7.util.ListUpdateCallback; +import android.widget.RemoteViews; import com.android.settings.TestConfig; import com.android.settings.dashboard.conditional.AirplaneModeCondition; @@ -35,6 +36,7 @@ import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Objects; @@ -224,6 +226,28 @@ public class DashboardDataTest { testDiffUtil(mDashboardDataWithOneConditions, mDashboardDataWithNoItems, testResultData); } + @Test + public void testDiffUtil_typeSuggestedContainer_ResultDataNothingChanged() { + //Build testResultData + final List testResultData = new ArrayList<>(); + testResultData.add(new ListUpdateResult.ResultData( + ListUpdateResult.ResultData.TYPE_OPERATION_CHANGE, 0, 1)); + Tile tile = new Tile(); + tile.remoteViews = mock(RemoteViews.class); + + DashboardData prevData = new DashboardData.Builder() + .setConditions(null) + .setCategory(null) + .setSuggestions(Arrays.asList(tile)) + .build(); + DashboardData currentData = new DashboardData.Builder() + .setConditions(null) + .setCategory(null) + .setSuggestions(Arrays.asList(tile)) + .build(); + testDiffUtil(prevData, currentData, testResultData); + } + /** * Test when using the * {@link com.android.settings.dashboard.DashboardData.ItemsDataDiffCallback} From bdc8fe6da9a57b7ca544194a0d0535602ab4b119 Mon Sep 17 00:00:00 2001 From: Matthew Fritze Date: Tue, 22 Aug 2017 15:51:50 -0700 Subject: [PATCH 08/17] Separate collection of indexable data from indexing The first step in refactoring the god class, DatabaseIndexingManager. The class has one major entry point: indexDatabase which begins a chain of calls that first collects all the data from the fragments, and then massages that data into the SQLite database. Unfortunately, most of the methods do not return data, and just pass along some mutated form of the data until it can be insterted. Reading and testing this class is very difficult. This first step moves the collection of the indexable data into a new class which has a few benefits: - The data can be easily mocked in tests - Reduces complexity of D.I.M. - Separates data collection from indexing, which allows the indexable data to be piped into a new API that unbundled search can consume. Bug:33577327 Test: make RunSettingsRoboTests Test: Grabbed a DB dump before change, compared to DB dump after change to make sure everything is still indexed. Change-Id: Ibc91e3d75ff5dcf5274b93b29bf3544f90b2194d --- .../search/DatabaseIndexingManager.java | 364 +++--------------- .../search/DatabaseIndexingUtils.java | 40 -- .../settings/search/DatabaseResultLoader.java | 26 +- .../settings/search/IndexDatabaseHelper.java | 48 +-- .../search/SearchFeatureProviderImpl.java | 3 +- .../indexing/IndexableDataCollector.java | 361 +++++++++++++++++ .../search/indexing/PreIndexData.java | 55 +++ .../search/DatabaseIndexingManagerTest.java | 267 ++++--------- .../indexing/IndexableDataCollectorTest.java | 170 ++++++++ 9 files changed, 729 insertions(+), 605 deletions(-) create mode 100644 src/com/android/settings/search/indexing/IndexableDataCollector.java create mode 100644 src/com/android/settings/search/indexing/PreIndexData.java create mode 100644 tests/robotests/src/com/android/settings/search/indexing/IndexableDataCollectorTest.java diff --git a/src/com/android/settings/search/DatabaseIndexingManager.java b/src/com/android/settings/search/DatabaseIndexingManager.java index 7f6f0129548..a0f47d16542 100644 --- a/src/com/android/settings/search/DatabaseIndexingManager.java +++ b/src/com/android/settings/search/DatabaseIndexingManager.java @@ -17,27 +17,6 @@ package com.android.settings.search; -import static android.provider.SearchIndexablesContract.COLUMN_INDEX_NON_INDEXABLE_KEYS_KEY_VALUE; -import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_CLASS_NAME; -import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_ENTRIES; -import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_ICON_RESID; -import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_INTENT_ACTION; -import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_INTENT_TARGET_CLASS; -import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_INTENT_TARGET_PACKAGE; -import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_KEY; -import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_KEYWORDS; -import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_RANK; -import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_SCREEN_TITLE; -import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_SUMMARY_OFF; -import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_SUMMARY_ON; -import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_TITLE; -import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_USER_ID; -import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_CLASS_NAME; -import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_ICON_RESID; -import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_INTENT_ACTION; -import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_INTENT_TARGET_CLASS; -import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_INTENT_TARGET_PACKAGE; -import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_RESID; import static com.android.settings.search.DatabaseResultLoader.COLUMN_INDEX_ID; import static com.android.settings.search.DatabaseResultLoader .COLUMN_INDEX_INTENT_ACTION_TARGET_PACKAGE; @@ -70,17 +49,14 @@ import static com.android.settings.search.IndexDatabaseHelper.IndexColumns.USER_ import static com.android.settings.search.IndexDatabaseHelper.Tables.TABLE_PREFS_INDEX; import android.content.ComponentName; -import android.content.ContentResolver; import android.content.ContentValues; import android.content.Context; import android.content.Intent; -import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.res.XmlResourceParser; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteException; -import android.net.Uri; import android.os.AsyncTask; import android.os.Build; import android.provider.SearchIndexableData; @@ -89,7 +65,6 @@ import android.provider.SearchIndexablesContract; import android.support.annotation.DrawableRes; import android.support.annotation.VisibleForTesting; import android.text.TextUtils; -import android.util.ArraySet; import android.util.AttributeSet; import android.util.Log; import android.util.Xml; @@ -98,13 +73,13 @@ import com.android.settings.SettingsActivity; import com.android.settings.core.PreferenceControllerMixin; import com.android.settings.overlay.FeatureFactory; +import com.android.settings.search.indexing.IndexableDataCollector; +import com.android.settings.search.indexing.PreIndexData; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import java.io.IOException; import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; @@ -132,23 +107,14 @@ public class DatabaseIndexingManager { private static final String NODE_NAME_CHECK_BOX_PREFERENCE = "CheckBoxPreference"; private static final String NODE_NAME_LIST_PREFERENCE = "ListPreference"; - private static final List EMPTY_LIST = Collections.emptyList(); - - private final String mBaseAuthority; - @VisibleForTesting final AtomicBoolean mIsIndexingComplete = new AtomicBoolean(false); - @VisibleForTesting - final UpdateData mDataToProcess = new UpdateData(); + private IndexableDataCollector mCollector; + private Context mContext; - public DatabaseIndexingManager(Context context, String baseAuthority) { - mContext = context; - mBaseAuthority = baseAuthority; - } - - public void setContext(Context context) { + public DatabaseIndexingManager(Context context) { mContext = context; } @@ -177,33 +143,17 @@ public class DatabaseIndexingManager { final String providerVersionedNames = IndexDatabaseHelper.buildProviderVersionedNames(providers); - final boolean isFullIndex = IndexDatabaseHelper.isFullIndex(mContext, localeStr, + final boolean isFullIndex = isFullIndex(mContext, localeStr, fingerprint, providerVersionedNames); if (isFullIndex) { rebuildDatabase(); } - for (final ResolveInfo info : providers) { - if (!DatabaseIndexingUtils.isWellKnownProvider(info, mContext)) { - continue; - } - final String authority = info.providerInfo.authority; - final String packageName = info.providerInfo.packageName; + PreIndexData indexData = getIndexDataFromProviders(providers, isFullIndex); - if (isFullIndex) { - addIndexablesFromRemoteProvider(packageName, authority); - } - final long nonIndexableStartTime = System.currentTimeMillis(); - addNonIndexablesKeysFromRemoteProvider(packageName, authority); - if (SettingsSearchIndexablesProvider.DEBUG) { - final long nonIndextableTime = System.currentTimeMillis() - nonIndexableStartTime; - Log.d(LOG_TAG, "performIndexing update non-indexable for package " + packageName - + " took time: " + nonIndextableTime); - } - } final long updateDatabaseStartTime = System.currentTimeMillis(); - updateDatabase(isFullIndex, localeStr); + updateDatabase(indexData, isFullIndex, localeStr); if (SettingsSearchIndexablesProvider.DEBUG) { final long updateDatabaseTime = System.currentTimeMillis() - updateDatabaseStartTime; Log.d(LOG_TAG, "performIndexing updateDatabase took time: " + updateDatabaseTime); @@ -221,10 +171,37 @@ public class DatabaseIndexingManager { } } + @VisibleForTesting + PreIndexData getIndexDataFromProviders(List providers, boolean isFullIndex) { + if (mCollector == null) { + mCollector = new IndexableDataCollector(mContext); + } + return mCollector.collectIndexableData(providers, isFullIndex); + } + /** - * Reconstruct the database in the following cases: - * - Language has changed - * - Build has changed + * Checks if the indexed data is obsolete, when either: + * - Device language has changed + * - Device has taken an OTA. + * In both cases, the device requires a full index. + * + * @param locale is the default for the device + * @param fingerprint id for the current build. + * @return true if a full index should be preformed. + */ + @VisibleForTesting + boolean isFullIndex(Context context, String locale, String fingerprint, + String providerVersionedNames) { + final boolean isLocaleIndexed = IndexDatabaseHelper.isLocaleAlreadyIndexed(context, locale); + final boolean isBuildIndexed = IndexDatabaseHelper.isBuildIndexed(context, fingerprint); + final boolean areProvidersIndexed = IndexDatabaseHelper + .areProvidersIndexed(context, providerVersionedNames); + + return !(isLocaleIndexed && isBuildIndexed && areProvidersIndexed); + } + + /** + * Drop the currently stored database, and clear the flags which mark the database as indexed. */ private void rebuildDatabase() { // Drop the database when the locale or build has changed. This eliminates rows which are @@ -244,16 +221,9 @@ public class DatabaseIndexingManager { * @param localeStr the default locale for the device. */ @VisibleForTesting - void updateDatabase(boolean needsReindexing, String localeStr) { - final UpdateData copy; - - synchronized (mDataToProcess) { - copy = mDataToProcess.copy(); - mDataToProcess.clear(); - } - - final List dataToUpdate = copy.dataToUpdate; - final Map> nonIndexableKeys = copy.nonIndexableKeys; + void updateDatabase(PreIndexData indexData, boolean needsReindexing, String localeStr) { + final List dataToUpdate = indexData.dataToUpdate; + final Map> nonIndexableKeys = indexData.nonIndexableKeys; final SQLiteDatabase database = getWritableDatabase(); if (database == null) { @@ -378,99 +348,10 @@ public class DatabaseIndexingManager { disabledResults.close(); } - @VisibleForTesting - boolean addIndexablesFromRemoteProvider(String packageName, String authority) { - try { - final Context context = mBaseAuthority.equals(authority) ? - mContext : mContext.createPackageContext(packageName, 0); - - final Uri uriForResources = buildUriForXmlResources(authority); - addIndexablesForXmlResourceUri(context, packageName, uriForResources, - SearchIndexablesContract.INDEXABLES_XML_RES_COLUMNS); - - final Uri uriForRawData = buildUriForRawData(authority); - addIndexablesForRawDataUri(context, packageName, uriForRawData, - SearchIndexablesContract.INDEXABLES_RAW_COLUMNS); - return true; - } catch (PackageManager.NameNotFoundException e) { - Log.w(LOG_TAG, "Could not create context for " + packageName + ": " - + Log.getStackTraceString(e)); - return false; - } - } - - @VisibleForTesting - void addNonIndexablesKeysFromRemoteProvider(String packageName, - String authority) { - final List keys = - getNonIndexablesKeysFromRemoteProvider(packageName, authority); - - addNonIndexableKeys(packageName, keys); - } - - private List getNonIndexablesKeysFromRemoteProvider(String packageName, - String authority) { - try { - final Context packageContext = mContext.createPackageContext(packageName, 0); - - final Uri uriForNonIndexableKeys = buildUriForNonIndexableKeys(authority); - return getNonIndexablesKeys(packageContext, uriForNonIndexableKeys, - SearchIndexablesContract.NON_INDEXABLES_KEYS_COLUMNS); - } catch (PackageManager.NameNotFoundException e) { - Log.w(LOG_TAG, "Could not create context for " + packageName + ": " - + Log.getStackTraceString(e)); - return EMPTY_LIST; - } - } - - private List getNonIndexablesKeys(Context packageContext, Uri uri, - String[] projection) { - - final ContentResolver resolver = packageContext.getContentResolver(); - final Cursor cursor = resolver.query(uri, projection, null, null, null); - - if (cursor == null) { - Log.w(LOG_TAG, "Cannot add index data for Uri: " + uri.toString()); - return EMPTY_LIST; - } - - final List result = new ArrayList<>(); - try { - final int count = cursor.getCount(); - if (count > 0) { - while (cursor.moveToNext()) { - final String key = cursor.getString(COLUMN_INDEX_NON_INDEXABLE_KEYS_KEY_VALUE); - - if (TextUtils.isEmpty(key) && Log.isLoggable(LOG_TAG, Log.VERBOSE)) { - Log.v(LOG_TAG, "Empty non-indexable key from: " - + packageContext.getPackageName()); - continue; - } - - result.add(key); - } - } - return result; - } finally { - cursor.close(); - } - } - - public void addIndexableData(SearchIndexableData data) { - synchronized (mDataToProcess) { - mDataToProcess.dataToUpdate.add(data); - } - } - - public void addNonIndexableKeys(String authority, List keys) { - synchronized (mDataToProcess) { - if (keys != null && !keys.isEmpty()) { - mDataToProcess.nonIndexableKeys.put(authority, new ArraySet<>(keys)); - } - } - } /** + * TODO (b/64951285): Deprecate this method + * * Update the Index for a specific class name resources * * @param className the class name (typically a fragment name). @@ -491,9 +372,9 @@ public class DatabaseIndexingManager { AsyncTask.execute(new Runnable() { @Override public void run() { - addIndexableData(res); - updateDatabase(false, Locale.getDefault().toString()); - res.enabled = false; +// addIndexableData(res); +// updateDatabase(false, Locale.getDefault().toString()); +// res.enabled = false; } }); } @@ -507,126 +388,9 @@ public class DatabaseIndexingManager { } } - private static Uri buildUriForXmlResources(String authority) { - return Uri.parse("content://" + authority + "/" + - SearchIndexablesContract.INDEXABLES_XML_RES_PATH); - } - private static Uri buildUriForRawData(String authority) { - return Uri.parse("content://" + authority + "/" + - SearchIndexablesContract.INDEXABLES_RAW_PATH); - } - - private static Uri buildUriForNonIndexableKeys(String authority) { - return Uri.parse("content://" + authority + "/" + - SearchIndexablesContract.NON_INDEXABLES_KEYS_PATH); - } - - private void addIndexablesForXmlResourceUri(Context packageContext, String packageName, - Uri uri, String[] projection) { - - final ContentResolver resolver = packageContext.getContentResolver(); - final Cursor cursor = resolver.query(uri, projection, null, null, null); - - if (cursor == null) { - Log.w(LOG_TAG, "Cannot add index data for Uri: " + uri.toString()); - return; - } - - try { - final int count = cursor.getCount(); - if (count > 0) { - while (cursor.moveToNext()) { - final int xmlResId = cursor.getInt(COLUMN_INDEX_XML_RES_RESID); - - final String className = cursor.getString(COLUMN_INDEX_XML_RES_CLASS_NAME); - final int iconResId = cursor.getInt(COLUMN_INDEX_XML_RES_ICON_RESID); - - final String action = cursor.getString(COLUMN_INDEX_XML_RES_INTENT_ACTION); - final String targetPackage = cursor.getString( - COLUMN_INDEX_XML_RES_INTENT_TARGET_PACKAGE); - final String targetClass = cursor.getString( - COLUMN_INDEX_XML_RES_INTENT_TARGET_CLASS); - - SearchIndexableResource sir = new SearchIndexableResource(packageContext); - sir.xmlResId = xmlResId; - sir.className = className; - sir.packageName = packageName; - sir.iconResId = iconResId; - sir.intentAction = action; - sir.intentTargetPackage = targetPackage; - sir.intentTargetClass = targetClass; - - addIndexableData(sir); - } - } - } finally { - cursor.close(); - } - } - - private void addIndexablesForRawDataUri(Context packageContext, String packageName, - Uri uri, String[] projection) { - - final ContentResolver resolver = packageContext.getContentResolver(); - final Cursor cursor = resolver.query(uri, projection, null, null, null); - - if (cursor == null) { - Log.w(LOG_TAG, "Cannot add index data for Uri: " + uri.toString()); - return; - } - - try { - final int count = cursor.getCount(); - if (count > 0) { - while (cursor.moveToNext()) { - final int providerRank = cursor.getInt(COLUMN_INDEX_RAW_RANK); - // TODO Remove rank - final String title = cursor.getString(COLUMN_INDEX_RAW_TITLE); - final String summaryOn = cursor.getString(COLUMN_INDEX_RAW_SUMMARY_ON); - final String summaryOff = cursor.getString(COLUMN_INDEX_RAW_SUMMARY_OFF); - final String entries = cursor.getString(COLUMN_INDEX_RAW_ENTRIES); - final String keywords = cursor.getString(COLUMN_INDEX_RAW_KEYWORDS); - - final String screenTitle = cursor.getString(COLUMN_INDEX_RAW_SCREEN_TITLE); - - final String className = cursor.getString(COLUMN_INDEX_RAW_CLASS_NAME); - final int iconResId = cursor.getInt(COLUMN_INDEX_RAW_ICON_RESID); - - final String action = cursor.getString(COLUMN_INDEX_RAW_INTENT_ACTION); - final String targetPackage = cursor.getString( - COLUMN_INDEX_RAW_INTENT_TARGET_PACKAGE); - final String targetClass = cursor.getString( - COLUMN_INDEX_RAW_INTENT_TARGET_CLASS); - - final String key = cursor.getString(COLUMN_INDEX_RAW_KEY); - final int userId = cursor.getInt(COLUMN_INDEX_RAW_USER_ID); - - SearchIndexableRaw data = new SearchIndexableRaw(packageContext); - data.title = title; - data.summaryOn = summaryOn; - data.summaryOff = summaryOff; - data.entries = entries; - data.keywords = keywords; - data.screenTitle = screenTitle; - data.className = className; - data.packageName = packageName; - data.iconResId = iconResId; - data.intentAction = action; - data.intentTargetPackage = targetPackage; - data.intentTargetClass = targetClass; - data.key = key; - data.userId = userId; - - addIndexableData(data); - } - } - } finally { - cursor.close(); - } - } - - public void indexOneSearchIndexableData(SQLiteDatabase database, String localeStr, + @VisibleForTesting + void indexOneSearchIndexableData(SQLiteDatabase database, String localeStr, SearchIndexableData data, Map> nonIndexableKeys) { if (data instanceof SearchIndexableResource) { indexOneResource(database, localeStr, (SearchIndexableResource) data, nonIndexableKeys); @@ -1010,38 +774,6 @@ public class DatabaseIndexingManager { } } - /** - * A private class to describe the indexDatabase data for the Index database - */ - @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) - static class UpdateData { - public List dataToUpdate; - public List dataToDisable; - public Map> nonIndexableKeys; - - public UpdateData() { - dataToUpdate = new ArrayList<>(); - dataToDisable = new ArrayList<>(); - nonIndexableKeys = new HashMap<>(); - } - - public UpdateData(UpdateData other) { - dataToUpdate = new ArrayList<>(other.dataToUpdate); - dataToDisable = new ArrayList<>(other.dataToDisable); - nonIndexableKeys = new HashMap<>(other.nonIndexableKeys); - } - - public UpdateData copy() { - return new UpdateData(this); - } - - public void clear() { - dataToUpdate.clear(); - dataToDisable.clear(); - nonIndexableKeys.clear(); - } - } - public static class DatabaseRow { public final String locale; public final String updatedTitle; diff --git a/src/com/android/settings/search/DatabaseIndexingUtils.java b/src/com/android/settings/search/DatabaseIndexingUtils.java index 938ddb74b3f..7093134d769 100644 --- a/src/com/android/settings/search/DatabaseIndexingUtils.java +++ b/src/com/android/settings/search/DatabaseIndexingUtils.java @@ -174,35 +174,6 @@ public class DatabaseIndexingUtils { return null; } - /** - * Only allow a "well known" SearchIndexablesProvider. The provider should: - * - * - have read/write {@link Manifest.permission#READ_SEARCH_INDEXABLES} - * - be from a privileged package - */ - static boolean isWellKnownProvider(ResolveInfo info, Context context) { - final String authority = info.providerInfo.authority; - final String packageName = info.providerInfo.applicationInfo.packageName; - - if (TextUtils.isEmpty(authority) || TextUtils.isEmpty(packageName)) { - return false; - } - - final String readPermission = info.providerInfo.readPermission; - final String writePermission = info.providerInfo.writePermission; - - if (TextUtils.isEmpty(readPermission) || TextUtils.isEmpty(writePermission)) { - return false; - } - - if (!android.Manifest.permission.READ_SEARCH_INDEXABLES.equals(readPermission) || - !android.Manifest.permission.READ_SEARCH_INDEXABLES.equals(writePermission)) { - return false; - } - - return isPrivilegedPackage(packageName, context); - } - static String normalizeHyphen(String input) { return (input != null) ? input.replaceAll(NON_BREAKING_HYPHEN, HYPHEN) : EMPTY; } @@ -217,15 +188,4 @@ public class DatabaseIndexingUtils { static String normalizeKeywords(String input) { return (input != null) ? input.replaceAll(LIST_DELIMITERS, SPACE) : EMPTY; } - - private static boolean isPrivilegedPackage(String packageName, Context context) { - final PackageManager pm = context.getPackageManager(); - try { - PackageInfo packInfo = pm.getPackageInfo(packageName, 0); - return ((packInfo.applicationInfo.privateFlags - & ApplicationInfo.PRIVATE_FLAG_PRIVILEGED) != 0); - } catch (PackageManager.NameNotFoundException e) { - return false; - } - } } diff --git a/src/com/android/settings/search/DatabaseResultLoader.java b/src/com/android/settings/search/DatabaseResultLoader.java index 7815a45ad2c..3d280b2002e 100644 --- a/src/com/android/settings/search/DatabaseResultLoader.java +++ b/src/com/android/settings/search/DatabaseResultLoader.java @@ -40,19 +40,19 @@ public class DatabaseResultLoader extends AsyncLoader providers) { StringBuilder sb = new StringBuilder(); @@ -282,44 +268,42 @@ public class IndexDatabaseHelper extends SQLiteOpenHelper { return sb.toString(); } - static void clearCachedIndexed(Context context) { - context.getSharedPreferences(INDEX, Context.MODE_PRIVATE).edit().clear().commit(); - } - static void setLocaleIndexed(Context context, String locale) { - context.getSharedPreferences(INDEX, Context.MODE_PRIVATE) + context.getSharedPreferences(SHARED_PREFS_TAG, Context.MODE_PRIVATE) .edit() .putBoolean(locale, true) .apply(); } static void setProvidersIndexed(Context context, String providerVersionedNames) { - context.getSharedPreferences(INDEX, Context.MODE_PRIVATE) + context.getSharedPreferences(SHARED_PREFS_TAG, Context.MODE_PRIVATE) .edit() .putString(PREF_KEY_INDEXED_PROVIDERS, providerVersionedNames) .apply(); } static boolean isLocaleAlreadyIndexed(Context context, String locale) { - return context.getSharedPreferences(INDEX, Context.MODE_PRIVATE).getBoolean(locale, false); + return context.getSharedPreferences(SHARED_PREFS_TAG, Context.MODE_PRIVATE) + .getBoolean(locale, false); } static boolean areProvidersIndexed(Context context, String providerVersionedNames) { - final String indexedProviders = context.getSharedPreferences(INDEX, Context.MODE_PRIVATE) - .getString(PREF_KEY_INDEXED_PROVIDERS, null); + final String indexedProviders = + context.getSharedPreferences(SHARED_PREFS_TAG, Context.MODE_PRIVATE) + .getString(PREF_KEY_INDEXED_PROVIDERS, null); return TextUtils.equals(indexedProviders, providerVersionedNames); } static boolean isBuildIndexed(Context context, String buildNo) { - return context.getSharedPreferences(INDEX, Context.MODE_PRIVATE).getBoolean(buildNo, false); + return context.getSharedPreferences(SHARED_PREFS_TAG, + Context.MODE_PRIVATE).getBoolean(buildNo, false); } static void setBuildIndexed(Context context, String buildNo) { - context.getSharedPreferences(INDEX, 0).edit().putBoolean(buildNo, true).commit(); + context.getSharedPreferences(SHARED_PREFS_TAG, 0).edit().putBoolean(buildNo, true).commit(); } private void dropTables(SQLiteDatabase db) { - clearCachedIndexed(mContext); db.execSQL("DROP TABLE IF EXISTS " + Tables.TABLE_META_INDEX); db.execSQL("DROP TABLE IF EXISTS " + Tables.TABLE_PREFS_INDEX); db.execSQL("DROP TABLE IF EXISTS " + Tables.TABLE_SAVED_QUERIES); diff --git a/src/com/android/settings/search/SearchFeatureProviderImpl.java b/src/com/android/settings/search/SearchFeatureProviderImpl.java index 400cf8fe5f3..69c086f1659 100644 --- a/src/com/android/settings/search/SearchFeatureProviderImpl.java +++ b/src/com/android/settings/search/SearchFeatureProviderImpl.java @@ -74,8 +74,7 @@ public class SearchFeatureProviderImpl implements SearchFeatureProvider { @Override public DatabaseIndexingManager getIndexingManager(Context context) { if (mDatabaseIndexingManager == null) { - mDatabaseIndexingManager = new DatabaseIndexingManager(context.getApplicationContext(), - context.getPackageName()); + mDatabaseIndexingManager = new DatabaseIndexingManager(context.getApplicationContext()); } return mDatabaseIndexingManager; } diff --git a/src/com/android/settings/search/indexing/IndexableDataCollector.java b/src/com/android/settings/search/indexing/IndexableDataCollector.java new file mode 100644 index 00000000000..cd6b9bbd2c0 --- /dev/null +++ b/src/com/android/settings/search/indexing/IndexableDataCollector.java @@ -0,0 +1,361 @@ +/* + * 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.search.indexing; + +import android.Manifest; +import android.content.ContentResolver; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.database.Cursor; +import android.net.Uri; +import android.provider.SearchIndexableResource; +import android.provider.SearchIndexablesContract; +import android.text.TextUtils; +import android.util.ArraySet; +import android.util.Log; +import com.android.internal.annotations.VisibleForTesting; +import com.android.settings.search.SearchIndexableRaw; +import com.android.settings.search.SettingsSearchIndexablesProvider; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_CLASS_NAME; +import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_ICON_RESID; +import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_RESID; + +import static android.provider.SearchIndexablesContract.COLUMN_INDEX_NON_INDEXABLE_KEYS_KEY_VALUE; +import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_CLASS_NAME; +import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_ENTRIES; +import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_ICON_RESID; +import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_INTENT_ACTION; +import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_INTENT_TARGET_CLASS; +import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_INTENT_TARGET_PACKAGE; +import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_KEY; +import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_KEYWORDS; +import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_RANK; +import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_SCREEN_TITLE; +import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_SUMMARY_OFF; +import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_SUMMARY_ON; +import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_TITLE; +import static android.provider.SearchIndexablesContract.COLUMN_INDEX_RAW_USER_ID; +import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_INTENT_ACTION; +import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_INTENT_TARGET_CLASS; +import static android.provider.SearchIndexablesContract.COLUMN_INDEX_XML_RES_INTENT_TARGET_PACKAGE; + +/** + * Collects all data from {@link android.provider.SearchIndexablesProvider} to be indexed. + */ +public class IndexableDataCollector { + + private static final String TAG = "IndexableDataCollector"; + + // TODO (b/64938328) update to new search package. + private final String BASE_AUTHORITY = "com.android.settings"; + + private static final List EMPTY_LIST = Collections.emptyList(); + + private Context mContext; + + private PreIndexData mIndexData; + + public IndexableDataCollector(Context context) { + mContext = context; + } + + public PreIndexData collectIndexableData(List providers, boolean isFullIndex) { + mIndexData = new PreIndexData(); + + for (final ResolveInfo info : providers) { + if (!isWellKnownProvider(info)) { + continue; + } + final String authority = info.providerInfo.authority; + final String packageName = info.providerInfo.packageName; + + if (isFullIndex) { + addIndexablesFromRemoteProvider(packageName, authority); + } + + final long nonIndexableStartTime = System.currentTimeMillis(); + addNonIndexablesKeysFromRemoteProvider(packageName, authority); + if (SettingsSearchIndexablesProvider.DEBUG) { + final long nonIndexableTime = System.currentTimeMillis() - nonIndexableStartTime; + Log.d(TAG, "performIndexing update non-indexable for package " + packageName + + " took time: " + nonIndexableTime); + } + } + + return mIndexData; + } + + private boolean addIndexablesFromRemoteProvider(String packageName, String authority) { + try { + final Context context = BASE_AUTHORITY.equals(authority) ? + mContext : mContext.createPackageContext(packageName, 0); + + final Uri uriForResources = buildUriForXmlResources(authority); + mIndexData.dataToUpdate.addAll(getIndexablesForXmlResourceUri(context, packageName, + uriForResources, SearchIndexablesContract.INDEXABLES_XML_RES_COLUMNS)); + + final Uri uriForRawData = buildUriForRawData(authority); + mIndexData.dataToUpdate.addAll(getIndexablesForRawDataUri(context, packageName, + uriForRawData, SearchIndexablesContract.INDEXABLES_RAW_COLUMNS)); + return true; + } catch (PackageManager.NameNotFoundException e) { + Log.w(TAG, "Could not create context for " + packageName + ": " + + Log.getStackTraceString(e)); + return false; + } + } + + @VisibleForTesting + List getIndexablesForXmlResourceUri(Context packageContext, + String packageName, Uri uri, String[] projection) { + + final ContentResolver resolver = packageContext.getContentResolver(); + final Cursor cursor = resolver.query(uri, projection, null, null, null); + List resources = new ArrayList<>(); + + if (cursor == null) { + Log.w(TAG, "Cannot add index data for Uri: " + uri.toString()); + return resources; + } + + try { + final int count = cursor.getCount(); + if (count > 0) { + while (cursor.moveToNext()) { + final int xmlResId = cursor.getInt(COLUMN_INDEX_XML_RES_RESID); + + final String className = cursor.getString(COLUMN_INDEX_XML_RES_CLASS_NAME); + final int iconResId = cursor.getInt(COLUMN_INDEX_XML_RES_ICON_RESID); + + final String action = cursor.getString(COLUMN_INDEX_XML_RES_INTENT_ACTION); + final String targetPackage = cursor.getString( + COLUMN_INDEX_XML_RES_INTENT_TARGET_PACKAGE); + final String targetClass = cursor.getString( + COLUMN_INDEX_XML_RES_INTENT_TARGET_CLASS); + + SearchIndexableResource sir = new SearchIndexableResource(packageContext); + sir.xmlResId = xmlResId; + sir.className = className; + sir.packageName = packageName; + sir.iconResId = iconResId; + sir.intentAction = action; + sir.intentTargetPackage = targetPackage; + sir.intentTargetClass = targetClass; + + resources.add(sir); + } + } + } finally { + cursor.close(); + } + return resources; + } + + private void addNonIndexablesKeysFromRemoteProvider(String packageName, + String authority) { + final List keys = + getNonIndexablesKeysFromRemoteProvider(packageName, authority); + + if (keys != null && !keys.isEmpty()) { + mIndexData.nonIndexableKeys.put(authority, new ArraySet<>(keys)); + } + } + + @VisibleForTesting + List getNonIndexablesKeysFromRemoteProvider(String packageName, + String authority) { + try { + final Context packageContext = mContext.createPackageContext(packageName, 0); + + final Uri uriForNonIndexableKeys = buildUriForNonIndexableKeys(authority); + return getNonIndexablesKeys(packageContext, uriForNonIndexableKeys, + SearchIndexablesContract.NON_INDEXABLES_KEYS_COLUMNS); + } catch (PackageManager.NameNotFoundException e) { + Log.w(TAG, "Could not create context for " + packageName + ": " + + Log.getStackTraceString(e)); + return EMPTY_LIST; + } + } + + @VisibleForTesting + Uri buildUriForXmlResources(String authority) { + return Uri.parse("content://" + authority + "/" + + SearchIndexablesContract.INDEXABLES_XML_RES_PATH); + } + + @VisibleForTesting + Uri buildUriForRawData(String authority) { + return Uri.parse("content://" + authority + "/" + + SearchIndexablesContract.INDEXABLES_RAW_PATH); + } + + @VisibleForTesting + Uri buildUriForNonIndexableKeys(String authority) { + return Uri.parse("content://" + authority + "/" + + SearchIndexablesContract.NON_INDEXABLES_KEYS_PATH); + } + + @VisibleForTesting + List getIndexablesForRawDataUri(Context packageContext, String packageName, + Uri uri, String[] projection) { + final ContentResolver resolver = packageContext.getContentResolver(); + final Cursor cursor = resolver.query(uri, projection, null, null, null); + List rawData = new ArrayList<>(); + + if (cursor == null) { + Log.w(TAG, "Cannot add index data for Uri: " + uri.toString()); + return rawData; + } + + try { + final int count = cursor.getCount(); + if (count > 0) { + while (cursor.moveToNext()) { + final int providerRank = cursor.getInt(COLUMN_INDEX_RAW_RANK); + // TODO Remove rank + final String title = cursor.getString(COLUMN_INDEX_RAW_TITLE); + final String summaryOn = cursor.getString(COLUMN_INDEX_RAW_SUMMARY_ON); + final String summaryOff = cursor.getString(COLUMN_INDEX_RAW_SUMMARY_OFF); + final String entries = cursor.getString(COLUMN_INDEX_RAW_ENTRIES); + final String keywords = cursor.getString(COLUMN_INDEX_RAW_KEYWORDS); + + final String screenTitle = cursor.getString(COLUMN_INDEX_RAW_SCREEN_TITLE); + + final String className = cursor.getString(COLUMN_INDEX_RAW_CLASS_NAME); + final int iconResId = cursor.getInt(COLUMN_INDEX_RAW_ICON_RESID); + + final String action = cursor.getString(COLUMN_INDEX_RAW_INTENT_ACTION); + final String targetPackage = cursor.getString( + COLUMN_INDEX_RAW_INTENT_TARGET_PACKAGE); + final String targetClass = cursor.getString( + COLUMN_INDEX_RAW_INTENT_TARGET_CLASS); + + final String key = cursor.getString(COLUMN_INDEX_RAW_KEY); + final int userId = cursor.getInt(COLUMN_INDEX_RAW_USER_ID); + + SearchIndexableRaw data = new SearchIndexableRaw(packageContext); + data.title = title; + data.summaryOn = summaryOn; + data.summaryOff = summaryOff; + data.entries = entries; + data.keywords = keywords; + data.screenTitle = screenTitle; + data.className = className; + data.packageName = packageName; + data.iconResId = iconResId; + data.intentAction = action; + data.intentTargetPackage = targetPackage; + data.intentTargetClass = targetClass; + data.key = key; + data.userId = userId; + + rawData.add(data); + } + } + } finally { + cursor.close(); + } + + return rawData; + } + + private List getNonIndexablesKeys(Context packageContext, Uri uri, + String[] projection) { + + final ContentResolver resolver = packageContext.getContentResolver(); + final Cursor cursor = resolver.query(uri, projection, null, null, null); + final List result = new ArrayList<>(); + + if (cursor == null) { + Log.w(TAG, "Cannot add index data for Uri: " + uri.toString()); + return result; + } + + try { + final int count = cursor.getCount(); + if (count > 0) { + while (cursor.moveToNext()) { + final String key = cursor.getString(COLUMN_INDEX_NON_INDEXABLE_KEYS_KEY_VALUE); + + if (TextUtils.isEmpty(key) && Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "Empty non-indexable key from: " + + packageContext.getPackageName()); + continue; + } + + result.add(key); + } + } + return result; + } finally { + cursor.close(); + } + } + + /** + * Only allow a "well known" SearchIndexablesProvider. The provider should: + * + * - have read/write {@link Manifest.permission#READ_SEARCH_INDEXABLES} + * - be from a privileged package + */ + @VisibleForTesting + boolean isWellKnownProvider(ResolveInfo info) { + final String authority = info.providerInfo.authority; + final String packageName = info.providerInfo.applicationInfo.packageName; + + if (TextUtils.isEmpty(authority) || TextUtils.isEmpty(packageName)) { + return false; + } + + final String readPermission = info.providerInfo.readPermission; + final String writePermission = info.providerInfo.writePermission; + + if (TextUtils.isEmpty(readPermission) || TextUtils.isEmpty(writePermission)) { + return false; + } + + if (!android.Manifest.permission.READ_SEARCH_INDEXABLES.equals(readPermission) || + !android.Manifest.permission.READ_SEARCH_INDEXABLES.equals(writePermission)) { + return false; + } + + return isPrivilegedPackage(packageName, mContext); + } + + /** + * @return true if the {@param packageName} is privileged. + */ + private boolean isPrivilegedPackage(String packageName, Context context) { + final PackageManager pm = context.getPackageManager(); + try { + PackageInfo packInfo = pm.getPackageInfo(packageName, 0); + return ((packInfo.applicationInfo.privateFlags + & ApplicationInfo.PRIVATE_FLAG_PRIVILEGED) != 0); + } catch (PackageManager.NameNotFoundException e) { + return false; + } + } +} diff --git a/src/com/android/settings/search/indexing/PreIndexData.java b/src/com/android/settings/search/indexing/PreIndexData.java new file mode 100644 index 00000000000..de3cf7ca273 --- /dev/null +++ b/src/com/android/settings/search/indexing/PreIndexData.java @@ -0,0 +1,55 @@ +/* + * 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.search.indexing; + +import android.provider.SearchIndexableData; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + + +/** + * Holds Data sources for indexable data. + * TODO (b/33577327) add getters and setters for data. + */ +public class PreIndexData { + public List dataToUpdate; + public Map> nonIndexableKeys; + + public PreIndexData() { + dataToUpdate = new ArrayList<>(); + nonIndexableKeys = new HashMap<>(); + } + + public PreIndexData(PreIndexData other) { + dataToUpdate = new ArrayList<>(other.dataToUpdate); + nonIndexableKeys = new HashMap<>(other.nonIndexableKeys); + } + + public PreIndexData copy() { + return new PreIndexData(this); + } + + public void clear() { + dataToUpdate.clear(); + nonIndexableKeys.clear(); + } +} diff --git a/tests/robotests/src/com/android/settings/search/DatabaseIndexingManagerTest.java b/tests/robotests/src/com/android/settings/search/DatabaseIndexingManagerTest.java index 70ed568447d..fff38c59322 100644 --- a/tests/robotests/src/com/android/settings/search/DatabaseIndexingManagerTest.java +++ b/tests/robotests/src/com/android/settings/search/DatabaseIndexingManagerTest.java @@ -17,24 +17,20 @@ package com.android.settings.search; -import static android.provider.SearchIndexablesContract.INDEXABLES_RAW_COLUMNS; 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.anyList; import static org.mockito.Matchers.anyMap; import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.anyBoolean; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; -import android.annotation.NonNull; -import android.annotation.Nullable; -import android.content.ContentProvider; import android.content.ContentValues; import android.content.Context; import android.content.Intent; @@ -43,19 +39,18 @@ import android.content.pm.PackageManager; import android.content.pm.ProviderInfo; import android.content.pm.ResolveInfo; import android.database.Cursor; -import android.database.MatrixCursor; import android.database.sqlite.SQLiteDatabase; -import android.net.Uri; import android.os.Build; +import android.provider.SearchIndexableData; import android.provider.SearchIndexableResource; import android.util.ArrayMap; import com.android.settings.R; import com.android.settings.TestConfig; +import com.android.settings.search.indexing.PreIndexData; import com.android.settings.testutils.DatabaseTestUtils; import com.android.settings.testutils.FakeFeatureFactory; import com.android.settings.testutils.SettingsRobolectricTestRunner; -import com.android.settings.testutils.shadow.ShadowDatabaseIndexingUtils; import com.android.settings.testutils.shadow.ShadowRunnableAsyncTask; import org.junit.After; @@ -67,7 +62,6 @@ import org.mockito.MockitoAnnotations; import org.robolectric.Robolectric; import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Config; -import org.robolectric.shadows.ShadowContentResolver; import java.util.ArrayList; import java.util.Arrays; @@ -84,8 +78,6 @@ import java.util.Set; sdk = TestConfig.SDK_VERSION, shadows = { ShadowRunnableAsyncTask.class, - ShadowDatabaseIndexingUtils.class, - ShadowContentResolver.class } ) public class DatabaseIndexingManagerTest { @@ -129,6 +121,8 @@ public class DatabaseIndexingManagerTest { private DatabaseIndexingManager mManager; private SQLiteDatabase mDb; + private final List FAKE_PROVIDER_LIST = new ArrayList<>(); + @Mock private PackageManager mPackageManager; @@ -136,10 +130,12 @@ public class DatabaseIndexingManagerTest { public void setUp() { MockitoAnnotations.initMocks(this); mContext = spy(RuntimeEnvironment.application); - mManager = spy(new DatabaseIndexingManager(mContext, PACKAGE_ONE)); + mManager = spy(new DatabaseIndexingManager(mContext)); mDb = IndexDatabaseHelper.getInstance(mContext).getWritableDatabase(); doReturn(mPackageManager).when(mContext).getPackageManager(); + doReturn(FAKE_PROVIDER_LIST).when(mPackageManager) + .queryIntentContentProviders(any(Intent.class), anyInt()); FakeFeatureFactory.setupForTest(mContext); } @@ -755,113 +751,60 @@ public class DatabaseIndexingManagerTest { @Test public void testPerformIndexing_fullIndex_getsDataFromProviders() { - DummyProvider provider = new DummyProvider(); - provider.onCreate(); - ShadowContentResolver.registerProvider(AUTHORITY_ONE, provider); + SearchIndexableRaw rawData = getFakeRaw(); + PreIndexData data = getPreIndexData(rawData); + doReturn(data).when(mManager).getIndexDataFromProviders(anyList(), anyBoolean()); + doReturn(true).when(mManager).isFullIndex(any(Context.class), anyString(), anyString(), + anyString()); - // Test that Indexables are added for Full indexing - when(mPackageManager.queryIntentContentProviders(any(Intent.class), anyInt())) - .thenReturn(getDummyResolveInfo()); + mManager.performIndexing(); - DatabaseIndexingManager manager = - spy(new DatabaseIndexingManager(mContext, PACKAGE_ONE)); - - manager.performIndexing(); - - verify(manager).addIndexablesFromRemoteProvider(PACKAGE_ONE, AUTHORITY_ONE); - verify(manager).updateDatabase(true /* isFullIndex */, Locale.getDefault().toString()); + verify(mManager).updateDatabase(data, true /* isFullIndex */, + Locale.getDefault().toString()); } @Test - public void testPerformIndexing_incrementalIndex_noDataAdded() { - final List providerInfo = getDummyResolveInfo(); - skipFullIndex(providerInfo); - DummyProvider provider = new DummyProvider(); - provider.onCreate(); - ShadowContentResolver.registerProvider(AUTHORITY_ONE, provider); - // Test that Indexables are added for Full indexing - when(mPackageManager.queryIntentContentProviders(any(Intent.class), anyInt())) - .thenReturn(providerInfo); - + public void testPerformIndexing_fullIndex_databaseDropped() { + // Initialize the Manager and force rebuild DatabaseIndexingManager manager = - spy(new DatabaseIndexingManager(mContext, PACKAGE_ONE)); - - manager.mDataToProcess.dataToUpdate.clear(); - - manager.performIndexing(); - - verify(manager, times(0)).addDataToDatabase(any(SQLiteDatabase.class), anyString(), - anyList(), anyMap()); - verify(manager, times(0)).addIndexablesFromRemoteProvider(PACKAGE_ONE, AUTHORITY_ONE); - verify(manager).updateDataInDatabase(any(SQLiteDatabase.class), anyMap()); - } - - @Test - public void testPerformIndexing_localeChanged_databaseDropped() { - DummyProvider provider = new DummyProvider(); - provider.onCreate(); - ShadowContentResolver.registerProvider(AUTHORITY_ONE, provider); - - // Test that Indexables are added for Full indexing - when(mPackageManager.queryIntentContentProviders(any(Intent.class), anyInt())) - .thenReturn(getDummyResolveInfo()); - - // Initialize the Manager - DatabaseIndexingManager manager = - spy(new DatabaseIndexingManager(mContext, PACKAGE_ONE)); + spy(new DatabaseIndexingManager(mContext)); + doReturn(false).when(mManager).isFullIndex(any(Context.class), anyString(), anyString(), + anyString()); // Insert data point which will be dropped - final String oldTitle = "This is French"; - insertSpecialCase(oldTitle, true, "key"); - - // Add a data point to be added by the indexing - SearchIndexableRaw raw = new SearchIndexableRaw(mContext); - final String newTitle = "This is English"; - raw.title = newTitle; - manager.mDataToProcess.dataToUpdate.add(raw); + insertSpecialCase("Ceci n'est pas un pipe", true, "oui oui mon ami"); manager.performIndexing(); - // Assert that the New Title is inserted - final Cursor newCursor = mDb.rawQuery("SELECT * FROM prefs_index WHERE data_title = '" + - newTitle + "'", null); - assertThat(newCursor.getCount()).isEqualTo(1); - // Assert that the Old Title is no longer in the database, since it was dropped - final Cursor oldCursor = mDb.rawQuery("SELECT * FROM prefs_index WHERE data_title = '" + - oldTitle + "'", null); + final Cursor oldCursor = mDb.rawQuery("SELECT * FROM prefs_index", null); + assertThat(oldCursor.getCount()).isEqualTo(0); } @Test - public void testPerformIndexing_onOta_FullIndex() { - DummyProvider provider = new DummyProvider(); - provider.onCreate(); - ShadowContentResolver.registerProvider( - AUTHORITY_ONE, provider - ); + public void testPerformIndexing_isfullIndex() { + SearchIndexableRaw rawData = getFakeRaw(); + PreIndexData data = getPreIndexData(rawData); + doReturn(data).when(mManager).getIndexDataFromProviders(anyList(), anyBoolean()); + doReturn(true).when(mManager).isFullIndex(any(Context.class), anyString(), anyString(), + anyString()); - // Test that Indexables are added for Full indexing - when(mPackageManager.queryIntentContentProviders(any(Intent.class), anyInt())) - .thenReturn(getDummyResolveInfo()); + mManager.performIndexing(); - DatabaseIndexingManager manager = - spy(new DatabaseIndexingManager(mContext, PACKAGE_ONE)); - - manager.performIndexing(); - - verify(manager).updateDatabase(true /* isFullIndex */, Locale.getDefault().toString()); + verify(mManager).updateDatabase(data, true /* isFullIndex */, + Locale.getDefault().toString()); } @Test - public void testPerformIndexing_onPackageChange_shouldFullIndex() { + public void testPerformIndexing_onPackageChange_fullIndex() { final List providers = getDummyResolveInfo(); final String buildNumber = Build.FINGERPRINT; final String locale = Locale.getDefault().toString(); skipFullIndex(providers); // This snapshot is already indexed. Should return false - assertThat(IndexDatabaseHelper.isFullIndex( + assertThat(mManager.isFullIndex( mContext, locale, buildNumber, IndexDatabaseHelper.buildProviderVersionedNames(providers))) .isFalse(); @@ -869,65 +812,46 @@ public class DatabaseIndexingManagerTest { // Change provider version number, this should trigger full index. providers.get(0).providerInfo.applicationInfo.versionCode++; - assertThat(IndexDatabaseHelper.isFullIndex(mContext, locale, buildNumber, + assertThat(mManager.isFullIndex(mContext, locale, buildNumber, IndexDatabaseHelper.buildProviderVersionedNames(providers))) .isTrue(); } @Test public void testPerformIndexing_onOta_buildNumberIsCached() { - DummyProvider provider = new DummyProvider(); - provider.onCreate(); - ShadowContentResolver.registerProvider( - AUTHORITY_ONE, provider - ); + mManager.performIndexing(); - // Test that Indexables are added for Full indexing - when(mPackageManager.queryIntentContentProviders(any(Intent.class), anyInt())) - .thenReturn(getDummyResolveInfo()); - - DatabaseIndexingManager manager = - spy(new DatabaseIndexingManager(mContext, PACKAGE_ONE)); - - manager.performIndexing(); - - assertThat(IndexDatabaseHelper.getInstance(mContext).isBuildIndexed(mContext, - Build.FINGERPRINT)).isTrue(); + assertThat(IndexDatabaseHelper.isBuildIndexed(mContext, Build.FINGERPRINT)).isTrue(); } @Test public void testFullUpdatedDatabase_noData_addDataToDatabaseNotCalled() { - mManager.updateDatabase(true /* isFullIndex */, localeStr); - mManager.mDataToProcess.dataToUpdate.clear(); + PreIndexData emptydata = new PreIndexData(); + mManager.updateDatabase(emptydata, true /* isFullIndex */, localeStr); verify(mManager, times(0)).addDataToDatabase(any(SQLiteDatabase.class), anyString(), anyList(), anyMap()); } - @Test - public void testFullUpdatedDatabase_updatedDataInDatabaseNotCalled() { - mManager.updateDatabase(true /* isFullIndex */, localeStr); - verify(mManager, times(0)).updateDataInDatabase(any(SQLiteDatabase.class), anyMap()); - } - @Test public void testLocaleUpdated_afterIndexing_localeNotAdded() { - mManager.updateDatabase(true /* isFullIndex */, localeStr); - assertThat(IndexDatabaseHelper.getInstance(mContext) - .isLocaleAlreadyIndexed(mContext, localeStr)).isFalse(); + PreIndexData emptydata = new PreIndexData(); + mManager.updateDatabase(emptydata, true /* isFullIndex */, localeStr); + + assertThat(IndexDatabaseHelper.isLocaleAlreadyIndexed(mContext, localeStr)).isFalse(); } @Test public void testLocaleUpdated_afterFullIndexing_localeAdded() { mManager.performIndexing(); - assertThat(IndexDatabaseHelper.getInstance(mContext) - .isLocaleAlreadyIndexed(mContext, localeStr)).isTrue(); + assertThat(IndexDatabaseHelper.isLocaleAlreadyIndexed(mContext, localeStr)).isTrue(); } @Test public void testUpdateDatabase_newEligibleData_addedToDatabase() { // Test that addDataToDatabase is called when dataToUpdate is non-empty - mManager.mDataToProcess.dataToUpdate.add(getFakeRaw()); - mManager.updateDatabase(true /* isFullIndex */, localeStr); + PreIndexData indexData = new PreIndexData(); + indexData.dataToUpdate.add(getFakeRaw()); + mManager.updateDatabase(indexData, true /* isFullIndex */, localeStr); Cursor cursor = mDb.rawQuery("SELECT * FROM prefs_index", null); cursor.moveToPosition(0); @@ -1020,8 +944,8 @@ public class DatabaseIndexingManagerTest { @Test public void testEmptyNonIndexableKeys_emptyDataKeyResources_addedToDatabase() { insertSpecialCase(TITLE_ONE, true /* enabled */, null /* dataReferenceKey */); - - mManager.updateDatabase(false, localeStr); + PreIndexData emptydata = new PreIndexData(); + mManager.updateDatabase(emptydata, false /* needsReindexing */, localeStr); Cursor cursor = mDb.rawQuery("SELECT * FROM prefs_index WHERE enabled = 1", null); cursor.moveToPosition(0); @@ -1111,46 +1035,6 @@ public class DatabaseIndexingManagerTest { return niks; } - private List getDummyResolveInfo() { - List infoList = new ArrayList<>(); - ResolveInfo info = new ResolveInfo(); - info.providerInfo = new ProviderInfo(); - info.providerInfo.exported = true; - info.providerInfo.authority = AUTHORITY_ONE; - info.providerInfo.packageName = PACKAGE_ONE; - info.providerInfo.applicationInfo = new ApplicationInfo(); - infoList.add(info); - - return infoList; - } - - // TODO move this method and its counterpart in CursorToSearchResultConverterTest into - // a util class with public fields to assert values. - private Cursor getDummyCursor() { - MatrixCursor cursor = new MatrixCursor(INDEXABLES_RAW_COLUMNS); - final String BLANK = ""; - - ArrayList item = - new ArrayList<>(INDEXABLES_RAW_COLUMNS.length); - item.add("42"); // Rank - item.add(TITLE_ONE); // Title - item.add(BLANK); // Summary on - item.add(BLANK); // summary off - item.add(BLANK); // entries - item.add(BLANK); // keywords - item.add(BLANK); // screen title - item.add(BLANK); // classname - item.add("123"); // Icon - item.add(BLANK); // Intent action - item.add(BLANK); // target package - item.add(BLANK); // target class - item.add(KEY_ONE); // Key - item.add("-1"); // userId - cursor.addRow(item); - - return cursor; - } - private void insertSpecialCase(String specialCase, boolean enabled, String key) { ContentValues values = new ContentValues(); values.put(IndexDatabaseHelper.IndexColumns.DOCID, specialCase.hashCode()); @@ -1179,43 +1063,22 @@ public class DatabaseIndexingManagerTest { mDb.replaceOrThrow(IndexDatabaseHelper.Tables.TABLE_PREFS_INDEX, null, values); } - private class DummyProvider extends ContentProvider { + private PreIndexData getPreIndexData(SearchIndexableData fakeData) { + PreIndexData data = new PreIndexData(); + data.dataToUpdate.add(fakeData); + return data; + } - @Override - public boolean onCreate() { - return false; - } + private List getDummyResolveInfo() { + List infoList = new ArrayList<>(); + ResolveInfo info = new ResolveInfo(); + info.providerInfo = new ProviderInfo(); + info.providerInfo.exported = true; + info.providerInfo.authority = AUTHORITY_ONE; + info.providerInfo.packageName = PACKAGE_ONE; + info.providerInfo.applicationInfo = new ApplicationInfo(); + infoList.add(info); - @Override - public Cursor query(@NonNull Uri uri, @Nullable String[] projection, - @Nullable String selection, @Nullable String[] selectionArgs, - @Nullable String sortOrder) { - if (uri.toString().contains("xml")) { - return null; - } - return getDummyCursor(); - } - - @Override - public String getType(@NonNull Uri uri) { - return null; - } - - @Override - public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) { - return null; - } - - @Override - public int delete(@NonNull Uri uri, @Nullable String selection, - @Nullable String[] selectionArgs) { - return 0; - } - - @Override - public int update(@NonNull Uri uri, @Nullable ContentValues values, - @Nullable String selection, @Nullable String[] selectionArgs) { - return 0; - } + return infoList; } } diff --git a/tests/robotests/src/com/android/settings/search/indexing/IndexableDataCollectorTest.java b/tests/robotests/src/com/android/settings/search/indexing/IndexableDataCollectorTest.java new file mode 100644 index 00000000000..0f1f34524d4 --- /dev/null +++ b/tests/robotests/src/com/android/settings/search/indexing/IndexableDataCollectorTest.java @@ -0,0 +1,170 @@ +/* + * 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.search.indexing; + +import android.content.ContentResolver; +import android.content.Context; + +import android.content.pm.ApplicationInfo; +import android.content.pm.ProviderInfo; +import android.content.pm.ResolveInfo; +import android.net.Uri; +import android.provider.SearchIndexableResource; +import com.android.settings.TestConfig; +import com.android.settings.search.SearchIndexableRaw; +import com.android.settings.testutils.SettingsRobolectricTestRunner; +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 java.util.ArrayList; +import java.util.List; + +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.spy; + +@RunWith(SettingsRobolectricTestRunner.class) +@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION) +public class IndexableDataCollectorTest { + + private final String AUTHORITY_ONE = "authority"; + private final String PACKAGE_ONE = "com.android.settings"; + + @Mock + ContentResolver mResolver; + + Context mContext; + + IndexableDataCollector mDataCollector; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mContext = spy(RuntimeEnvironment.application); + doReturn(mResolver).when(mContext).getContentResolver(); + //doReturn(mPackageManager).when(mContext).getPackageManager(); + + mDataCollector = spy(new IndexableDataCollector(mContext)); + } + + @Test + public void testCollectIndexableData_addsResourceData() { + final List providerInfo = getDummyResolveInfo(); + doReturn(true).when(mDataCollector).isWellKnownProvider(any(ResolveInfo.class)); + + List resources = getFakeResource(); + doReturn(resources).when(mDataCollector).getIndexablesForXmlResourceUri( + any(Context.class), anyString(), any(Uri.class), any(String[].class)); + + PreIndexData data = mDataCollector.collectIndexableData(providerInfo, + true /* isFullIndex */); + + assertThat(data.dataToUpdate).containsAllIn(resources); + } + + @Test + public void testCollectIndexableData_addsRawData() { + final List providerInfo = getDummyResolveInfo(); + doReturn(true).when(mDataCollector).isWellKnownProvider(any(ResolveInfo.class)); + + List rawData = getFakeRaw(); + doReturn(rawData).when(mDataCollector).getIndexablesForRawDataUri(any(Context.class), + anyString(), any(Uri.class), any(String[].class)); + + + PreIndexData data = mDataCollector.collectIndexableData(providerInfo, + true /* isFullIndex */); + + assertThat(data.dataToUpdate).containsAllIn(rawData); + } + + @Test + public void testCollectIndexableData_addsNonIndexables() { + final List providerInfo = getDummyResolveInfo(); + doReturn(true).when(mDataCollector).isWellKnownProvider(any(ResolveInfo.class)); + + List niks = getFakeNonIndexables(); + + doReturn(niks).when(mDataCollector).getNonIndexablesKeysFromRemoteProvider(anyString(), + anyString()); + + PreIndexData data = mDataCollector.collectIndexableData(providerInfo, + true /* isFullIndex */); + + assertThat(data.nonIndexableKeys.get(AUTHORITY_ONE)).containsAllIn(niks); + } + + private List getDummyResolveInfo() { + List infoList = new ArrayList<>(); + ResolveInfo info = new ResolveInfo(); + info.providerInfo = new ProviderInfo(); + info.providerInfo.exported = true; + info.providerInfo.authority = AUTHORITY_ONE; + info.providerInfo.packageName = PACKAGE_ONE; + info.providerInfo.applicationInfo = new ApplicationInfo(); + infoList.add(info); + + return infoList; + } + + private List getFakeResource() { + List resources = new ArrayList<>(); + final String BLANK = ""; + + SearchIndexableResource sir = new SearchIndexableResource(mContext); + sir.rank = 0; + sir.xmlResId = 0; + sir.className = BLANK; + sir.packageName = BLANK; + sir.iconResId = 0; + sir.intentAction = BLANK; + sir.intentTargetPackage = BLANK; + sir.intentTargetClass = BLANK; + sir.enabled = true; + resources.add(sir); + + return resources; + } + + private List getFakeRaw() { + List rawData = new ArrayList<>(); + + SearchIndexableRaw data = new SearchIndexableRaw(mContext); + data.title = "bront"; + data.key = "brint"; + rawData.add(data); + + return rawData; + } + + private List getFakeNonIndexables() { + List niks = new ArrayList<>(); + niks.add("they're"); + niks.add("good"); + niks.add("dogs"); + niks.add("brent"); + return niks; + } +} From 3ed509cb3513fee5077e672d13d12346e6465c0e Mon Sep 17 00:00:00 2001 From: Fan Zhang Date: Thu, 7 Sep 2017 10:48:45 -0700 Subject: [PATCH 09/17] Some code cleanup - Formatting - Remove unused code Test: robotests Change-Id: If259c7c3893df96744f3c0ad62a378ef6ce01de7 --- .../settings/dashboard/DashboardAdapter.java | 79 +++++++------------ .../settings/dashboard/DashboardData.java | 28 +------ 2 files changed, 32 insertions(+), 75 deletions(-) diff --git a/src/com/android/settings/dashboard/DashboardAdapter.java b/src/com/android/settings/dashboard/DashboardAdapter.java index c06a58fe24c..fff70023371 100644 --- a/src/com/android/settings/dashboard/DashboardAdapter.java +++ b/src/com/android/settings/dashboard/DashboardAdapter.java @@ -94,19 +94,6 @@ public class DashboardAdapter extends RecyclerView.Adapter conditions, SuggestionParser suggestionParser, SuggestionDismissController.Callback callback) { @@ -129,7 +116,7 @@ public class DashboardAdapter extends RecyclerView.Adapter 0 && data.conditionCount > 0 - && curMode != DashboardData.HEADER_MODE_SUGGESTION_EXPANDED - ? DashboardData.HEADER_MODE_SUGGESTION_EXPANDED - : DashboardData.HEADER_MODE_FULLY_EXPANDED; + && curMode != DashboardData.HEADER_MODE_SUGGESTION_EXPANDED + ? DashboardData.HEADER_MODE_SUGGESTION_EXPANDED + : DashboardData.HEADER_MODE_FULLY_EXPANDED; final boolean moreSuggestions = data.hiddenSuggestionCount > 0; final boolean hasConditions = data.conditionCount > 0; if (data.conditionCount > 0) { @@ -377,22 +364,22 @@ public class DashboardAdapter extends RecyclerView.Adapter 0) { holder.summary.setText(mContext.getResources().getQuantityString( - R.plurals.suggestions_collapsed_summary, - data.hiddenSuggestionCount, data.hiddenSuggestionCount)); + R.plurals.suggestions_collapsed_summary, + data.hiddenSuggestionCount, data.hiddenSuggestionCount)); } else { holder.title.setText(mContext.getResources().getQuantityString( - R.plurals.suggestions_collapsed_title, - data.hiddenSuggestionCount, data.hiddenSuggestionCount)); + R.plurals.suggestions_collapsed_title, + data.hiddenSuggestionCount, data.hiddenSuggestionCount)); holder.title.setTextColor(Color.BLACK); holder.summary.setText(null); } } else if (curMode == DashboardData.HEADER_MODE_DEFAULT) { if (data.conditionCount > 0) { holder.summary.setText(mContext.getString( - R.string.suggestions_summary, data.hiddenSuggestionCount)); + R.string.suggestions_summary, data.hiddenSuggestionCount)); } else { holder.title.setText(mContext.getString( - R.string.suggestions_more_title, data.hiddenSuggestionCount)); + R.string.suggestions_more_title, data.hiddenSuggestionCount)); holder.title.setTextColor(Color.BLACK); holder.summary.setText(null); } @@ -400,7 +387,7 @@ public class DashboardAdapter extends RecyclerView.Adapter 1) { holder.summary.setTextColor(Utils.getColorAccent(mContext)); holder.summary.setText( - mContext.getString(R.string.condition_summary, data.conditionCount)); + mContext.getString(R.string.condition_summary, data.conditionCount)); } else { holder.summary.setText(null); } @@ -413,16 +400,16 @@ public class DashboardAdapter extends RecyclerView.Adapter { - if (moreSuggestions ) { + if (moreSuggestions) { logSuggestions(); } else if (hasConditions) { mMetricsFeatureProvider.action(mContext, - MetricsEvent.ACTION_SETTINGS_CONDITION_EXPAND, true); + MetricsEvent.ACTION_SETTINGS_CONDITION_EXPAND, true); } DashboardData prevData = mDashboardData; final boolean wasCollapsed = curMode == DashboardData.HEADER_MODE_COLLAPSED; mDashboardData = new DashboardData.Builder(prevData) - .setSuggestionConditionMode(nextMode).build(); + .setSuggestionConditionMode(nextMode).build(); notifyDashboardDataChanged(prevData); if (wasCollapsed) { mRecyclerView.scrollToPosition(SUGGESTION_CONDITION_HEADER_POSITION); @@ -439,13 +426,13 @@ public class DashboardAdapter extends RecyclerView.Adapter 0) { mSuggestionAdapter = new SuggestionAdapter(mContext, (List) - mDashboardData.getItemEntityByPosition(position), mSuggestionsShownLogged); + mDashboardData.getItemEntityByPosition(position), mSuggestionsShownLogged); mSuggestionDismissHandler = new SuggestionDismissController(mContext, - holder.data, mSuggestionParser, mCallback); + holder.data, mSuggestionParser, mCallback); holder.data.setAdapter(mSuggestionAdapter); } else { ConditionAdapter adapter = new ConditionAdapter(mContext, - (List) mDashboardData.getItemEntityByPosition(position), + (List) mDashboardData.getItemEntityByPosition(position), mDashboardData.getSuggestionConditionMode()); adapter.addDismissHandling(holder.data); holder.data.setAdapter(adapter); @@ -454,19 +441,13 @@ public class DashboardAdapter extends RecyclerView.Adapter icons, ViewGroup parent) { @@ -519,7 +500,7 @@ public class DashboardAdapter extends RecyclerView.Adapter result = new ArrayList(); + List result = new ArrayList<>(); final int size = conditions == null ? 0 : conditions.size(); for (int i = 0; i < size; i++) { final Condition condition = conditions.get(i); @@ -482,7 +458,7 @@ public class DashboardData { conditionCount = sizeOf(conditions); this.hiddenSuggestionCount = hiddenSuggestionCount; title = conditionCount > 0 ? conditions.get(0).getTitle() : null; - conditionIcons = new ArrayList(); + conditionIcons = new ArrayList<>(); for (int i = 0; conditions != null && i < conditions.size(); i++) { final Condition condition = conditions.get(i); conditionIcons.add(condition.getIcon()); From 5b017f7b055d2d501e7aec537e5ad01d28c4fb0b Mon Sep 17 00:00:00 2001 From: Jack He Date: Wed, 30 Aug 2017 19:18:40 -0700 Subject: [PATCH 10/17] Bluetooth: add metrics for pairing with devices without names * Also caches context in onClick() method Bug: 34685932 Test: make, unit test Change-Id: I99beab2c85b8e48c4bc41f69146759d4b7c62428 --- .../bluetooth/BluetoothDevicePreference.java | 13 +++++++++---- .../BluetoothDevicePreferenceTest.java | 19 +++++++++++++++++++ 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/src/com/android/settings/bluetooth/BluetoothDevicePreference.java b/src/com/android/settings/bluetooth/BluetoothDevicePreference.java index 084b50ebb96..94ba478c5b3 100644 --- a/src/com/android/settings/bluetooth/BluetoothDevicePreference.java +++ b/src/com/android/settings/bluetooth/BluetoothDevicePreference.java @@ -188,22 +188,27 @@ public final class BluetoothDevicePreference extends GearPreference implements } void onClicked() { + Context context = getContext(); int bondState = mCachedDevice.getBondState(); final MetricsFeatureProvider metricsFeatureProvider = - FeatureFactory.getFactory(getContext()).getMetricsFeatureProvider(); + FeatureFactory.getFactory(context).getMetricsFeatureProvider(); if (mCachedDevice.isConnected()) { - metricsFeatureProvider.action(getContext(), + metricsFeatureProvider.action(context, MetricsEvent.ACTION_SETTINGS_BLUETOOTH_DISCONNECT); askDisconnect(); } else if (bondState == BluetoothDevice.BOND_BONDED) { - metricsFeatureProvider.action(getContext(), + metricsFeatureProvider.action(context, MetricsEvent.ACTION_SETTINGS_BLUETOOTH_CONNECT); mCachedDevice.connect(true); } else if (bondState == BluetoothDevice.BOND_NONE) { - metricsFeatureProvider.action(getContext(), + metricsFeatureProvider.action(context, MetricsEvent.ACTION_SETTINGS_BLUETOOTH_PAIR); + if (!mCachedDevice.hasHumanReadableName()) { + metricsFeatureProvider.action(context, + MetricsEvent.ACTION_SETTINGS_BLUETOOTH_PAIR_DEVICES_WITHOUT_NAMES); + } pair(); } } diff --git a/tests/robotests/src/com/android/settings/bluetooth/BluetoothDevicePreferenceTest.java b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDevicePreferenceTest.java index a1db5de77e8..ac0720a246f 100644 --- a/tests/robotests/src/com/android/settings/bluetooth/BluetoothDevicePreferenceTest.java +++ b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDevicePreferenceTest.java @@ -42,6 +42,7 @@ import static com.google.common.truth.Truth.assertThat; import static org.mockito.Mockito.doAnswer; 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.when; @@ -98,11 +99,29 @@ public class BluetoothDevicePreferenceTest { when(mCachedBluetoothDevice.isConnected()).thenReturn(false); when(mCachedBluetoothDevice.getBondState()).thenReturn(BluetoothDevice.BOND_NONE); when(mCachedBluetoothDevice.startPairing()).thenReturn(true); + when(mCachedBluetoothDevice.hasHumanReadableName()).thenReturn(true); mPreference.onClicked(); verify(mMetricsFeatureProvider).action( mContext, MetricsEvent.ACTION_SETTINGS_BLUETOOTH_PAIR); + verify(mMetricsFeatureProvider, never()).action(mContext, + MetricsEvent.ACTION_SETTINGS_BLUETOOTH_PAIR_DEVICES_WITHOUT_NAMES); + } + + @Test + public void onClicked_deviceNotBonded_shouldLogBluetoothPairEventAndPairWithoutNameEvent() { + when(mCachedBluetoothDevice.isConnected()).thenReturn(false); + when(mCachedBluetoothDevice.getBondState()).thenReturn(BluetoothDevice.BOND_NONE); + when(mCachedBluetoothDevice.startPairing()).thenReturn(true); + when(mCachedBluetoothDevice.hasHumanReadableName()).thenReturn(false); + + mPreference.onClicked(); + + verify(mMetricsFeatureProvider).action( + mContext, MetricsEvent.ACTION_SETTINGS_BLUETOOTH_PAIR); + verify(mMetricsFeatureProvider).action(mContext, + MetricsEvent.ACTION_SETTINGS_BLUETOOTH_PAIR_DEVICES_WITHOUT_NAMES); } @Test From 5b1651b3bbd2306029687f890238eec50616240b Mon Sep 17 00:00:00 2001 From: debesay guadad Date: Fri, 26 Feb 2016 14:49:27 +0800 Subject: [PATCH 11/17] Add ims registration status Display "IMS registration state" in Status menu. Introduce carrier config to enable/disable the feature for customization. Since some carriers require, this feature is necessary. Test: manual Checked "IMS registration state" in Status menu Bug: 28806101 Merged-In: I6c452c512f03cf41704b91331e44141ed3050cf9 Change-Id: I6c452c512f03cf41704b91331e44141ed3050cf9 --- res/values/strings.xml | 7 +++++ res/xml/device_info_status.xml | 6 ++++ .../android/settings/deviceinfo/Status.java | 28 +++++++++++++++++++ 3 files changed, 41 insertions(+) diff --git a/res/values/strings.xml b/res/values/strings.xml index 93f7a623f4f..e775fb38142 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -9050,4 +9050,11 @@ This feature is not available on this device + + + "IMS registration state" + + "Registered" + + "Not registered" diff --git a/res/xml/device_info_status.xml b/res/xml/device_info_status.xml index 9a57af987bf..6fe43ec87b0 100644 --- a/res/xml/device_info_status.xml +++ b/res/xml/device_info_status.xml @@ -89,4 +89,10 @@ android:title="@string/status_wimax_mac_address" android:summary="@string/device_info_not_available" android:persistent="false" /> + diff --git a/src/com/android/settings/deviceinfo/Status.java b/src/com/android/settings/deviceinfo/Status.java index baddc6cac2b..faa4134a935 100644 --- a/src/com/android/settings/deviceinfo/Status.java +++ b/src/com/android/settings/deviceinfo/Status.java @@ -28,11 +28,15 @@ import android.net.wifi.WifiManager; import android.os.Bundle; import android.os.Handler; import android.os.Message; +import android.os.PersistableBundle; import android.os.SystemClock; import android.os.SystemProperties; import android.os.UserManager; import android.support.v7.preference.Preference; import android.support.v7.preference.PreferenceScreen; +import android.telephony.CarrierConfigManager; +import android.telephony.SubscriptionManager; +import android.telephony.TelephonyManager; import android.text.TextUtils; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; @@ -56,6 +60,7 @@ public class Status extends SettingsPreferenceFragment { private static final String KEY_WIMAX_MAC_ADDRESS = "wimax_mac_address"; private static final String KEY_SIM_STATUS = "sim_status"; private static final String KEY_IMEI_INFO = "imei_info"; + private static final String KEY_IMS_REGISTRATION_STATE = "ims_reg_state"; // Broadcasts to listen to for connectivity changes. private static final String[] CONNECTIVITY_INTENTS = { @@ -85,6 +90,8 @@ public class Status extends SettingsPreferenceFragment { private Preference mIpAddress; private Preference mWifiMacAddress; private Preference mWimaxMacAddress; + private Preference mImsStatus; + private Handler mHandler; private static class MyHandler extends Handler { @@ -162,6 +169,7 @@ public class Status extends SettingsPreferenceFragment { mWifiMacAddress = findPreference(KEY_WIFI_MAC_ADDRESS); mWimaxMacAddress = findPreference(KEY_WIMAX_MAC_ADDRESS); mIpAddress = findPreference(KEY_IP_ADDRESS); + mImsStatus = findPreference(KEY_IMS_REGISTRATION_STATE); mRes = getResources(); mUnavailable = mRes.getString(R.string.status_unavailable); @@ -269,11 +277,31 @@ public class Status extends SettingsPreferenceFragment { } } + private void setImsRegistrationStatus() { + CarrierConfigManager configManager = (CarrierConfigManager) + getSystemService(Context.CARRIER_CONFIG_SERVICE); + int subId = SubscriptionManager.getDefaultDataSubscriptionId(); + PersistableBundle config = null; + if (configManager != null) { + config = configManager.getConfigForSubId(subId); + } + if (config != null && config.getBoolean( + CarrierConfigManager.KEY_SHOW_IMS_REGISTRATION_STATUS_BOOL)) { + TelephonyManager tm = (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE); + mImsStatus.setSummary((tm != null && tm.isImsRegistered(subId)) ? + R.string.ims_reg_status_registered : R.string.ims_reg_status_not_registered); + } else { + removePreferenceFromScreen(KEY_IMS_REGISTRATION_STATE); + mImsStatus = null; + } + } + void updateConnectivity() { setWimaxStatus(); setWifiStatus(); setBtStatus(); setIpAddressStatus(); + setImsRegistrationStatus(); } void updateTimes() { From 1ca6b253215949df1d561a1c9bcc1e265aa7c295 Mon Sep 17 00:00:00 2001 From: Fan Zhang Date: Thu, 7 Sep 2017 16:06:24 -0700 Subject: [PATCH 12/17] Don't attempt to tint icon if icon is not available Change-Id: I0aa2a17d51d966dce182381c7bba8913ad997738 Fixes: 65460834 Test: robotests --- .../android/settings/dashboard/DashboardFragment.java | 3 +++ .../settings/dashboard/DashboardFragmentTest.java | 11 +++++++++++ 2 files changed, 14 insertions(+) diff --git a/src/com/android/settings/dashboard/DashboardFragment.java b/src/com/android/settings/dashboard/DashboardFragment.java index e8970ecd697..8c3ac0d0267 100644 --- a/src/com/android/settings/dashboard/DashboardFragment.java +++ b/src/com/android/settings/dashboard/DashboardFragment.java @@ -230,6 +230,9 @@ public abstract class DashboardFragment extends SettingsPreferenceFragment @VisibleForTesting boolean tintTileIcon(Tile tile) { + if (tile.icon == null) { + return false; + } // First check if the tile has set the icon tintable metadata. final Bundle metadata = tile.metaData; if (metadata != null diff --git a/tests/robotests/src/com/android/settings/dashboard/DashboardFragmentTest.java b/tests/robotests/src/com/android/settings/dashboard/DashboardFragmentTest.java index 2687714a959..ea160dd355f 100644 --- a/tests/robotests/src/com/android/settings/dashboard/DashboardFragmentTest.java +++ b/tests/robotests/src/com/android/settings/dashboard/DashboardFragmentTest.java @@ -27,6 +27,7 @@ import static org.mockito.Mockito.when; import android.content.ComponentName; import android.content.Context; import android.content.Intent; +import android.graphics.drawable.Icon; import android.os.Bundle; import android.support.v7.preference.Preference; import android.support.v7.preference.PreferenceManager; @@ -158,6 +159,7 @@ public class DashboardFragmentTest { @Test public void tintTileIcon_hasMetadata_shouldReturnIconTintableMetadata() { final Tile tile = new Tile(); + tile.icon = mock(Icon.class); final Bundle metaData = new Bundle(); tile.metaData = metaData; @@ -168,10 +170,19 @@ public class DashboardFragmentTest { assertThat(mTestFragment.tintTileIcon(tile)).isTrue(); } + @Test + public void tintTileIcon_noIcon_shouldReturnFalse() { + final Tile tile = new Tile(); + final Bundle metaData = new Bundle(); + tile.metaData = metaData; + + assertThat(mTestFragment.tintTileIcon(tile)).isFalse(); + } @Test public void tintTileIcon_noMetadata_shouldReturnPackageNameCheck() { final Tile tile = new Tile(); + tile.icon = mock(Icon.class); final Intent intent = new Intent(); tile.intent = intent; From 0aa45bed65f544e68e0d38f255da005b3ce9fddf Mon Sep 17 00:00:00 2001 From: Andrew Scull Date: Wed, 6 Sep 2017 17:53:25 +0100 Subject: [PATCH 13/17] Policy transparency when DISALLOW_FACTORY_RESET prevents OEM unlock. If an admin has set the DISALLOW_FACTORY_RESET user restriction, OEM unlock is also restricted. If this is the case, it should be explained to the user that the admin is preventing the OEM unlock. DISALLOW_OEM_UNLOCK is deprecated and now managed by OemLockManager. This restriction is also one set by the carrier, not an admin. Test: RunSettingsRoboTests Test: CTS verifier test from the bug Bug: 65124732 Change-Id: I8bde87a522742a7cbda006eee17c2a19797b1835 --- .../development/DevelopmentSettings.java | 20 +++++++++++++------ .../service/oemlock/OemLockManager.java | 3 --- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/com/android/settings/development/DevelopmentSettings.java b/src/com/android/settings/development/DevelopmentSettings.java index 4df72e6ce2f..b32b1419ab3 100644 --- a/src/com/android/settings/development/DevelopmentSettings.java +++ b/src/com/android/settings/development/DevelopmentSettings.java @@ -53,6 +53,7 @@ import android.os.RemoteException; import android.os.ServiceManager; import android.os.StrictMode; import android.os.SystemProperties; +import android.os.UserHandle; import android.os.UserManager; import android.os.storage.IStorageManager; import android.provider.SearchIndexableResource; @@ -1025,8 +1026,19 @@ public class DevelopmentSettings extends RestrictedSettingsFragment return context.getSystemService(Context.OEM_LOCK_SERVICE) != null; } + /** + * Returns whether OEM unlock is allowed by the user and carrier. + * + * This does not take into account any restrictions imposed by the device policy. + */ + private boolean isOemUnlockAllowedByUserAndCarrier() { + final UserHandle userHandle = UserHandle.of(UserHandle.myUserId()); + return mOemLockManager.isOemUnlockAllowedByCarrier() + && !mUm.hasBaseUserRestriction(UserManager.DISALLOW_FACTORY_RESET, userHandle); + } + private boolean enableOemUnlockPreference() { - return !isBootloaderUnlocked() && mOemLockManager.canUserAllowOemUnlock(); + return !isBootloaderUnlocked() && isOemUnlockAllowedByUserAndCarrier(); } private void updateOemUnlockOptions() { @@ -1040,10 +1052,6 @@ public class DevelopmentSettings extends RestrictedSettingsFragment // Check restriction, disable mEnableOemUnlock and apply policy transparency. mEnableOemUnlock.checkRestrictionAndSetDisabled(UserManager.DISALLOW_FACTORY_RESET); } - if (mEnableOemUnlock.isEnabled()) { - // Check restriction, disable mEnableOemUnlock and apply policy transparency. - mEnableOemUnlock.checkRestrictionAndSetDisabled(UserManager.DISALLOW_OEM_UNLOCK); - } } } @@ -2568,7 +2576,7 @@ public class DevelopmentSettings extends RestrictedSettingsFragment oemUnlockSummary = R.string.oem_unlock_enable_disabled_summary_bootloader_unlocked; } else if (isSimLockedDevice()) { oemUnlockSummary = R.string.oem_unlock_enable_disabled_summary_sim_locked_device; - } else if (!mOemLockManager.canUserAllowOemUnlock()) { + } else if (!isOemUnlockAllowedByUserAndCarrier()) { // If the device isn't SIM-locked but OEM unlock is disallowed by some party, this // means either some other carrier restriction is in place or the device hasn't been // able to confirm which restrictions (SIM-lock or otherwise) apply. diff --git a/tests/robotests/src/android/service/oemlock/OemLockManager.java b/tests/robotests/src/android/service/oemlock/OemLockManager.java index 7c015cf5051..c168089be4b 100644 --- a/tests/robotests/src/android/service/oemlock/OemLockManager.java +++ b/tests/robotests/src/android/service/oemlock/OemLockManager.java @@ -32,9 +32,6 @@ public class OemLockManager { return false; } - public boolean canUserAllowOemUnlock() { - return true; - } public boolean isOemUnlockAllowed() { return false; } From 2ca3017f8e2a98de85212e3057e9bc21c8c9f0f4 Mon Sep 17 00:00:00 2001 From: Andrew Scull Date: Wed, 6 Sep 2017 17:53:25 +0100 Subject: [PATCH 14/17] Policy transparency when DISALLOW_FACTORY_RESET prevents OEM unlock. If an admin has set the DISALLOW_FACTORY_RESET user restriction, OEM unlock is also restricted. If this is the case, it should be explained to the user that the admin is preventing the OEM unlock. DISALLOW_OEM_UNLOCK is deprecated and now managed by OemLockManager. This restriction is also one set by the carrier, not an admin. Test: RunSettingsRoboTests Test: CTS verifier test from the bug Bug: 65124732 Change-Id: I8bde87a522742a7cbda006eee17c2a19797b1835 (cherry picked from commit 0aa45bed65f544e68e0d38f255da005b3ce9fddf) --- .../development/DevelopmentSettings.java | 20 +++++++++++++------ .../service/oemlock/OemLockManager.java | 3 --- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/com/android/settings/development/DevelopmentSettings.java b/src/com/android/settings/development/DevelopmentSettings.java index 8477a3312d9..137b697314c 100644 --- a/src/com/android/settings/development/DevelopmentSettings.java +++ b/src/com/android/settings/development/DevelopmentSettings.java @@ -54,6 +54,7 @@ import android.os.RemoteException; import android.os.ServiceManager; import android.os.StrictMode; import android.os.SystemProperties; +import android.os.UserHandle; import android.os.UserManager; import android.os.storage.IStorageManager; import android.provider.SearchIndexableResource; @@ -1054,8 +1055,19 @@ public class DevelopmentSettings extends RestrictedSettingsFragment return context.getSystemService(Context.OEM_LOCK_SERVICE) != null; } + /** + * Returns whether OEM unlock is allowed by the user and carrier. + * + * This does not take into account any restrictions imposed by the device policy. + */ + private boolean isOemUnlockAllowedByUserAndCarrier() { + final UserHandle userHandle = UserHandle.of(UserHandle.myUserId()); + return mOemLockManager.isOemUnlockAllowedByCarrier() + && !mUm.hasBaseUserRestriction(UserManager.DISALLOW_FACTORY_RESET, userHandle); + } + private boolean enableOemUnlockPreference() { - return !isBootloaderUnlocked() && mOemLockManager.canUserAllowOemUnlock(); + return !isBootloaderUnlocked() && isOemUnlockAllowedByUserAndCarrier(); } private void updateOemUnlockOptions() { @@ -1069,10 +1081,6 @@ public class DevelopmentSettings extends RestrictedSettingsFragment // Check restriction, disable mEnableOemUnlock and apply policy transparency. mEnableOemUnlock.checkRestrictionAndSetDisabled(UserManager.DISALLOW_FACTORY_RESET); } - if (mEnableOemUnlock.isEnabled()) { - // Check restriction, disable mEnableOemUnlock and apply policy transparency. - mEnableOemUnlock.checkRestrictionAndSetDisabled(UserManager.DISALLOW_OEM_UNLOCK); - } } } @@ -2834,7 +2842,7 @@ public class DevelopmentSettings extends RestrictedSettingsFragment oemUnlockSummary = R.string.oem_unlock_enable_disabled_summary_bootloader_unlocked; } else if (isSimLockedDevice()) { oemUnlockSummary = R.string.oem_unlock_enable_disabled_summary_sim_locked_device; - } else if (!mOemLockManager.canUserAllowOemUnlock()) { + } else if (!isOemUnlockAllowedByUserAndCarrier()) { // If the device isn't SIM-locked but OEM unlock is disallowed by some party, this // means either some other carrier restriction is in place or the device hasn't been // able to confirm which restrictions (SIM-lock or otherwise) apply. diff --git a/tests/robotests/src/android/service/oemlock/OemLockManager.java b/tests/robotests/src/android/service/oemlock/OemLockManager.java index 7c015cf5051..c168089be4b 100644 --- a/tests/robotests/src/android/service/oemlock/OemLockManager.java +++ b/tests/robotests/src/android/service/oemlock/OemLockManager.java @@ -32,9 +32,6 @@ public class OemLockManager { return false; } - public boolean canUserAllowOemUnlock() { - return true; - } public boolean isOemUnlockAllowed() { return false; } From 59c8d77c8c5ee62fabd962db2555b5324caa51df Mon Sep 17 00:00:00 2001 From: Daniel Nishi Date: Fri, 1 Sep 2017 15:58:30 -0700 Subject: [PATCH 15/17] Re-add the block of text explaining the feature. When the automatic storage manager is off (or has never been run), we display a block of text that explains what it does. In order to make this testable, I've implemented the feature in a PreferenceController and upgraded the existing AutomaticStorageManagerSettings class to be closer to the newer Settings IA. Change-Id: I3f7d20347a6d5a7bae8bffcd2014c3fdcd315b90 Fixes: 63082545 Test: Settings Robotest --- ...anagerDescriptionPreferenceController.java | 76 +++++++++++++++ .../AutomaticStorageManagerSettings.java | 93 ++++++++++--------- .../search/SearchIndexableResources.java | 5 + .../grandfather_not_implementing_indexable | 1 - ...erDescriptionPreferenceControllerTest.java | 83 +++++++++++++++++ 5 files changed, 215 insertions(+), 43 deletions(-) create mode 100644 src/com/android/settings/deletionhelper/AutomaticStorageManagerDescriptionPreferenceController.java create mode 100644 tests/robotests/src/com/android/settings/deletionhelper/AutomaticStorageManagerDescriptionPreferenceControllerTest.java diff --git a/src/com/android/settings/deletionhelper/AutomaticStorageManagerDescriptionPreferenceController.java b/src/com/android/settings/deletionhelper/AutomaticStorageManagerDescriptionPreferenceController.java new file mode 100644 index 00000000000..261f66cea6e --- /dev/null +++ b/src/com/android/settings/deletionhelper/AutomaticStorageManagerDescriptionPreferenceController.java @@ -0,0 +1,76 @@ +/** + * 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.deletionhelper; + +import android.content.ContentResolver; +import android.content.Context; +import android.provider.Settings; +import android.support.v7.preference.Preference; +import android.support.v7.preference.PreferenceScreen; +import android.text.format.DateUtils; +import android.text.format.Formatter; + +import com.android.settings.R; +import com.android.settings.core.PreferenceControllerMixin; +import com.android.settingslib.core.AbstractPreferenceController; + +/** + * Handles the wall of text which appears below the options in the Storage Management settings drill + * down. + */ +public class AutomaticStorageManagerDescriptionPreferenceController + extends AbstractPreferenceController implements PreferenceControllerMixin { + private static final String KEY_FREED = "freed_bytes"; + + public AutomaticStorageManagerDescriptionPreferenceController(Context context) { + super(context); + } + + @Override + public boolean isAvailable() { + return true; + } + + @Override + public String getPreferenceKey() { + return KEY_FREED; + } + + @Override + public void displayPreference(PreferenceScreen screen) { + Preference preference = screen.findPreference(getPreferenceKey()); + final Context context = preference.getContext(); + ContentResolver cr = context.getContentResolver(); + long freedBytes = + Settings.Secure.getLong( + cr, Settings.Secure.AUTOMATIC_STORAGE_MANAGER_BYTES_CLEARED, 0); + long lastRunMillis = + Settings.Secure.getLong(cr, Settings.Secure.AUTOMATIC_STORAGE_MANAGER_LAST_RUN, 0); + if (freedBytes == 0 || lastRunMillis == 0 || !isStorageManagerEnabled(cr)) { + preference.setSummary(R.string.automatic_storage_manager_text); + } else { + preference.setSummary( + context.getString( + R.string.automatic_storage_manager_freed_bytes, + Formatter.formatFileSize(context, freedBytes), + DateUtils.formatDateTime( + context, lastRunMillis, DateUtils.FORMAT_SHOW_DATE))); + } + } + + private boolean isStorageManagerEnabled(ContentResolver cr) { + return Settings.Secure.getInt(cr, Settings.Secure.AUTOMATIC_STORAGE_MANAGER_ENABLED, 0) + != 0; + } +} diff --git a/src/com/android/settings/deletionhelper/AutomaticStorageManagerSettings.java b/src/com/android/settings/deletionhelper/AutomaticStorageManagerSettings.java index f7b62b1666f..e38317a824d 100644 --- a/src/com/android/settings/deletionhelper/AutomaticStorageManagerSettings.java +++ b/src/com/android/settings/deletionhelper/AutomaticStorageManagerSettings.java @@ -16,15 +16,13 @@ package com.android.settings.deletionhelper; -import android.app.Activity; import android.content.ContentResolver; +import android.content.Context; import android.os.Bundle; import android.provider.Settings; import android.support.v7.preference.DropDownPreference; import android.support.v7.preference.Preference; import android.support.v7.preference.Preference.OnPreferenceChangeListener; -import android.text.format.DateUtils; -import android.text.format.Formatter; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -32,39 +30,34 @@ import android.view.ViewGroup; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.settings.R; import com.android.settings.SettingsActivity; -import com.android.settings.SettingsPreferenceFragment; import com.android.settings.Utils; +import com.android.settings.dashboard.DashboardFragment; +import com.android.settings.search.BaseSearchIndexProvider; +import com.android.settings.search.Indexable; import com.android.settings.widget.SwitchBar; +import com.android.settingslib.core.AbstractPreferenceController; + +import java.util.ArrayList; +import java.util.List; /** * AutomaticStorageManagerSettings is the Settings screen for configuration and management of the * automatic storage manager. */ -public class AutomaticStorageManagerSettings extends SettingsPreferenceFragment +public class AutomaticStorageManagerSettings extends DashboardFragment implements OnPreferenceChangeListener { private static final String KEY_DAYS = "days"; - private static final String KEY_FREED = "freed_bytes"; - private static final String STORAGE_MANAGER_ENABLED_BY_DEFAULT_PROPERTY = - "ro.storage_manager.enabled"; private AutomaticStorageManagerSwitchBarController mSwitchController; private DropDownPreference mDaysToRetain; - private Preference mFreedBytes; private SwitchBar mSwitchBar; - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - addPreferencesFromResource(R.xml.automatic_storage_management_settings); - } - @Override public View onCreateView( LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = super.onCreateView(inflater, container, savedInstanceState); initializeDaysToRetainPreference(); - initializeFreedBytesPreference(); initializeSwitchBar(); return view; @@ -98,35 +91,25 @@ public class AutomaticStorageManagerSettings extends SettingsPreferenceFragment getFragmentManager()); } - private void initializeFreedBytesPreference() { - ContentResolver cr = getContentResolver(); - mFreedBytes = findPreference(KEY_FREED); - long freedBytes = Settings.Secure.getLong(cr, - Settings.Secure.AUTOMATIC_STORAGE_MANAGER_BYTES_CLEARED, - 0); - long lastRunMillis = Settings.Secure.getLong(cr, - Settings.Secure.AUTOMATIC_STORAGE_MANAGER_LAST_RUN, - 0); - if (freedBytes == 0 || lastRunMillis == 0) { - mFreedBytes.setVisible(false); - } else { - final Activity activity = getActivity(); - mFreedBytes.setSummary( - activity.getString( - R.string.automatic_storage_manager_freed_bytes, - Formatter.formatFileSize(activity, freedBytes), - DateUtils.formatDateTime( - activity, lastRunMillis, DateUtils.FORMAT_SHOW_DATE))); - } - } - @Override public void onResume() { super.onResume(); - boolean isStorageManagerChecked = - Settings.Secure.getInt(getContentResolver(), - Settings.Secure.AUTOMATIC_STORAGE_MANAGER_ENABLED, 0) != 0; - mDaysToRetain.setEnabled(isStorageManagerChecked); + mDaysToRetain.setEnabled(isStorageManagerEnabled()); + } + + @Override + protected String getLogTag() { + return null; + } + + @Override + protected int getPreferenceScreenResId() { + return R.xml.automatic_storage_management_settings; + } + + @Override + protected List getPreferenceControllers(Context context) { + return buildPreferenceControllers(context); } @Override @@ -168,4 +151,30 @@ public class AutomaticStorageManagerSettings extends SettingsPreferenceFragment return indices.length - 1; } + private boolean isStorageManagerEnabled() { + return Settings.Secure.getInt( + getContentResolver(), Settings.Secure.AUTOMATIC_STORAGE_MANAGER_ENABLED, 0) + != 0; + } + + private static List buildPreferenceControllers(Context context) { + final List controllers = new ArrayList<>(); + controllers.add(new AutomaticStorageManagerDescriptionPreferenceController(context)); + return controllers; + } + + /** For Search. */ + public static final Indexable.SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = + new BaseSearchIndexProvider() { + @Override + protected boolean isPageSearchEnabled(Context context) { + return false; + } + + @Override + public List getPreferenceControllers( + Context context) { + return buildPreferenceControllers(context); + } + }; } diff --git a/src/com/android/settings/search/SearchIndexableResources.java b/src/com/android/settings/search/SearchIndexableResources.java index c20d2a03007..32098130784 100644 --- a/src/com/android/settings/search/SearchIndexableResources.java +++ b/src/com/android/settings/search/SearchIndexableResources.java @@ -44,6 +44,7 @@ import com.android.settings.connecteddevice.ConnectedDeviceDashboardFragment; import com.android.settings.datausage.DataPlanUsageSummary; import com.android.settings.datausage.DataUsageMeteredSettings; import com.android.settings.datausage.DataUsageSummary; +import com.android.settings.deletionhelper.AutomaticStorageManagerSettings; import com.android.settings.development.DevelopmentSettings; import com.android.settings.deviceinfo.StorageDashboardFragment; import com.android.settings.deviceinfo.StorageSettings; @@ -211,6 +212,10 @@ public final class SearchIndexableResources { R.drawable.ic_settings_notifications); addIndex(DreamSettings.class, NO_DATA_RES_ID, R.drawable.ic_settings_display); addIndex(SupportDashboardActivity.class, NO_DATA_RES_ID, R.drawable.ic_help); + addIndex( + AutomaticStorageManagerSettings.class, + NO_DATA_RES_ID, + R.drawable.ic_settings_storage); } private SearchIndexableResources() { diff --git a/tests/robotests/assets/grandfather_not_implementing_indexable b/tests/robotests/assets/grandfather_not_implementing_indexable index be5a99b2c65..08fe16a2e24 100644 --- a/tests/robotests/assets/grandfather_not_implementing_indexable +++ b/tests/robotests/assets/grandfather_not_implementing_indexable @@ -64,7 +64,6 @@ com.android.settings.notification.AppNotificationSettings com.android.settings.deviceinfo.PrivateVolumeSettings com.android.settings.users.AppRestrictionsFragment com.android.settings.deviceinfo.PrivateVolumeUnmount -com.android.settings.deletionhelper.AutomaticStorageManagerSettings com.android.settings.notification.ZenAccessSettings com.android.settings.accessibility.ToggleFontSizePreferenceFragment com.android.settings.applications.PremiumSmsAccess diff --git a/tests/robotests/src/com/android/settings/deletionhelper/AutomaticStorageManagerDescriptionPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/deletionhelper/AutomaticStorageManagerDescriptionPreferenceControllerTest.java new file mode 100644 index 00000000000..dd438ffd77b --- /dev/null +++ b/tests/robotests/src/com/android/settings/deletionhelper/AutomaticStorageManagerDescriptionPreferenceControllerTest.java @@ -0,0 +1,83 @@ +package com.android.settings.deletionhelper; + +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.provider.Settings; +import android.support.v7.preference.Preference; +import android.support.v7.preference.PreferenceScreen; + +import com.android.settings.R; +import com.android.settings.TestConfig; +import com.android.settings.testutils.SettingsRobolectricTestRunner; + +import org.junit.Before; +import org.junit.Ignore; +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; + +@RunWith(SettingsRobolectricTestRunner.class) +@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION) +public class AutomaticStorageManagerDescriptionPreferenceControllerTest { + @Mock private PreferenceScreen mScreen; + @Mock private Preference mPreference; + private AutomaticStorageManagerDescriptionPreferenceController mController; + private Context mContext = RuntimeEnvironment.application; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mController = new AutomaticStorageManagerDescriptionPreferenceController(mContext); + when(mScreen.findPreference(mController.getPreferenceKey())).thenReturn(mPreference); + when(mPreference.getKey()).thenReturn(mController.getPreferenceKey()); + when(mPreference.getContext()).thenReturn(mContext); + } + + @Test + public void displayPreference_asmDisabled_shouldHaveDescription() { + mController.displayPreference(mScreen); + + verify(mPreference).setSummary(eq(R.string.automatic_storage_manager_text)); + } + + @Test + public void displayPreference_asmEnabledButUnused_shouldHaveDescription() { + Settings.Secure.putInt( + mContext.getContentResolver(), + Settings.Secure.AUTOMATIC_STORAGE_MANAGER_ENABLED, + 1); + + mController.displayPreference(mScreen); + + verify(mPreference).setSummary(eq(R.string.automatic_storage_manager_text)); + } + + @Ignore("Robolectric doesn't do locale switching for date localization -- yet.") + @Test + @Config(qualifiers = "en") + public void displayPreference_asmEnabledAndUsed_shouldHaveDescriptionFilledOut() { + Settings.Secure.putInt( + mContext.getContentResolver(), + Settings.Secure.AUTOMATIC_STORAGE_MANAGER_ENABLED, + 1); + Settings.Secure.putLong( + mContext.getContentResolver(), + Settings.Secure.AUTOMATIC_STORAGE_MANAGER_BYTES_CLEARED, + 10); + Settings.Secure.putLong( + mContext.getContentResolver(), + Settings.Secure.AUTOMATIC_STORAGE_MANAGER_LAST_RUN, + 43200000); // January 1, 1970 12:00:00 PM to avoid timezone issues. + + mController.displayPreference(mScreen); + + verify(mPreference) + .setSummary(eq("10.00B total made available\n\nLast ran on January 1, 1970")); + } +} From f2063cff30bfff1b8e08e4ff8bced9c20b6224ee Mon Sep 17 00:00:00 2001 From: Maurice Lam Date: Wed, 6 Sep 2017 20:52:22 -0700 Subject: [PATCH 16/17] Don't default to PIN on non FBE devices. DO NOT MERGE So that EncryptionInterstitial is shown as part of the flow Test: cd tests/robotests && mma Bug: 65192141 Change-Id: I13e8b9059aae39cef2a8509f9f0eee1cd0808220 --- .../SetupFingerprintEnrollIntroduction.java | 14 ++- .../password/StorageManagerWrapper.java | 29 +++++ ...etupFingerprintEnrollIntroductionTest.java | 55 ++++++++++ .../shadow/ShadowFingerprintManager.java | 102 ++++++++++++++++++ 4 files changed, 195 insertions(+), 5 deletions(-) create mode 100644 src/com/android/settings/password/StorageManagerWrapper.java create mode 100644 tests/robotests/src/com/android/settings/testutils/shadow/ShadowFingerprintManager.java diff --git a/src/com/android/settings/fingerprint/SetupFingerprintEnrollIntroduction.java b/src/com/android/settings/fingerprint/SetupFingerprintEnrollIntroduction.java index 59907cf493b..5656a27d540 100644 --- a/src/com/android/settings/fingerprint/SetupFingerprintEnrollIntroduction.java +++ b/src/com/android/settings/fingerprint/SetupFingerprintEnrollIntroduction.java @@ -32,6 +32,7 @@ import com.android.settings.SetupWizardUtils; import com.android.settings.password.ChooseLockGeneric.ChooseLockGenericFragment; import com.android.settings.password.SetupChooseLockGeneric; import com.android.settings.password.SetupSkipDialog; +import com.android.settings.password.StorageManagerWrapper; public class SetupFingerprintEnrollIntroduction extends FingerprintEnrollIntroduction { private static final String KEY_LOCK_SCREEN_PRESENT = "wasLockScreenPresent"; @@ -56,11 +57,14 @@ public class SetupFingerprintEnrollIntroduction extends FingerprintEnrollIntrodu @Override protected Intent getChooseLockIntent() { - Intent intent = new Intent(this, SetupChooseLockGeneric.class) - .putExtra( - LockPatternUtils.PASSWORD_TYPE_KEY, - DevicePolicyManager.PASSWORD_QUALITY_NUMERIC); - intent.putExtra(ChooseLockGenericFragment.EXTRA_SHOW_OPTIONS_BUTTON, true); + Intent intent = new Intent(this, SetupChooseLockGeneric.class); + + if (StorageManagerWrapper.isFileEncryptedNativeOrEmulated()) { + intent.putExtra( + LockPatternUtils.PASSWORD_TYPE_KEY, + DevicePolicyManager.PASSWORD_QUALITY_NUMERIC); + intent.putExtra(ChooseLockGenericFragment.EXTRA_SHOW_OPTIONS_BUTTON, true); + } SetupWizardUtils.copySetupExtras(getIntent(), intent); return intent; } diff --git a/src/com/android/settings/password/StorageManagerWrapper.java b/src/com/android/settings/password/StorageManagerWrapper.java new file mode 100644 index 00000000000..5adfaf2e51c --- /dev/null +++ b/src/com/android/settings/password/StorageManagerWrapper.java @@ -0,0 +1,29 @@ +/* + * 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.password; + +import android.os.storage.StorageManager; + +/** + * Wrapper class to allow Robolectric to shadow methods introduced in newer API + */ +public class StorageManagerWrapper { + + public static boolean isFileEncryptedNativeOrEmulated() { + return StorageManager.isFileEncryptedNativeOrEmulated(); + } +} diff --git a/tests/robotests/src/com/android/settings/fingerprint/SetupFingerprintEnrollIntroductionTest.java b/tests/robotests/src/com/android/settings/fingerprint/SetupFingerprintEnrollIntroductionTest.java index 801ee5dbfb2..f5859acf52d 100644 --- a/tests/robotests/src/com/android/settings/fingerprint/SetupFingerprintEnrollIntroductionTest.java +++ b/tests/robotests/src/com/android/settings/fingerprint/SetupFingerprintEnrollIntroductionTest.java @@ -23,28 +23,38 @@ import static org.robolectric.RuntimeEnvironment.application; import android.app.KeyguardManager; import android.content.Intent; +import android.content.pm.PackageManager; import android.content.pm.UserInfo; import android.view.View; import android.widget.Button; import com.android.settings.R; import com.android.settings.TestConfig; +import com.android.settings.fingerprint.SetupFingerprintEnrollIntroductionTest + .ShadowStorageManagerWrapper; import com.android.settings.password.SetupChooseLockGeneric.SetupChooseLockGenericFragment; import com.android.settings.password.SetupSkipDialog; +import com.android.settings.password.StorageManagerWrapper; import com.android.settings.testutils.SettingsRobolectricTestRunner; import com.android.settings.testutils.shadow.ShadowEventLogWriter; +import com.android.settings.testutils.shadow.ShadowFingerprintManager; import com.android.settings.testutils.shadow.ShadowLockPatternUtils; import com.android.settings.testutils.shadow.ShadowUserManager; +import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.robolectric.Robolectric; +import org.robolectric.RuntimeEnvironment; import org.robolectric.Shadows; import org.robolectric.annotation.Config; +import org.robolectric.annotation.Implementation; +import org.robolectric.annotation.Implements; import org.robolectric.shadows.ShadowActivity; +import org.robolectric.shadows.ShadowActivity.IntentForResult; import org.robolectric.shadows.ShadowKeyguardManager; import org.robolectric.util.ActivityController; @@ -54,7 +64,9 @@ import org.robolectric.util.ActivityController; sdk = TestConfig.SDK_VERSION, shadows = { ShadowEventLogWriter.class, + ShadowFingerprintManager.class, ShadowLockPatternUtils.class, + ShadowStorageManagerWrapper.class, ShadowUserManager.class }) public class SetupFingerprintEnrollIntroductionTest { @@ -68,12 +80,22 @@ public class SetupFingerprintEnrollIntroductionTest { public void setUp() { MockitoAnnotations.initMocks(this); + RuntimeEnvironment.getRobolectricPackageManager() + .setSystemFeature(PackageManager.FEATURE_FINGERPRINT, true); + ShadowFingerprintManager.addToServiceMap(); + final Intent intent = new Intent(); mController = Robolectric.buildActivity(SetupFingerprintEnrollIntroduction.class, intent); ShadowUserManager.getShadow().setUserInfo(0, mUserInfo); } + @After + public void tearDown() { + ShadowStorageManagerWrapper.reset(); + ShadowFingerprintManager.reset(); + } + @Test public void testKeyguardNotSecure_shouldFinishWithSetupSkipDialogResultSkip() { getShadowKeyguardManager().setIsKeyguardSecure(false); @@ -188,8 +210,41 @@ public class SetupFingerprintEnrollIntroductionTest { assertThat(Shadows.shadowOf(activity).getResultIntent()).isNull(); } + @Test + public void testLockPattern() { + ShadowStorageManagerWrapper.sIsFileEncrypted = false; + + mController.create().postCreate(null).resume(); + + SetupFingerprintEnrollIntroduction activity = mController.get(); + + final Button nextButton = activity.findViewById(R.id.fingerprint_next_button); + nextButton.performClick(); + + ShadowActivity shadowActivity = Shadows.shadowOf(activity); + IntentForResult startedActivity = shadowActivity.getNextStartedActivityForResult(); + assertThat(startedActivity).isNotNull(); + assertThat(startedActivity.intent.hasExtra( + SetupChooseLockGenericFragment.EXTRA_PASSWORD_QUALITY)).isFalse(); + } + private ShadowKeyguardManager getShadowKeyguardManager() { return Shadows.shadowOf(application.getSystemService(KeyguardManager.class)); } + + @Implements(StorageManagerWrapper.class) + public static class ShadowStorageManagerWrapper { + + private static boolean sIsFileEncrypted = true; + + public static void reset() { + sIsFileEncrypted = true; + } + + @Implementation + public static boolean isFileEncryptedNativeOrEmulated() { + return sIsFileEncrypted; + } + } } diff --git a/tests/robotests/src/com/android/settings/testutils/shadow/ShadowFingerprintManager.java b/tests/robotests/src/com/android/settings/testutils/shadow/ShadowFingerprintManager.java new file mode 100644 index 00000000000..b84cf42ed14 --- /dev/null +++ b/tests/robotests/src/com/android/settings/testutils/shadow/ShadowFingerprintManager.java @@ -0,0 +1,102 @@ +/* + * 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.hardware.fingerprint.Fingerprint; +import android.hardware.fingerprint.FingerprintManager; +import android.support.annotation.NonNull; + +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Implementation; +import org.robolectric.annotation.Implements; +import org.robolectric.annotation.Resetter; +import org.robolectric.internal.ShadowExtractor; +import org.robolectric.shadows.ShadowContextImpl; +import org.robolectric.util.ReflectionHelpers; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.stream.IntStream; + +@Implements(FingerprintManager.class) +public class ShadowFingerprintManager { + + private static Map getSystemServiceMap() { + return ReflectionHelpers.getStaticField(ShadowContextImpl.class, "SYSTEM_SERVICE_MAP"); + } + + /** + * Call this in @Before of a test to add FingerprintManager to Robolectric's system service + * map. Otherwise getSystemService(FINGERPRINT_SERVICE) will return null. + */ + public static void addToServiceMap() { + getSystemServiceMap().put(Context.FINGERPRINT_SERVICE, FingerprintManager.class.getName()); + } + + @Resetter + public static void reset() { + getSystemServiceMap().remove(Context.FINGERPRINT_SERVICE); + } + + public boolean hardwareDetected = true; + + @NonNull + private List mFingerprints = Collections.emptyList(); + + @Implementation + public boolean isHardwareDetected() { + return hardwareDetected; + } + + @Implementation + public boolean hasEnrolledFingerprints() { + return !mFingerprints.isEmpty(); + } + + @Implementation + public List getEnrolledFingerprints() { + return mFingerprints; + } + + @Implementation + public List getEnrolledFingerprints(int userId) { + return mFingerprints; + } + + public void setEnrolledFingerprints(Fingerprint... fingerprints) { + mFingerprints = Arrays.asList(fingerprints); + } + + public void setDefaultFingerprints(int num) { + setEnrolledFingerprints( + IntStream.range(0, num) + .mapToObj(i -> new Fingerprint( + "Fingerprint " + i, + 0, /* groupId */ + i, /* fingerId */ + 0 /* deviceId */)) + .toArray(Fingerprint[]::new)); + } + + public static ShadowFingerprintManager get() { + return (ShadowFingerprintManager) ShadowExtractor.extract( + RuntimeEnvironment.application.getSystemService(FingerprintManager.class)); + } +} From 563af1cda94bcaadee75250d7e2f339604f2bffc Mon Sep 17 00:00:00 2001 From: Maurice Lam Date: Fri, 8 Sep 2017 13:08:03 -0700 Subject: [PATCH 17/17] Add FakeFeatureFactory.setupForTest In AssisGestureSettingsTest, FakeFeatureFactory was used but was missing a setupForTest call. Test: cd tests/robotests && \ ROBOTEST_FILTER=AssistGestureSettingsTest mma Change-Id: Ie78232d854aee00c7969a3d2dc148354be7de484 --- .../android/settings/gestures/AssistGestureSettingsTest.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/robotests/src/com/android/settings/gestures/AssistGestureSettingsTest.java b/tests/robotests/src/com/android/settings/gestures/AssistGestureSettingsTest.java index 1f5c9be1739..7ea42b8d07b 100644 --- a/tests/robotests/src/com/android/settings/gestures/AssistGestureSettingsTest.java +++ b/tests/robotests/src/com/android/settings/gestures/AssistGestureSettingsTest.java @@ -32,6 +32,7 @@ import com.android.settingslib.core.AbstractPreferenceController; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.Answers; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.robolectric.RuntimeEnvironment; @@ -43,7 +44,7 @@ import java.util.List; @RunWith(SettingsRobolectricTestRunner.class) @Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION) public class AssistGestureSettingsTest { - @Mock + @Mock(answer = Answers.RETURNS_DEEP_STUBS) private Context mContext; private FakeFeatureFactory mFakeFeatureFactory; private AssistGestureSettings mSettings; @@ -51,6 +52,7 @@ public class AssistGestureSettingsTest { @Before public void setUp() { MockitoAnnotations.initMocks(this); + FakeFeatureFactory.setupForTest(mContext); mFakeFeatureFactory = (FakeFeatureFactory) FakeFeatureFactory.getFactory(mContext); mSettings = new AssistGestureSettings(); }