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
This commit is contained in:
Jason Chiu
2022-05-06 11:14:54 +08:00
parent 9a14f087cd
commit c9615611e1
7 changed files with 103 additions and 27 deletions

View File

@@ -58,6 +58,7 @@ public class CategoryMixin implements LifecycleObserver {
private final PackageReceiver mPackageReceiver = new PackageReceiver(); private final PackageReceiver mPackageReceiver = new PackageReceiver();
private final List<CategoryListener> mCategoryListeners = new ArrayList<>(); private final List<CategoryListener> mCategoryListeners = new ArrayList<>();
private int mCategoriesUpdateTaskCount; private int mCategoriesUpdateTaskCount;
private boolean mFirstOnResume = true;
public CategoryMixin(Context context) { public CategoryMixin(Context context) {
mContext = context; mContext = context;
@@ -75,6 +76,12 @@ public class CategoryMixin implements LifecycleObserver {
filter.addDataScheme(DATA_SCHEME_PKG); filter.addDataScheme(DATA_SCHEME_PKG);
mContext.registerReceiver(mPackageReceiver, filter); 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(); updateCategories();
} }

View File

@@ -235,13 +235,13 @@ public class DashboardFeatureProviderImpl implements DashboardFeatureProvider {
public void onDataChanged() { public void onDataChanged() {
switch (method) { switch (method) {
case METHOD_GET_DYNAMIC_TITLE: case METHOD_GET_DYNAMIC_TITLE:
refreshTitle(uri, pref); refreshTitle(uri, pref, this);
break; break;
case METHOD_GET_DYNAMIC_SUMMARY: case METHOD_GET_DYNAMIC_SUMMARY:
refreshSummary(uri, pref); refreshSummary(uri, pref, this);
break; break;
case METHOD_IS_CHECKED: case METHOD_IS_CHECKED:
refreshSwitch(uri, pref); refreshSwitch(uri, pref, this);
break; break;
} }
} }
@@ -262,19 +262,18 @@ public class DashboardFeatureProviderImpl implements DashboardFeatureProvider {
final Uri uri = TileUtils.getCompleteUri(tile, META_DATA_PREFERENCE_TITLE_URI, final Uri uri = TileUtils.getCompleteUri(tile, META_DATA_PREFERENCE_TITLE_URI,
METHOD_GET_DYNAMIC_TITLE); METHOD_GET_DYNAMIC_TITLE);
refreshTitle(uri, preference);
return createDynamicDataObserver(METHOD_GET_DYNAMIC_TITLE, uri, preference); return createDynamicDataObserver(METHOD_GET_DYNAMIC_TITLE, uri, preference);
} }
return null; return null;
} }
private void refreshTitle(Uri uri, Preference preference) { private void refreshTitle(Uri uri, Preference preference, DynamicDataObserver observer) {
ThreadUtils.postOnBackgroundThread(() -> { ThreadUtils.postOnBackgroundThread(() -> {
final Map<String, IContentProvider> providerMap = new ArrayMap<>(); final Map<String, IContentProvider> providerMap = new ArrayMap<>();
final String titleFromUri = TileUtils.getTextFromUri( final String titleFromUri = TileUtils.getTextFromUri(
mContext, uri, providerMap, META_DATA_PREFERENCE_TITLE); mContext, uri, providerMap, META_DATA_PREFERENCE_TITLE);
if (!TextUtils.equals(titleFromUri, preference.getTitle())) { 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, final Uri uri = TileUtils.getCompleteUri(tile, META_DATA_PREFERENCE_SUMMARY_URI,
METHOD_GET_DYNAMIC_SUMMARY); METHOD_GET_DYNAMIC_SUMMARY);
refreshSummary(uri, preference);
return createDynamicDataObserver(METHOD_GET_DYNAMIC_SUMMARY, uri, preference); return createDynamicDataObserver(METHOD_GET_DYNAMIC_SUMMARY, uri, preference);
} }
return null; return null;
} }
private void refreshSummary(Uri uri, Preference preference) { private void refreshSummary(Uri uri, Preference preference, DynamicDataObserver observer) {
ThreadUtils.postOnBackgroundThread(() -> { ThreadUtils.postOnBackgroundThread(() -> {
final Map<String, IContentProvider> providerMap = new ArrayMap<>(); final Map<String, IContentProvider> providerMap = new ArrayMap<>();
final String summaryFromUri = TileUtils.getTextFromUri( final String summaryFromUri = TileUtils.getTextFromUri(
mContext, uri, providerMap, META_DATA_PREFERENCE_SUMMARY); mContext, uri, providerMap, META_DATA_PREFERENCE_SUMMARY);
if (!TextUtils.equals(summaryFromUri, preference.getSummary())) { 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, final Uri isCheckedUri = TileUtils.getCompleteUri(tile, META_DATA_PREFERENCE_SWITCH_URI,
METHOD_IS_CHECKED); METHOD_IS_CHECKED);
setSwitchEnabled(preference, false); setSwitchEnabled(preference, false);
refreshSwitch(isCheckedUri, preference);
return createDynamicDataObserver(METHOD_IS_CHECKED, 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(() -> { ThreadUtils.postOnBackgroundThread(() -> {
final Map<String, IContentProvider> providerMap = new ArrayMap<>(); final Map<String, IContentProvider> providerMap = new ArrayMap<>();
final boolean checked = TileUtils.getBooleanFromUri(mContext, uri, providerMap, final boolean checked = TileUtils.getBooleanFromUri(mContext, uri, providerMap,
EXTRA_SWITCH_CHECKED_STATE); EXTRA_SWITCH_CHECKED_STATE);
ThreadUtils.postOnMainThread(() -> { observer.post(() -> {
setSwitchChecked(preference, checked); setSwitchChecked(preference, checked);
setSwitchEnabled(preference, true); setSwitchEnabled(preference, true);
}); });

View File

@@ -16,7 +16,6 @@
package com.android.settings.dashboard; package com.android.settings.dashboard;
import android.app.Activity; import android.app.Activity;
import android.app.admin.DevicePolicyManager;
import android.app.settings.SettingsEnums; import android.app.settings.SettingsEnums;
import android.content.ContentResolver; import android.content.ContentResolver;
import android.content.Context; import android.content.Context;
@@ -57,6 +56,8 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Set; 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. * 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 { BasePreferenceController.UiBlockListener {
public static final String CATEGORY = "category"; public static final String CATEGORY = "category";
private static final String TAG = "DashboardFragment"; private static final String TAG = "DashboardFragment";
private static final long TIMEOUT_MILLIS = 50L;
@VisibleForTesting @VisibleForTesting
final ArrayMap<String, List<DynamicDataObserver>> mDashboardTilePrefKeys = new ArrayMap<>(); final ArrayMap<String, List<DynamicDataObserver>> mDashboardTilePrefKeys = new ArrayMap<>();
@@ -461,8 +463,9 @@ public abstract class DashboardFragment extends SettingsPreferenceFragment
// Create a list to track which tiles are to be removed. // Create a list to track which tiles are to be removed.
final Map<String, List<DynamicDataObserver>> remove = new ArrayMap(mDashboardTilePrefKeys); final Map<String, List<DynamicDataObserver>> remove = new ArrayMap(mDashboardTilePrefKeys);
// Install dashboard tiles. // Install dashboard tiles and collect pending observers.
final boolean forceRoundedIcons = shouldForceRoundedIcon(); final boolean forceRoundedIcons = shouldForceRoundedIcon();
final List<DynamicDataObserver> pendingObservers = new ArrayList<>();
for (Tile tile : tiles) { for (Tile tile : tiles) {
final String key = mDashboardFeatureProvider.getDashboardKeyForTile(tile); final String key = mDashboardFeatureProvider.getDashboardKeyForTile(tile);
if (TextUtils.isEmpty(key)) { if (TextUtils.isEmpty(key)) {
@@ -472,26 +475,30 @@ public abstract class DashboardFragment extends SettingsPreferenceFragment
if (!displayTile(tile)) { if (!displayTile(tile)) {
continue; continue;
} }
final List<DynamicDataObserver> observers;
if (mDashboardTilePrefKeys.containsKey(key)) { if (mDashboardTilePrefKeys.containsKey(key)) {
// Have the key already, will rebind. // Have the key already, will rebind.
final Preference preference = screen.findPreference(key); final Preference preference = screen.findPreference(key);
mDashboardFeatureProvider.bindPreferenceToTileAndGetObservers(getActivity(), this, observers = mDashboardFeatureProvider.bindPreferenceToTileAndGetObservers(
forceRoundedIcons, preference, tile, key, getActivity(), this, forceRoundedIcons, preference, tile, key,
mPlaceholderPreferenceController.getOrder()); mPlaceholderPreferenceController.getOrder());
} else { } else {
// Don't have this key, add it. // Don't have this key, add it.
final Preference pref = createPreference(tile); final Preference pref = createPreference(tile);
final List<DynamicDataObserver> observers = observers = mDashboardFeatureProvider.bindPreferenceToTileAndGetObservers(
mDashboardFeatureProvider.bindPreferenceToTileAndGetObservers(getActivity(), getActivity(), this, forceRoundedIcons, pref, tile, key,
this, forceRoundedIcons, pref, tile, key, mPlaceholderPreferenceController.getOrder());
mPlaceholderPreferenceController.getOrder());
screen.addPreference(pref); screen.addPreference(pref);
registerDynamicDataObservers(observers); registerDynamicDataObservers(observers);
mDashboardTilePrefKeys.put(key, observers); mDashboardTilePrefKeys.put(key, observers);
} }
if (observers != null) {
pendingObservers.addAll(observers);
}
remove.remove(key); remove.remove(key);
} }
// Finally remove tiles that are gone.
// Remove tiles that are gone.
for (Map.Entry<String, List<DynamicDataObserver>> entry : remove.entrySet()) { for (Map.Entry<String, List<DynamicDataObserver>> entry : remove.entrySet()) {
final String key = entry.getKey(); final String key = entry.getKey();
mDashboardTilePrefKeys.remove(key); mDashboardTilePrefKeys.remove(key);
@@ -501,6 +508,20 @@ public abstract class DashboardFragment extends SettingsPreferenceFragment
} }
unregisterDynamicDataObservers(entry.getValue()); 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 @Override
@@ -546,4 +567,12 @@ public abstract class DashboardFragment extends SettingsPreferenceFragment
resolver.unregisterContentObserver(observer); resolver.unregisterContentObserver(observer);
}); });
} }
private void awaitObserverLatch(CountDownLatch latch) {
try {
latch.await(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
// Do nothing
}
}
} }

View File

@@ -20,13 +20,24 @@ import android.net.Uri;
import android.os.Handler; import android.os.Handler;
import android.os.Looper; import android.os.Looper;
import com.android.settingslib.utils.ThreadUtils;
import java.util.concurrent.CountDownLatch;
/** /**
* Observer for updating injected dynamic data. * Observer for updating injected dynamic data.
*/ */
public abstract class DynamicDataObserver extends ContentObserver { public abstract class DynamicDataObserver extends ContentObserver {
private Runnable mUpdateRunnable;
private CountDownLatch mCountDownLatch;
private boolean mUpdateDelegated;
protected DynamicDataObserver() { protected DynamicDataObserver() {
super(new Handler(Looper.getMainLooper())); super(new Handler(Looper.getMainLooper()));
mCountDownLatch = new CountDownLatch(1);
// Load data for the first time
onDataChanged();
} }
/** Returns the uri of the callback. */ /** Returns the uri of the callback. */
@@ -35,8 +46,30 @@ public abstract class DynamicDataObserver extends ContentObserver {
/** Called when data changes. */ /** Called when data changes. */
public abstract void onDataChanged(); 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 @Override
public void onChange(boolean selfChange) { public void onChange(boolean selfChange) {
onDataChanged(); onDataChanged();
} }
protected synchronized void post(Runnable runnable) {
if (mUpdateDelegated) {
ThreadUtils.postOnMainThread(runnable);
} else {
mUpdateRunnable = runnable;
mCountDownLatch.countDown();
}
}
} }

View File

@@ -176,7 +176,7 @@ public class BluetoothDevicesSlice implements CustomSliceable {
List<CachedBluetoothDevice> getPairedBluetoothDevices() { List<CachedBluetoothDevice> getPairedBluetoothDevices() {
final List<CachedBluetoothDevice> bluetoothDeviceList = new ArrayList<>(); final List<CachedBluetoothDevice> 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()) { if (!BluetoothAdapter.getDefaultAdapter().isEnabled()) {
Log.i(TAG, "Cannot get Bluetooth devices, Bluetooth is disabled."); Log.i(TAG, "Cannot get Bluetooth devices, Bluetooth is disabled.");
return bluetoothDeviceList; return bluetoothDeviceList;

View File

@@ -308,8 +308,12 @@ public class DashboardFeatureProviderImplTest {
mActivity, mFragment, mForceRoundedIcon, preference, tile, null /* key */, mActivity, mFragment, mForceRoundedIcon, preference, tile, null /* key */,
Preference.DEFAULT_ORDER); Preference.DEFAULT_ORDER);
assertThat(preference.getSummary()).isEqualTo(ShadowTileUtils.MOCK_SUMMARY);
assertThat(observers.get(0).getUri().toString()).isEqualTo(uriString); 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 @Test
@@ -324,8 +328,12 @@ public class DashboardFeatureProviderImplTest {
mActivity, mFragment, mForceRoundedIcon, preference, tile, null /* key */, mActivity, mFragment, mForceRoundedIcon, preference, tile, null /* key */,
Preference.DEFAULT_ORDER); Preference.DEFAULT_ORDER);
assertThat(preference.getTitle()).isEqualTo(ShadowTileUtils.MOCK_SUMMARY);
assertThat(observers.get(0).getUri().toString()).isEqualTo(uriString); 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 @Test
@@ -379,6 +387,7 @@ public class DashboardFeatureProviderImplTest {
final List<DynamicDataObserver> observers = mImpl.bindPreferenceToTileAndGetObservers( final List<DynamicDataObserver> observers = mImpl.bindPreferenceToTileAndGetObservers(
mActivity, mFragment, mForceRoundedIcon, preference, tile, null /* key */, mActivity, mFragment, mForceRoundedIcon, preference, tile, null /* key */,
Preference.DEFAULT_ORDER); Preference.DEFAULT_ORDER);
observers.get(0).updateUi();
ShadowTileUtils.setProviderChecked(false); ShadowTileUtils.setProviderChecked(false);
observers.get(0).onDataChanged(); observers.get(0).onDataChanged();

View File

@@ -34,7 +34,7 @@ import java.util.Map;
@Implements(TileUtils.class) @Implements(TileUtils.class)
public class ShadowTileUtils { public class ShadowTileUtils {
public static final String MOCK_SUMMARY = "summary"; public static final String MOCK_TEXT = "text";
private static boolean sChecked; private static boolean sChecked;
private static Bundle sResult; private static Bundle sResult;
@@ -42,13 +42,14 @@ public class ShadowTileUtils {
@Implementation @Implementation
protected static String getTextFromUri(Context context, Uri uri, protected static String getTextFromUri(Context context, Uri uri,
Map<String, IContentProvider> providerMap, String key) { Map<String, IContentProvider> providerMap, String key) {
return MOCK_SUMMARY; return MOCK_TEXT;
} }
@Implementation @Implementation
protected static Pair<String, Integer> getIconFromUri(Context context, String packageName, protected static Pair<String, Integer> getIconFromUri(Context context, String packageName,
Uri uri, Map<String, IContentProvider> providerMap) { Uri uri, Map<String, IContentProvider> providerMap) {
return Pair.create(RuntimeEnvironment.application.getPackageName(), R.drawable.ic_settings_accent); return Pair.create(RuntimeEnvironment.application.getPackageName(),
R.drawable.ic_settings_accent);
} }
@Implementation @Implementation