diff --git a/src/com/android/settings/Utils.java b/src/com/android/settings/Utils.java index 15e4eb3fbcc..cfa8377b5f8 100644 --- a/src/com/android/settings/Utils.java +++ b/src/com/android/settings/Utils.java @@ -943,6 +943,8 @@ public final class Utils extends com.android.settingslib.Utils { return result; } + // TODO: move this out of Utils to a mixin or a controller or a helper class. + @Deprecated public static void handleLoadingContainer(View loading, View doneLoading, boolean done, boolean animate) { setViewShown(loading, !done, animate); diff --git a/src/com/android/settings/applications/ManageApplications.java b/src/com/android/settings/applications/ManageApplications.java index d6ae7b26740..07632da6bf9 100644 --- a/src/com/android/settings/applications/ManageApplications.java +++ b/src/com/android/settings/applications/ManageApplications.java @@ -346,7 +346,6 @@ public class ManageApplications extends InstrumentedPreferenceFragment mRootView = inflater.inflate(R.layout.manage_applications_apps, null); mLoadingContainer = mRootView.findViewById(R.id.loading_container); - mLoadingContainer.setVisibility(View.VISIBLE); mListContainer = mRootView.findViewById(R.id.list_container); if (mListContainer != null) { // Create adapter and list view here @@ -395,7 +394,8 @@ public class ManageApplications extends InstrumentedPreferenceFragment return mRootView; } - private void createHeader() { + @VisibleForTesting + void createHeader() { Activity activity = getActivity(); FrameLayout pinnedHeader = (FrameLayout) mRootView.findViewById(R.id.pinned_header); mSpinnerHeader = activity.getLayoutInflater() @@ -834,6 +834,10 @@ public class ManageApplications extends InstrumentedPreferenceFragment static class ApplicationsAdapter extends BaseAdapter implements Filterable, ApplicationsState.Callbacks, AppStateBaseBridge.Callback, AbsListView.RecyclerListener, SectionIndexer { + + // how long to wait for app list to populate without showing the loading container + private static final long DELAY_SHOW_LOADING_CONTAINER_THRESHOLD_MS = 100L; + private static final SectionInfo[] EMPTY_SECTIONS = new SectionInfo[0]; private final ApplicationsState mState; @@ -889,6 +893,13 @@ public class ManageApplications extends InstrumentedPreferenceFragment } }; + private Runnable mShowLoadingContainerRunnable = new Runnable() { + public void run() { + Utils.handleLoadingContainer(mManageApplications.mLoadingContainer, + mManageApplications.mListContainer, false /* done */, false /* animate */); + } + }; + public ApplicationsAdapter(ApplicationsState state, ManageApplications manageApplications, int filterMode) { mState = state; @@ -1097,6 +1108,9 @@ public class ManageApplications extends InstrumentedPreferenceFragment if (mSession.getAllApps().size() != 0 && mManageApplications.mListContainer.getVisibility() != View.VISIBLE) { + // Cancel any pending task to show the loading animation and show the list of + // apps directly. + mFgHandler.removeCallbacks(mShowLoadingContainerRunnable); Utils.handleLoadingContainer(mManageApplications.mLoadingContainer, mManageApplications.mListContainer, true, true); } @@ -1148,10 +1162,16 @@ public class ManageApplications extends InstrumentedPreferenceFragment } } - private void updateLoading() { - Utils.handleLoadingContainer(mManageApplications.mLoadingContainer, - mManageApplications.mListContainer, - mHasReceivedLoadEntries && mSession.getAllApps().size() != 0, false); + @VisibleForTesting + void updateLoading() { + final boolean appLoaded = mHasReceivedLoadEntries && mSession.getAllApps().size() != 0; + if (appLoaded) { + Utils.handleLoadingContainer(mManageApplications.mLoadingContainer, + mManageApplications.mListContainer, true /* done */, false /* animate */); + } else { + mFgHandler.postDelayed( + mShowLoadingContainerRunnable, DELAY_SHOW_LOADING_CONTAINER_THRESHOLD_MS); + } } ArrayList applyPrefixFilter(CharSequence prefix, diff --git a/tests/robotests/src/com/android/settings/applications/ManageApplicationsTest.java b/tests/robotests/src/com/android/settings/applications/ManageApplicationsTest.java index ea1d2c35959..52c1069e422 100644 --- a/tests/robotests/src/com/android/settings/applications/ManageApplicationsTest.java +++ b/tests/robotests/src/com/android/settings/applications/ManageApplicationsTest.java @@ -17,29 +17,38 @@ package com.android.settings.applications; import android.app.Activity; +import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; +import android.os.Handler; import android.os.Looper; +import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; import android.widget.TextView; import com.android.settings.R; import com.android.settings.Settings; -import com.android.settings.testutils.SettingsRobolectricTestRunner; import com.android.settings.TestConfig; +import com.android.settings.testutils.SettingsRobolectricTestRunner; import com.android.settings.testutils.shadow.SettingsShadowResources; import com.android.settings.testutils.shadow.SettingsShadowResources.SettingsShadowTheme; import com.android.settings.testutils.shadow.ShadowDynamicIndexableContentMonitor; import com.android.settings.testutils.shadow.ShadowEventLogWriter; import com.android.settingslib.applications.ApplicationsState; +import com.android.settingslib.applications.ApplicationsState.Callbacks; import com.android.settingslib.core.lifecycle.Lifecycle; +import java.util.ArrayList; + import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import org.robolectric.RuntimeEnvironment; import org.robolectric.annotation.Config; import org.robolectric.fakes.RoboMenuItem; import org.robolectric.util.ReflectionHelpers; @@ -47,7 +56,12 @@ import org.robolectric.util.ReflectionHelpers; import static com.google.common.truth.Truth.assertThat; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyInt; +import static org.mockito.Matchers.anyLong; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -132,6 +146,107 @@ public class ManageApplicationsTest { assertThat(mMenu.findItem(R.id.reset_app_preferences).isVisible()).isFalse(); } + @Test + public void onCreateView_shouldNotShowLoadingContainer() { + final ManageApplications fragment = spy(new ManageApplications()); + ReflectionHelpers.setField(fragment, "mResetAppsHelper", + mock(ResetAppsHelper.class)); + doNothing().when(fragment).createHeader(); + + final LayoutInflater layoutInflater = mock(LayoutInflater.class); + final View view = mock(View.class); + final View loadingContainer = mock(View.class); + when(layoutInflater.inflate(anyInt(), eq(null))).thenReturn(view); + when(view.findViewById(R.id.loading_container)).thenReturn(loadingContainer); + + fragment.onCreateView(layoutInflater, mock(ViewGroup.class), null); + + verify(loadingContainer, never()).setVisibility(View.VISIBLE); + } + + @Test + public void updateLoading_appLoaded_shouldNotDelayCallToHandleLoadingContainer() { + final ManageApplications fragment = mock(ManageApplications.class); + ReflectionHelpers.setField(fragment, "mLoadingContainer", mock(View.class)); + ReflectionHelpers.setField(fragment, "mListContainer", mock(View.class)); + when(fragment.getActivity()).thenReturn(mock(Activity.class)); + final Runnable showLoadingContainerRunnable = mock(Runnable.class); + final Handler handler = mock(Handler.class); + final ManageApplications.ApplicationsAdapter adapter = + spy(new ManageApplications.ApplicationsAdapter(mState, fragment, 0)); + ReflectionHelpers.setField(adapter, "mShowLoadingContainerRunnable", + showLoadingContainerRunnable); + ReflectionHelpers.setField(adapter, "mFgHandler", handler); + + // app loading completed + ReflectionHelpers.setField(adapter, "mHasReceivedLoadEntries", true); + final ArrayList appList = new ArrayList<>(); + appList.add(mock(ApplicationsState.AppEntry.class)); + when(mSession.getAllApps()).thenReturn(appList); + + adapter.updateLoading(); + + verify(handler, never()).postDelayed(eq(showLoadingContainerRunnable), anyLong()); + } + + @Test + public void updateLoading_appNotLoaded_shouldDelayCallToHandleLoadingContainer() { + final ManageApplications fragment = mock(ManageApplications.class); + ReflectionHelpers.setField(fragment, "mLoadingContainer", mock(View.class)); + ReflectionHelpers.setField(fragment, "mListContainer", mock(View.class)); + when(fragment.getActivity()).thenReturn(mock(Activity.class)); + final Runnable showLoadingContainerRunnable = mock(Runnable.class); + final Handler handler = mock(Handler.class); + final ManageApplications.ApplicationsAdapter adapter = + spy(new ManageApplications.ApplicationsAdapter(mState, fragment, 0)); + ReflectionHelpers.setField(adapter, "mShowLoadingContainerRunnable", + showLoadingContainerRunnable); + ReflectionHelpers.setField(adapter, "mFgHandler", handler); + + // app loading not yet completed + ReflectionHelpers.setField(adapter, "mHasReceivedLoadEntries", false); + + adapter.updateLoading(); + + verify(handler).postDelayed(eq(showLoadingContainerRunnable), anyLong()); + } + + @Test + public void onRebuildComplete_shouldCancelDelayedRunnable() { + final Context context = RuntimeEnvironment.application; + final ManageApplications fragment = mock(ManageApplications.class); + final View loadingContainer = mock(View.class); + when(loadingContainer.getContext()).thenReturn(context); + final View listContainer = mock(View.class); + when(listContainer.getVisibility()).thenReturn(View.INVISIBLE); + when(listContainer.getContext()).thenReturn(context); + ReflectionHelpers.setField(fragment, "mLoadingContainer", loadingContainer); + ReflectionHelpers.setField(fragment, "mListContainer", listContainer); + when(fragment.getActivity()).thenReturn(mock(Activity.class)); + final Runnable showLoadingContainerRunnable = mock(Runnable.class); + final Handler handler = mock(Handler.class); + final ManageApplications.ApplicationsAdapter adapter = + spy(new ManageApplications.ApplicationsAdapter(mState, fragment, 0)); + ReflectionHelpers.setField(adapter, "mShowLoadingContainerRunnable", + showLoadingContainerRunnable); + ReflectionHelpers.setField(adapter, "mFgHandler", handler); + ReflectionHelpers.setField(adapter, "mFilterMode", -1); + + // app loading not yet completed + ReflectionHelpers.setField(adapter, "mHasReceivedLoadEntries", false); + adapter.updateLoading(); + + // app loading completed + ReflectionHelpers.setField(adapter, "mHasReceivedLoadEntries", true); + final ArrayList appList = new ArrayList<>(); + appList.add(mock(ApplicationsState.AppEntry.class)); + when(mSession.getAllApps()).thenReturn(appList); + + adapter.onRebuildComplete(null); + + verify(handler).removeCallbacks(showLoadingContainerRunnable); + } + private void setUpOptionMenus() { when(mMenu.findItem(anyInt())).thenAnswer(invocation -> { final Object[] args = invocation.getArguments();