diff options
author | kwaky <kwaky@google.com> | 2019-10-30 12:22:51 -0700 |
---|---|---|
committer | android-build-merger <android-build-merger@google.com> | 2019-10-30 12:22:51 -0700 |
commit | 10b115aa4bebb3b8d3cbc663945f8dede27fd738 (patch) | |
tree | 67eb98afd2872a717f369b28d1d8413e5b5f1bfa | |
parent | 66c678107c435efd6166db6ac8fe74b7b0930ba7 (diff) | |
parent | 4b04bd41315849f80b4eff72785521234742f8f1 (diff) | |
download | Settings-10b115aa4bebb3b8d3cbc663945f8dede27fd738.tar.gz |
Merge "Load app list data real-time with throttling instead of waiting for all of them to load succesfully." into pi-car-dev am: 7070df90f7
am: 4b04bd4131
Change-Id: I1d681c417cf4c3f12001418bd5e8e61007554bf2
6 files changed, 151 insertions, 20 deletions
diff --git a/res/values/integers.xml b/res/values/integers.xml index 91c952212..0ab321373 100644 --- a/res/values/integers.xml +++ b/res/values/integers.xml @@ -45,4 +45,10 @@ <!-- Maximum size in pixels of default app icons --> <integer name="default_app_safe_icon_size">500</integer> + + <!-- Millisecond interval between app data update --> + <integer name="millisecond_app_data_update_interval">500</integer> + + <!-- Millisecond interval between app data update --> + <integer name="millisecond_max_app_load_wait_interval">5000</integer> </resources> diff --git a/src/com/android/car/settings/applications/ApplicationListItemManager.java b/src/com/android/car/settings/applications/ApplicationListItemManager.java index 8f7f95b36..6a75d3b4e 100644 --- a/src/com/android/car/settings/applications/ApplicationListItemManager.java +++ b/src/com/android/car/settings/applications/ApplicationListItemManager.java @@ -15,7 +15,7 @@ */ package com.android.car.settings.applications; -import android.graphics.drawable.Drawable; +import android.os.Handler; import android.os.storage.VolumeInfo; import androidx.lifecycle.Lifecycle; @@ -25,7 +25,9 @@ import com.android.settingslib.applications.ApplicationsState; import java.util.ArrayList; import java.util.Comparator; +import java.util.HashSet; import java.util.List; +import java.util.Set; /** * Class used to load the applications installed on the system with their metadata. @@ -44,21 +46,43 @@ public class ApplicationListItemManager implements ApplicationsState.Callbacks { } private static final Logger LOG = new Logger(ApplicationListItemManager.class); + private static final String APP_NAME_UNKNOWN = "APP NAME UNKNOWN"; private final VolumeInfo mVolumeInfo; private final Lifecycle mLifecycle; private final ApplicationsState mAppState; private final List<AppListItemListener> mAppListItemListeners = new ArrayList<>(); + private final Handler mHandler; + private final int mMillisecondUpdateInterval; + // Milliseconds that warnIfNotAllLoadedInTime method waits before comparing mAppsToLoad and + // mLoadedApps to log any apps that failed to load. + private final int mMaxAppLoadWaitInterval; private ApplicationsState.Session mSession; private ApplicationsState.AppFilter mAppFilter; private Comparator<ApplicationsState.AppEntry> mAppEntryComparator; + // Contains all of the apps that we are expecting to load. + private Set<ApplicationsState.AppEntry> mAppsToLoad = new HashSet<>(); + // Contains all apps that have been successfully loaded. + private ArrayList<ApplicationsState.AppEntry> mLoadedApps; + + // Indicates whether onRebuildComplete's throttling is off and it is ready to render updates. + // onRebuildComplete uses throttling to prevent it from being called too often, since the + // animation can be choppy if the refresh rate is too high. + private boolean mReadyToRenderUpdates = true; + // Parameter we use to call onRebuildComplete method when the throttling is off and we are + // "ReadyToRenderUpdates" again. + private ArrayList<ApplicationsState.AppEntry> mDeferredAppsToUpload; public ApplicationListItemManager(VolumeInfo volumeInfo, Lifecycle lifecycle, - ApplicationsState appState) { + ApplicationsState appState, int millisecondUpdateInterval, + int maxWaitIntervalToFinishLoading) { mVolumeInfo = volumeInfo; mLifecycle = lifecycle; mAppState = appState; + mHandler = new Handler(); + mMillisecondUpdateInterval = millisecondUpdateInterval; + mMaxAppLoadWaitInterval = maxWaitIntervalToFinishLoading; } /** @@ -78,10 +102,11 @@ public class ApplicationListItemManager implements ApplicationsState.Callbacks { } /** - * Resumes the session on fragment start. + * Resumes the session and starts meauring app loading time on fragment start. */ public void onFragmentStart() { mSession.onResume(); + warnIfNotAllLoadedInTime(); } /** @@ -157,24 +182,64 @@ public class ApplicationListItemManager implements ApplicationsState.Callbacks { @Override public void onRebuildComplete(ArrayList<ApplicationsState.AppEntry> apps) { - List<String> successfullyLoadedApplications = new ArrayList<>(); - for (ApplicationsState.AppEntry appEntry : apps) { - String key = appEntry.label + appEntry.sizeStr; - if (isLoaded(appEntry.label, - appEntry.sizeStr, appEntry.icon)) { - successfullyLoadedApplications.add(key); - } + // Checking for apps.size prevents us from unnecessarily triggering throttling and blocking + // subsequent updates. + if (apps.size() == 0) { + return; } - if (successfullyLoadedApplications.size() == apps.size()) { + if (mReadyToRenderUpdates) { + mReadyToRenderUpdates = false; + mLoadedApps = new ArrayList<>(); + + for (ApplicationsState.AppEntry app : apps) { + if (isLoaded(app)) { + mLoadedApps.add(app); + } + } + for (AppListItemListener appListItemListener : mAppListItemListeners) { - appListItemListener.onDataLoaded(apps); + appListItemListener.onDataLoaded(mLoadedApps); } + + mHandler.postDelayed(() -> { + mReadyToRenderUpdates = true; + if (mDeferredAppsToUpload != null) { + onRebuildComplete(mDeferredAppsToUpload); + mDeferredAppsToUpload = null; + } + }, mMillisecondUpdateInterval); + } else { + mDeferredAppsToUpload = apps; } + + // Add all apps that are not already contained in mAppsToLoad Set, since we want it to be an + // exhaustive Set of all apps to be loaded. + mAppsToLoad.addAll(apps); + } + + private boolean isLoaded(ApplicationsState.AppEntry app) { + return app.label != null && app.sizeStr != null && app.icon != null; } - private boolean isLoaded(String title, String summary, Drawable icon) { - return title != null && summary != null && icon != null; + private void warnIfNotAllLoadedInTime() { + mHandler.postDelayed(() -> { + if (mLoadedApps.size() < mAppsToLoad.size()) { + LOG.w("Expected to load " + mAppsToLoad.size() + " apps but only loaded " + + mLoadedApps.size()); + + // Creating a copy to avoid state inconsistency. + Set<ApplicationsState.AppEntry> appsToLoadCopy = new HashSet(mAppsToLoad); + for (ApplicationsState.AppEntry loadedApp : mLoadedApps) { + appsToLoadCopy.remove(loadedApp); + } + + for (ApplicationsState.AppEntry appEntry : appsToLoadCopy) { + String appName = appEntry.label == null ? APP_NAME_UNKNOWN : appEntry.label; + LOG.w("App failed to load: " + appName); + } + } + }, mMaxAppLoadWaitInterval); } ApplicationsState.AppFilter getCompositeFilter(String volumeUuid) { diff --git a/src/com/android/car/settings/applications/ApplicationsSettingsFragment.java b/src/com/android/car/settings/applications/ApplicationsSettingsFragment.java index 0dce6be3c..b74ac4aa8 100644 --- a/src/com/android/car/settings/applications/ApplicationsSettingsFragment.java +++ b/src/com/android/car/settings/applications/ApplicationsSettingsFragment.java @@ -47,7 +47,11 @@ public class ApplicationsSettingsFragment extends AppListFragment { StorageManager sm = context.getSystemService(StorageManager.class); VolumeInfo volume = maybeInitializeVolume(sm, getArguments()); mAppListItemManager = new ApplicationListItemManager(volume, getLifecycle(), - ApplicationsState.getInstance(application)); + ApplicationsState.getInstance(application), + getContext().getResources().getInteger( + R.integer.millisecond_app_data_update_interval), + getContext().getResources().getInteger( + R.integer.millisecond_max_app_load_wait_interval)); mAppListItemManager.registerListener( use(ApplicationsSettingsPreferenceController.class, R.string.pk_all_applications_settings_list)); diff --git a/src/com/android/car/settings/storage/StorageMediaCategoryDetailFragment.java b/src/com/android/car/settings/storage/StorageMediaCategoryDetailFragment.java index f7af59713..c1a3e313a 100644 --- a/src/com/android/car/settings/storage/StorageMediaCategoryDetailFragment.java +++ b/src/com/android/car/settings/storage/StorageMediaCategoryDetailFragment.java @@ -59,7 +59,11 @@ public class StorageMediaCategoryDetailFragment extends AppListFragment { VolumeInfo volume = maybeInitializeVolume(sm, getArguments()); Application application = requireActivity().getApplication(); mAppListItemManager = new ApplicationListItemManager(volume, getLifecycle(), - ApplicationsState.getInstance(application)); + ApplicationsState.getInstance(application), + getContext().getResources().getInteger( + R.integer.millisecond_app_data_update_interval), + getContext().getResources().getInteger( + R.integer.millisecond_max_app_load_wait_interval)); StorageMediaCategoryDetailPreferenceController pc = use( StorageMediaCategoryDetailPreferenceController.class, R.string.pk_storage_music_audio_details); diff --git a/src/com/android/car/settings/storage/StorageOtherCategoryDetailFragment.java b/src/com/android/car/settings/storage/StorageOtherCategoryDetailFragment.java index 6aec796dc..b02708164 100644 --- a/src/com/android/car/settings/storage/StorageOtherCategoryDetailFragment.java +++ b/src/com/android/car/settings/storage/StorageOtherCategoryDetailFragment.java @@ -47,7 +47,11 @@ public class StorageOtherCategoryDetailFragment extends AppListFragment { StorageManager sm = context.getSystemService(StorageManager.class); VolumeInfo volume = maybeInitializeVolume(sm, getArguments()); mAppListItemManager = new ApplicationListItemManager(volume, getLifecycle(), - ApplicationsState.getInstance(application)); + ApplicationsState.getInstance(application), + getContext().getResources().getInteger( + R.integer.millisecond_app_data_update_interval), + getContext().getResources().getInteger( + R.integer.millisecond_max_app_load_wait_interval)); mAppListItemManager.registerListener( use(StorageApplicationListPreferenceController.class, R.string.pk_storage_other_apps_details)); diff --git a/tests/robotests/src/com/android/car/settings/applications/ApplicationListItemManagerTest.java b/tests/robotests/src/com/android/car/settings/applications/ApplicationListItemManagerTest.java index 78702cdd2..a521708d8 100644 --- a/tests/robotests/src/com/android/car/settings/applications/ApplicationListItemManagerTest.java +++ b/tests/robotests/src/com/android/car/settings/applications/ApplicationListItemManagerTest.java @@ -37,6 +37,7 @@ import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.robolectric.RuntimeEnvironment; +import org.robolectric.shadows.ShadowLooper; import java.util.ArrayList; @@ -47,6 +48,8 @@ public class ApplicationListItemManagerTest { private static final String SIZE_STR = "12.34 MB"; private static final String SOURCE = "source"; private static final int UID = 12; + private static final int MILLISECOND_UPDATE_INTERVAL = 500; + private static final int MILLISECOND_MAX_APP_LOAD_WAIT_INTERVAL = 5000; private Context mContext; private ApplicationListItemManager mApplicationListItemManager; @@ -69,7 +72,7 @@ public class ApplicationListItemManagerTest { MockitoAnnotations.initMocks(this); mContext = RuntimeEnvironment.application; mApplicationListItemManager = new ApplicationListItemManager(mVolumeInfo, mLifecycle, - mAppState); + mAppState, MILLISECOND_UPDATE_INTERVAL, MILLISECOND_MAX_APP_LOAD_WAIT_INTERVAL); } @Test @@ -87,7 +90,7 @@ public class ApplicationListItemManagerTest { appInfo.sourceDir = SOURCE; ApplicationsState.AppEntry appEntry = new ApplicationsState.AppEntry(mContext, appInfo, - 1234L); + /* id= */ 1234L); appEntry.label = LABEL; appEntry.sizeStr = SIZE_STR; appEntry.icon = mContext.getDrawable(R.drawable.test_icon); @@ -109,7 +112,7 @@ public class ApplicationListItemManagerTest { appInfo.sourceDir = SOURCE; ApplicationsState.AppEntry appEntry = new ApplicationsState.AppEntry(mContext, appInfo, - 1234L); + /* id= */ 1234L); appEntry.label = LABEL; appEntry.sizeStr = SIZE_STR; appEntry.icon = mContext.getDrawable(R.drawable.test_icon); @@ -123,4 +126,49 @@ public class ApplicationListItemManagerTest { verify(mAppListItemListener1).onDataLoaded(apps); verify(mAppListItemListener2, times(0)).onDataLoaded(apps); } + + @Test + public void onRebuildComplete_calledAgainImmediately_shouldNotRunSecondCallImmediately() { + ArrayList<ApplicationsState.AppEntry> apps = new ArrayList<>(); + ApplicationInfo appInfo = new ApplicationInfo(); + appInfo.uid = UID; + appInfo.sourceDir = SOURCE; + + ApplicationsState.AppEntry appEntry = new ApplicationsState.AppEntry(mContext, appInfo, + /* id= */ 1234L); + + appEntry.label = LABEL; + appEntry.sizeStr = SIZE_STR; + appEntry.icon = mContext.getDrawable(R.drawable.test_icon); + apps.add(appEntry); + + mApplicationListItemManager.registerListener(mAppListItemListener1); + mApplicationListItemManager.onRebuildComplete(apps); + mApplicationListItemManager.onRebuildComplete(apps); + + verify(mAppListItemListener1, times(1)).onDataLoaded(apps); + } + + @Test + public void onRebuildComplete_calledAgainImmediately_shouldRunSecondCallAfterUpdateInterval() { + ArrayList<ApplicationsState.AppEntry> apps = new ArrayList<>(); + ApplicationInfo appInfo = new ApplicationInfo(); + appInfo.uid = UID; + appInfo.sourceDir = SOURCE; + + ApplicationsState.AppEntry appEntry = new ApplicationsState.AppEntry(mContext, appInfo, + /* id= */ 1234L); + + appEntry.label = LABEL; + appEntry.sizeStr = SIZE_STR; + appEntry.icon = mContext.getDrawable(R.drawable.test_icon); + apps.add(appEntry); + + mApplicationListItemManager.registerListener(mAppListItemListener1); + mApplicationListItemManager.onRebuildComplete(apps); + mApplicationListItemManager.onRebuildComplete(apps); + ShadowLooper.runUiThreadTasksIncludingDelayedTasks(); + + verify(mAppListItemListener1, times(2)).onDataLoaded(apps); + } } |