From c9615611e154b428911a5733ea93f5a32026414f Mon Sep 17 00:00:00 2001 From: Jason Chiu Date: Fri, 6 May 2022 11:14:54 +0800 Subject: [PATCH] Reduce flickers of Injection The injection dynamic data was loaded in the background and then post to main thread to update UI. However, it usually updates after Fragement.onResume(), which causes the flicker. To make it more smooth, DashboardFragment to wait for the dynamic data observers to update UI for a short period, which eliminates the flicker in most cases. Also skip the repeated tiles refresh called by onCategoriesChanged in onResume after all preferences refreshed. Test: robotest, visual Bug: 229177114 Change-Id: I04650af9692703f1fc1e6e5ad2090f051b1eeb81 --- .../android/settings/core/CategoryMixin.java | 7 +++ .../DashboardFeatureProviderImpl.java | 21 ++++----- .../settings/dashboard/DashboardFragment.java | 47 +++++++++++++++---- .../dashboard/DynamicDataObserver.java | 33 +++++++++++++ .../slices/BluetoothDevicesSlice.java | 2 +- .../DashboardFeatureProviderImplTest.java | 13 ++++- .../testutils/shadow/ShadowTileUtils.java | 7 +-- 7 files changed, 103 insertions(+), 27 deletions(-) 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