summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorkwaky <kwaky@google.com>2019-10-30 12:22:51 -0700
committerandroid-build-merger <android-build-merger@google.com>2019-10-30 12:22:51 -0700
commit10b115aa4bebb3b8d3cbc663945f8dede27fd738 (patch)
tree67eb98afd2872a717f369b28d1d8413e5b5f1bfa
parent66c678107c435efd6166db6ac8fe74b7b0930ba7 (diff)
parent4b04bd41315849f80b4eff72785521234742f8f1 (diff)
downloadSettings-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
-rw-r--r--res/values/integers.xml6
-rw-r--r--src/com/android/car/settings/applications/ApplicationListItemManager.java93
-rw-r--r--src/com/android/car/settings/applications/ApplicationsSettingsFragment.java6
-rw-r--r--src/com/android/car/settings/storage/StorageMediaCategoryDetailFragment.java6
-rw-r--r--src/com/android/car/settings/storage/StorageOtherCategoryDetailFragment.java6
-rw-r--r--tests/robotests/src/com/android/car/settings/applications/ApplicationListItemManagerTest.java54
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);
+ }
}