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:
@@ -58,6 +58,7 @@ public class CategoryMixin implements LifecycleObserver {
|
||||
private final PackageReceiver mPackageReceiver = new PackageReceiver();
|
||||
private final List<CategoryListener> 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();
|
||||
}
|
||||
|
||||
|
@@ -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<String, IContentProvider> 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<String, IContentProvider> 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<String, IContentProvider> 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);
|
||||
});
|
||||
|
@@ -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<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.
|
||||
final Map<String, List<DynamicDataObserver>> remove = new ArrayMap(mDashboardTilePrefKeys);
|
||||
|
||||
// Install dashboard tiles.
|
||||
// Install dashboard tiles and collect pending observers.
|
||||
final boolean forceRoundedIcons = shouldForceRoundedIcon();
|
||||
final List<DynamicDataObserver> 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<DynamicDataObserver> 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<DynamicDataObserver> observers =
|
||||
mDashboardFeatureProvider.bindPreferenceToTileAndGetObservers(getActivity(),
|
||||
this, forceRoundedIcons, pref, tile, key,
|
||||
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<String, List<DynamicDataObserver>> 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -176,7 +176,7 @@ public class BluetoothDevicesSlice implements CustomSliceable {
|
||||
List<CachedBluetoothDevice> getPairedBluetoothDevices() {
|
||||
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()) {
|
||||
Log.i(TAG, "Cannot get Bluetooth devices, Bluetooth is disabled.");
|
||||
return bluetoothDeviceList;
|
||||
|
@@ -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<DynamicDataObserver> observers = mImpl.bindPreferenceToTileAndGetObservers(
|
||||
mActivity, mFragment, mForceRoundedIcon, preference, tile, null /* key */,
|
||||
Preference.DEFAULT_ORDER);
|
||||
observers.get(0).updateUi();
|
||||
|
||||
ShadowTileUtils.setProviderChecked(false);
|
||||
observers.get(0).onDataChanged();
|
||||
|
@@ -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<String, IContentProvider> providerMap, String key) {
|
||||
return MOCK_SUMMARY;
|
||||
return MOCK_TEXT;
|
||||
}
|
||||
|
||||
@Implementation
|
||||
protected static Pair<String, Integer> getIconFromUri(Context context, String packageName,
|
||||
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
|
||||
|
Reference in New Issue
Block a user