diff --git a/src/com/android/settings/core/CategoryMixin.java b/src/com/android/settings/core/CategoryMixin.java index 8d0a412a60c..151ed7b47f2 100644 --- a/src/com/android/settings/core/CategoryMixin.java +++ b/src/com/android/settings/core/CategoryMixin.java @@ -58,6 +58,7 @@ public class CategoryMixin implements LifecycleObserver { private final PackageReceiver mPackageReceiver = new PackageReceiver(); private final List mCategoryListeners = new ArrayList<>(); private int mCategoriesUpdateTaskCount; + private boolean mFirstOnResume = true; public CategoryMixin(Context context) { mContext = context; @@ -75,6 +76,12 @@ public class CategoryMixin implements LifecycleObserver { filter.addDataScheme(DATA_SCHEME_PKG); mContext.registerReceiver(mPackageReceiver, filter); + if (mFirstOnResume) { + // Skip since all tiles have been refreshed in DashboardFragment.onCreatePreferences(). + Log.d(TAG, "Skip categories update"); + mFirstOnResume = false; + return; + } updateCategories(); } diff --git a/src/com/android/settings/dashboard/DashboardFeatureProviderImpl.java b/src/com/android/settings/dashboard/DashboardFeatureProviderImpl.java index 8ad66d23e4f..2ae20570e9c 100644 --- a/src/com/android/settings/dashboard/DashboardFeatureProviderImpl.java +++ b/src/com/android/settings/dashboard/DashboardFeatureProviderImpl.java @@ -235,13 +235,13 @@ public class DashboardFeatureProviderImpl implements DashboardFeatureProvider { public void onDataChanged() { switch (method) { case METHOD_GET_DYNAMIC_TITLE: - refreshTitle(uri, pref); + refreshTitle(uri, pref, this); break; case METHOD_GET_DYNAMIC_SUMMARY: - refreshSummary(uri, pref); + refreshSummary(uri, pref, this); break; case METHOD_IS_CHECKED: - refreshSwitch(uri, pref); + refreshSwitch(uri, pref, this); break; } } @@ -262,19 +262,18 @@ public class DashboardFeatureProviderImpl implements DashboardFeatureProvider { final Uri uri = TileUtils.getCompleteUri(tile, META_DATA_PREFERENCE_TITLE_URI, METHOD_GET_DYNAMIC_TITLE); - refreshTitle(uri, preference); return createDynamicDataObserver(METHOD_GET_DYNAMIC_TITLE, uri, preference); } return null; } - private void refreshTitle(Uri uri, Preference preference) { + private void refreshTitle(Uri uri, Preference preference, DynamicDataObserver observer) { ThreadUtils.postOnBackgroundThread(() -> { final Map providerMap = new ArrayMap<>(); final String titleFromUri = TileUtils.getTextFromUri( mContext, uri, providerMap, META_DATA_PREFERENCE_TITLE); if (!TextUtils.equals(titleFromUri, preference.getTitle())) { - ThreadUtils.postOnMainThread(() -> preference.setTitle(titleFromUri)); + observer.post(() -> preference.setTitle(titleFromUri)); } }); } @@ -291,19 +290,18 @@ public class DashboardFeatureProviderImpl implements DashboardFeatureProvider { final Uri uri = TileUtils.getCompleteUri(tile, META_DATA_PREFERENCE_SUMMARY_URI, METHOD_GET_DYNAMIC_SUMMARY); - refreshSummary(uri, preference); return createDynamicDataObserver(METHOD_GET_DYNAMIC_SUMMARY, uri, preference); } return null; } - private void refreshSummary(Uri uri, Preference preference) { + private void refreshSummary(Uri uri, Preference preference, DynamicDataObserver observer) { ThreadUtils.postOnBackgroundThread(() -> { final Map providerMap = new ArrayMap<>(); final String summaryFromUri = TileUtils.getTextFromUri( mContext, uri, providerMap, META_DATA_PREFERENCE_SUMMARY); if (!TextUtils.equals(summaryFromUri, preference.getSummary())) { - ThreadUtils.postOnMainThread(() -> preference.setSummary(summaryFromUri)); + observer.post(() -> preference.setSummary(summaryFromUri)); } }); } @@ -323,7 +321,6 @@ public class DashboardFeatureProviderImpl implements DashboardFeatureProvider { final Uri isCheckedUri = TileUtils.getCompleteUri(tile, META_DATA_PREFERENCE_SWITCH_URI, METHOD_IS_CHECKED); setSwitchEnabled(preference, false); - refreshSwitch(isCheckedUri, preference); return createDynamicDataObserver(METHOD_IS_CHECKED, isCheckedUri, preference); } @@ -350,12 +347,12 @@ public class DashboardFeatureProviderImpl implements DashboardFeatureProvider { }); } - private void refreshSwitch(Uri uri, Preference preference) { + private void refreshSwitch(Uri uri, Preference preference, DynamicDataObserver observer) { ThreadUtils.postOnBackgroundThread(() -> { final Map providerMap = new ArrayMap<>(); final boolean checked = TileUtils.getBooleanFromUri(mContext, uri, providerMap, EXTRA_SWITCH_CHECKED_STATE); - ThreadUtils.postOnMainThread(() -> { + observer.post(() -> { setSwitchChecked(preference, checked); setSwitchEnabled(preference, true); }); diff --git a/src/com/android/settings/dashboard/DashboardFragment.java b/src/com/android/settings/dashboard/DashboardFragment.java index 378d55e1bc9..fb0a09d47a7 100644 --- a/src/com/android/settings/dashboard/DashboardFragment.java +++ b/src/com/android/settings/dashboard/DashboardFragment.java @@ -16,7 +16,6 @@ package com.android.settings.dashboard; import android.app.Activity; -import android.app.admin.DevicePolicyManager; import android.app.settings.SettingsEnums; import android.content.ContentResolver; import android.content.Context; @@ -57,6 +56,8 @@ import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; /** * Base fragment for dashboard style UI containing a list of static and dynamic setting items. @@ -66,6 +67,7 @@ public abstract class DashboardFragment extends SettingsPreferenceFragment BasePreferenceController.UiBlockListener { public static final String CATEGORY = "category"; private static final String TAG = "DashboardFragment"; + private static final long TIMEOUT_MILLIS = 50L; @VisibleForTesting final ArrayMap> mDashboardTilePrefKeys = new ArrayMap<>(); @@ -461,8 +463,9 @@ public abstract class DashboardFragment extends SettingsPreferenceFragment // Create a list to track which tiles are to be removed. final Map> remove = new ArrayMap(mDashboardTilePrefKeys); - // Install dashboard tiles. + // Install dashboard tiles and collect pending observers. final boolean forceRoundedIcons = shouldForceRoundedIcon(); + final List pendingObservers = new ArrayList<>(); for (Tile tile : tiles) { final String key = mDashboardFeatureProvider.getDashboardKeyForTile(tile); if (TextUtils.isEmpty(key)) { @@ -472,26 +475,30 @@ public abstract class DashboardFragment extends SettingsPreferenceFragment if (!displayTile(tile)) { continue; } + final List observers; if (mDashboardTilePrefKeys.containsKey(key)) { // Have the key already, will rebind. final Preference preference = screen.findPreference(key); - mDashboardFeatureProvider.bindPreferenceToTileAndGetObservers(getActivity(), this, - forceRoundedIcons, preference, tile, key, + observers = mDashboardFeatureProvider.bindPreferenceToTileAndGetObservers( + getActivity(), this, forceRoundedIcons, preference, tile, key, mPlaceholderPreferenceController.getOrder()); } else { // Don't have this key, add it. final Preference pref = createPreference(tile); - final List observers = - mDashboardFeatureProvider.bindPreferenceToTileAndGetObservers(getActivity(), - this, forceRoundedIcons, pref, tile, key, - mPlaceholderPreferenceController.getOrder()); + observers = mDashboardFeatureProvider.bindPreferenceToTileAndGetObservers( + getActivity(), this, forceRoundedIcons, pref, tile, key, + mPlaceholderPreferenceController.getOrder()); screen.addPreference(pref); registerDynamicDataObservers(observers); mDashboardTilePrefKeys.put(key, observers); } + if (observers != null) { + pendingObservers.addAll(observers); + } remove.remove(key); } - // Finally remove tiles that are gone. + + // Remove tiles that are gone. for (Map.Entry> entry : remove.entrySet()) { final String key = entry.getKey(); mDashboardTilePrefKeys.remove(key); @@ -501,6 +508,20 @@ public abstract class DashboardFragment extends SettingsPreferenceFragment } unregisterDynamicDataObservers(entry.getValue()); } + + // Wait for pending observers to update UI. + if (!pendingObservers.isEmpty()) { + final CountDownLatch mainLatch = new CountDownLatch(1); + new Thread(() -> { + pendingObservers.forEach(observer -> + awaitObserverLatch(observer.getCountDownLatch())); + mainLatch.countDown(); + }).start(); + Log.d(tag, "Start waiting observers"); + awaitObserverLatch(mainLatch); + Log.d(tag, "Stop waiting observers"); + pendingObservers.forEach(DynamicDataObserver::updateUi); + } } @Override @@ -546,4 +567,12 @@ public abstract class DashboardFragment extends SettingsPreferenceFragment resolver.unregisterContentObserver(observer); }); } + + private void awaitObserverLatch(CountDownLatch latch) { + try { + latch.await(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + // Do nothing + } + } } diff --git a/src/com/android/settings/dashboard/DynamicDataObserver.java b/src/com/android/settings/dashboard/DynamicDataObserver.java index f5299be4168..41bc563e26e 100644 --- a/src/com/android/settings/dashboard/DynamicDataObserver.java +++ b/src/com/android/settings/dashboard/DynamicDataObserver.java @@ -20,13 +20,24 @@ import android.net.Uri; import android.os.Handler; import android.os.Looper; +import com.android.settingslib.utils.ThreadUtils; + +import java.util.concurrent.CountDownLatch; + /** * Observer for updating injected dynamic data. */ public abstract class DynamicDataObserver extends ContentObserver { + private Runnable mUpdateRunnable; + private CountDownLatch mCountDownLatch; + private boolean mUpdateDelegated; + protected DynamicDataObserver() { super(new Handler(Looper.getMainLooper())); + mCountDownLatch = new CountDownLatch(1); + // Load data for the first time + onDataChanged(); } /** Returns the uri of the callback. */ @@ -35,8 +46,30 @@ public abstract class DynamicDataObserver extends ContentObserver { /** Called when data changes. */ public abstract void onDataChanged(); + /** Calls the runnable to update UI */ + public synchronized void updateUi() { + mUpdateDelegated = true; + if (mUpdateRunnable != null) { + mUpdateRunnable.run(); + } + } + + /** Returns the count-down latch */ + public CountDownLatch getCountDownLatch() { + return mCountDownLatch; + } + @Override public void onChange(boolean selfChange) { onDataChanged(); } + + protected synchronized void post(Runnable runnable) { + if (mUpdateDelegated) { + ThreadUtils.postOnMainThread(runnable); + } else { + mUpdateRunnable = runnable; + mCountDownLatch.countDown(); + } + } } diff --git a/src/com/android/settings/homepage/contextualcards/slices/BluetoothDevicesSlice.java b/src/com/android/settings/homepage/contextualcards/slices/BluetoothDevicesSlice.java index 7e4730ca951..4e276c12c58 100644 --- a/src/com/android/settings/homepage/contextualcards/slices/BluetoothDevicesSlice.java +++ b/src/com/android/settings/homepage/contextualcards/slices/BluetoothDevicesSlice.java @@ -176,7 +176,7 @@ public class BluetoothDevicesSlice implements CustomSliceable { List getPairedBluetoothDevices() { final List bluetoothDeviceList = new ArrayList<>(); - // If Bluetooth is disable, skip getting the Bluetooth devices. + // If Bluetooth is disabled, skip getting the Bluetooth devices. if (!BluetoothAdapter.getDefaultAdapter().isEnabled()) { Log.i(TAG, "Cannot get Bluetooth devices, Bluetooth is disabled."); return bluetoothDeviceList; diff --git a/tests/robotests/src/com/android/settings/dashboard/DashboardFeatureProviderImplTest.java b/tests/robotests/src/com/android/settings/dashboard/DashboardFeatureProviderImplTest.java index e7c99c873b1..f2b0acddbed 100644 --- a/tests/robotests/src/com/android/settings/dashboard/DashboardFeatureProviderImplTest.java +++ b/tests/robotests/src/com/android/settings/dashboard/DashboardFeatureProviderImplTest.java @@ -308,8 +308,12 @@ public class DashboardFeatureProviderImplTest { mActivity, mFragment, mForceRoundedIcon, preference, tile, null /* key */, Preference.DEFAULT_ORDER); - assertThat(preference.getSummary()).isEqualTo(ShadowTileUtils.MOCK_SUMMARY); assertThat(observers.get(0).getUri().toString()).isEqualTo(uriString); + assertThat(preference.getSummary()).isNotEqualTo(ShadowTileUtils.MOCK_TEXT); + + observers.get(0).updateUi(); + + assertThat(preference.getSummary()).isEqualTo(ShadowTileUtils.MOCK_TEXT); } @Test @@ -324,8 +328,12 @@ public class DashboardFeatureProviderImplTest { mActivity, mFragment, mForceRoundedIcon, preference, tile, null /* key */, Preference.DEFAULT_ORDER); - assertThat(preference.getTitle()).isEqualTo(ShadowTileUtils.MOCK_SUMMARY); assertThat(observers.get(0).getUri().toString()).isEqualTo(uriString); + assertThat(preference.getTitle()).isNotEqualTo(ShadowTileUtils.MOCK_TEXT); + + observers.get(0).updateUi(); + + assertThat(preference.getTitle()).isEqualTo(ShadowTileUtils.MOCK_TEXT); } @Test @@ -379,6 +387,7 @@ public class DashboardFeatureProviderImplTest { final List observers = mImpl.bindPreferenceToTileAndGetObservers( mActivity, mFragment, mForceRoundedIcon, preference, tile, null /* key */, Preference.DEFAULT_ORDER); + observers.get(0).updateUi(); ShadowTileUtils.setProviderChecked(false); observers.get(0).onDataChanged(); diff --git a/tests/robotests/src/com/android/settings/testutils/shadow/ShadowTileUtils.java b/tests/robotests/src/com/android/settings/testutils/shadow/ShadowTileUtils.java index 45a9dd61677..d164cb99a81 100644 --- a/tests/robotests/src/com/android/settings/testutils/shadow/ShadowTileUtils.java +++ b/tests/robotests/src/com/android/settings/testutils/shadow/ShadowTileUtils.java @@ -34,7 +34,7 @@ import java.util.Map; @Implements(TileUtils.class) public class ShadowTileUtils { - public static final String MOCK_SUMMARY = "summary"; + public static final String MOCK_TEXT = "text"; private static boolean sChecked; private static Bundle sResult; @@ -42,13 +42,14 @@ public class ShadowTileUtils { @Implementation protected static String getTextFromUri(Context context, Uri uri, Map providerMap, String key) { - return MOCK_SUMMARY; + return MOCK_TEXT; } @Implementation protected static Pair getIconFromUri(Context context, String packageName, Uri uri, Map providerMap) { - return Pair.create(RuntimeEnvironment.application.getPackageName(), R.drawable.ic_settings_accent); + return Pair.create(RuntimeEnvironment.application.getPackageName(), + R.drawable.ic_settings_accent); } @Implementation