diff --git a/src/com/android/settings/SettingsPreferenceFragment.java b/src/com/android/settings/SettingsPreferenceFragment.java index 5f22545ca92..a3d26af8eb7 100644 --- a/src/com/android/settings/SettingsPreferenceFragment.java +++ b/src/com/android/settings/SettingsPreferenceFragment.java @@ -49,6 +49,7 @@ import com.android.settings.applications.LayoutPreference; import com.android.settings.core.InstrumentedPreferenceFragment; import com.android.settings.core.instrumentation.Instrumentable; import com.android.settings.core.instrumentation.InstrumentedDialogFragment; +import com.android.settings.widget.LoadingViewController; import com.android.settingslib.CustomDialogPreference; import com.android.settingslib.CustomEditTextPreference; import com.android.settingslib.HelpUtils; @@ -240,14 +241,11 @@ public abstract class SettingsPreferenceFragment extends InstrumentedPreferenceF unregisterObserverIfNeeded(); } - public void showLoadingWhenEmpty() { - View loading = getView().findViewById(R.id.loading_container); - setEmptyView(loading); - } - public void setLoading(boolean loading, boolean animate) { - View loading_container = getView().findViewById(R.id.loading_container); - Utils.handleLoadingContainer(loading_container, getListView(), !loading, animate); + View loadingContainer = getView().findViewById(R.id.loading_container); + LoadingViewController.handleLoadingContainer(loadingContainer, getListView(), + !loading /* done */, + animate); } public void registerObserverIfNeeded() { diff --git a/src/com/android/settings/Utils.java b/src/com/android/settings/Utils.java index cb8d3c2765b..b6a958c3e7d 100644 --- a/src/com/android/settings/Utils.java +++ b/src/com/android/settings/Utils.java @@ -949,43 +949,6 @@ 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); - setViewShown(doneLoading, done, animate); - } - - private static void setViewShown(final View view, boolean shown, boolean animate) { - if (animate) { - Animation animation = AnimationUtils.loadAnimation(view.getContext(), - shown ? android.R.anim.fade_in : android.R.anim.fade_out); - if (shown) { - view.setVisibility(View.VISIBLE); - } else { - animation.setAnimationListener(new AnimationListener() { - @Override - public void onAnimationStart(Animation animation) { - } - - @Override - public void onAnimationRepeat(Animation animation) { - } - - @Override - public void onAnimationEnd(Animation animation) { - view.setVisibility(View.INVISIBLE); - } - }); - } - view.startAnimation(animation); - } else { - view.clearAnimation(); - view.setVisibility(shown ? View.VISIBLE : View.INVISIBLE); - } - } - /** * Returns the application info of the currently installed MDM package. */ diff --git a/src/com/android/settings/applications/ManageApplications.java b/src/com/android/settings/applications/ManageApplications.java index 07632da6bf9..720f8267ecb 100644 --- a/src/com/android/settings/applications/ManageApplications.java +++ b/src/com/android/settings/applications/ManageApplications.java @@ -81,6 +81,7 @@ import com.android.settings.notification.AppNotificationSettings; import com.android.settings.notification.ConfigureNotificationSettings; import com.android.settings.notification.NotificationBackend; import com.android.settings.notification.NotificationBackend.AppRow; +import com.android.settings.widget.LoadingViewController; import com.android.settingslib.HelpUtils; import com.android.settingslib.applications.ApplicationsState; import com.android.settingslib.applications.ApplicationsState.AppEntry; @@ -848,6 +849,7 @@ public class ManageApplications extends InstrumentedPreferenceFragment private final AppStateBaseBridge mExtraInfoBridge; private final Handler mBgHandler; private final Handler mFgHandler; + private final LoadingViewController mLoadingViewController; private int mFilterMode; private ArrayList mBaseEntries; @@ -893,12 +895,6 @@ 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) { @@ -907,6 +903,10 @@ public class ManageApplications extends InstrumentedPreferenceFragment mBgHandler = new Handler(mState.getBackgroundLooper()); mSession = state.newSession(this); mManageApplications = manageApplications; + mLoadingViewController = new LoadingViewController( + mManageApplications.mLoadingContainer, + mManageApplications.mListContainer + ); mContext = manageApplications.getActivity(); mPm = mContext.getPackageManager(); mFilterMode = filterMode; @@ -1108,11 +1108,7 @@ 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); + mLoadingViewController.showContent(true /* animate */); } if (mManageApplications.mListType == LIST_TYPE_USAGE_ACCESS) { // No enabled or disabled filters for usage access. @@ -1166,11 +1162,9 @@ public class ManageApplications extends InstrumentedPreferenceFragment void updateLoading() { final boolean appLoaded = mHasReceivedLoadEntries && mSession.getAllApps().size() != 0; if (appLoaded) { - Utils.handleLoadingContainer(mManageApplications.mLoadingContainer, - mManageApplications.mListContainer, true /* done */, false /* animate */); + mLoadingViewController.showContent(false /* animate */); } else { - mFgHandler.postDelayed( - mShowLoadingContainerRunnable, DELAY_SHOW_LOADING_CONTAINER_THRESHOLD_MS); + mLoadingViewController.showLoadingViewDelayed(); } } diff --git a/src/com/android/settings/applications/RunningServices.java b/src/com/android/settings/applications/RunningServices.java index 736eafb1c98..634fefdb9f9 100644 --- a/src/com/android/settings/applications/RunningServices.java +++ b/src/com/android/settings/applications/RunningServices.java @@ -15,7 +15,6 @@ */ package com.android.settings.applications; -import android.app.Fragment; import android.os.Bundle; import android.view.LayoutInflater; import android.view.Menu; @@ -27,7 +26,7 @@ import android.view.ViewGroup; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.settings.R; import com.android.settings.SettingsPreferenceFragment; -import com.android.settings.Utils; +import com.android.settings.widget.LoadingViewController; public class RunningServices extends SettingsPreferenceFragment { @@ -37,6 +36,7 @@ public class RunningServices extends SettingsPreferenceFragment { private RunningProcessesView mRunningProcessesView; private Menu mOptionsMenu; private View mLoadingContainer; + private LoadingViewController mLoadingViewController; @Override public void onCreate(Bundle savedInstanceState) { @@ -47,12 +47,13 @@ public class RunningServices extends SettingsPreferenceFragment { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { + Bundle savedInstanceState) { View rootView = inflater.inflate(R.layout.manage_applications_running, null); - mRunningProcessesView = (RunningProcessesView) rootView.findViewById( - R.id.running_processes); + mRunningProcessesView = rootView.findViewById(R.id.running_processes); mRunningProcessesView.doCreate(); mLoadingContainer = rootView.findViewById(R.id.loading_container); + mLoadingViewController = new LoadingViewController( + mLoadingContainer, mRunningProcessesView); return rootView; } @@ -71,7 +72,7 @@ public class RunningServices extends SettingsPreferenceFragment { public void onResume() { super.onResume(); boolean haveData = mRunningProcessesView.doResume(this, mRunningProcessesAvail); - Utils.handleLoadingContainer(mLoadingContainer, mRunningProcessesView, haveData, false); + mLoadingViewController.handleLoadingContainer(haveData /* done */, false /* animate */); } @Override @@ -115,7 +116,7 @@ public class RunningServices extends SettingsPreferenceFragment { private final Runnable mRunningProcessesAvail = new Runnable() { @Override public void run() { - Utils.handleLoadingContainer(mLoadingContainer, mRunningProcessesView, true, true); + mLoadingViewController.showContent(true /* animate */); } }; diff --git a/src/com/android/settings/datausage/DataUsageList.java b/src/com/android/settings/datausage/DataUsageList.java index de92154546b..f0048a32a41 100644 --- a/src/com/android/settings/datausage/DataUsageList.java +++ b/src/com/android/settings/datausage/DataUsageList.java @@ -50,6 +50,7 @@ import android.widget.Spinner; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.settings.R; import com.android.settings.datausage.CycleAdapter.SpinnerInterface; +import com.android.settings.widget.LoadingViewController; import com.android.settingslib.AppItem; import com.android.settingslib.net.ChartData; import com.android.settingslib.net.ChartDataLoader; @@ -96,13 +97,13 @@ public class DataUsageList extends DataUsageBase { }; private INetworkStatsSession mStatsSession; - private ChartDataUsagePreference mChart; private NetworkTemplate mTemplate; private int mSubId; private ChartData mChartData; + private LoadingViewController mLoadingViewController; private UidDetailProvider mUidDetailProvider; private CycleAdapter mCycleAdapter; private Spinner mCycleSpinner; @@ -110,6 +111,7 @@ public class DataUsageList extends DataUsageBase { private PreferenceGroup mApps; private View mHeader; + @Override public int getMetricsCategory() { return MetricsEvent.DATA_USAGE_LIST; @@ -176,7 +178,10 @@ public class DataUsageList extends DataUsageBase { mCycleSpinner.setSelection(position); } }, mCycleListener, true); - setLoading(true, false); + + mLoadingViewController = new LoadingViewController( + getView().findViewById(R.id.loading_container), getListView()); + mLoadingViewController.showLoadingViewDelayed(); } @Override @@ -523,7 +528,7 @@ public class DataUsageList extends DataUsageBase { @Override public void onLoadFinished(Loader loader, ChartData data) { - setLoading(false, true); + mLoadingViewController.showContent(false /* animate */); mChartData = data; mChart.setNetworkStats(mChartData.network); diff --git a/src/com/android/settings/widget/LoadingViewController.java b/src/com/android/settings/widget/LoadingViewController.java new file mode 100644 index 00000000000..294e55e7ea8 --- /dev/null +++ b/src/com/android/settings/widget/LoadingViewController.java @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.widget; + +import android.os.Handler; +import android.os.Looper; +import android.view.View; +import android.view.animation.Animation; +import android.view.animation.AnimationUtils; + +/** + * A helper class that manages show/hide loading spinner. + */ +public class LoadingViewController { + + private static final long DELAY_SHOW_LOADING_CONTAINER_THRESHOLD_MS = 100L; + + public final Handler mFgHandler; + public final View mLoadingView; + public final View mContentView; + + public LoadingViewController(View loadingView, View contentView) { + mLoadingView = loadingView; + mContentView = contentView; + mFgHandler = new Handler(Looper.getMainLooper()); + } + + private Runnable mShowLoadingContainerRunnable = new Runnable() { + public void run() { + handleLoadingContainer(false /* done */, false /* animate */); + } + }; + + public void showContent(boolean animate) { + // Cancel any pending task to show the loading animation and show the list of + // apps directly. + mFgHandler.removeCallbacks(mShowLoadingContainerRunnable); + handleLoadingContainer(true /* show */, animate); + } + + public void showLoadingViewDelayed() { + mFgHandler.postDelayed( + mShowLoadingContainerRunnable, DELAY_SHOW_LOADING_CONTAINER_THRESHOLD_MS); + } + + public void handleLoadingContainer(boolean done, boolean animate) { + handleLoadingContainer(mLoadingView, mContentView, done, animate); + } + + /** + * Show/hide loading view and content view. + * + * @param loading The loading spinner view + * @param content The content view + * @param done If true, content is set visible and loading is set invisible. + * @param animate Whether or not content/loading views should animate in/out. + */ + public static void handleLoadingContainer(View loading, View content, boolean done, + boolean animate) { + setViewShown(loading, !done, animate); + setViewShown(content, done, animate); + } + + private static void setViewShown(final View view, boolean shown, boolean animate) { + if (animate) { + Animation animation = AnimationUtils.loadAnimation(view.getContext(), + shown ? android.R.anim.fade_in : android.R.anim.fade_out); + if (shown) { + view.setVisibility(View.VISIBLE); + } else { + animation.setAnimationListener(new Animation.AnimationListener() { + @Override + public void onAnimationStart(Animation animation) { + } + + @Override + public void onAnimationRepeat(Animation animation) { + } + + @Override + public void onAnimationEnd(Animation animation) { + view.setVisibility(View.INVISIBLE); + } + }); + } + view.startAnimation(animation); + } else { + view.clearAnimation(); + view.setVisibility(shown ? View.VISIBLE : View.INVISIBLE); + } + } +} diff --git a/tests/robotests/src/com/android/settings/applications/ManageApplicationsTest.java b/tests/robotests/src/com/android/settings/applications/ManageApplicationsTest.java index 52c1069e422..92aa675c00d 100644 --- a/tests/robotests/src/com/android/settings/applications/ManageApplicationsTest.java +++ b/tests/robotests/src/com/android/settings/applications/ManageApplicationsTest.java @@ -37,12 +37,10 @@ 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.settings.widget.LoadingViewController; 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; @@ -53,10 +51,11 @@ import org.robolectric.annotation.Config; import org.robolectric.fakes.RoboMenuItem; import org.robolectric.util.ReflectionHelpers; +import java.util.ArrayList; + 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; @@ -170,12 +169,12 @@ public class ManageApplicationsTest { 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); + final LoadingViewController loadingViewController = + mock(LoadingViewController.class); + ReflectionHelpers.setField(adapter, "mLoadingViewController", loadingViewController); ReflectionHelpers.setField(adapter, "mFgHandler", handler); // app loading completed @@ -186,7 +185,7 @@ public class ManageApplicationsTest { adapter.updateLoading(); - verify(handler, never()).postDelayed(eq(showLoadingContainerRunnable), anyLong()); + verify(loadingViewController, never()).showLoadingViewDelayed(); } @Test @@ -195,12 +194,13 @@ public class ManageApplicationsTest { 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); + final LoadingViewController loadingViewController = + mock(LoadingViewController.class); + ReflectionHelpers.setField(adapter, "mLoadingViewController", loadingViewController); ReflectionHelpers.setField(adapter, "mFgHandler", handler); // app loading not yet completed @@ -208,11 +208,11 @@ public class ManageApplicationsTest { adapter.updateLoading(); - verify(handler).postDelayed(eq(showLoadingContainerRunnable), anyLong()); + verify(loadingViewController).showLoadingViewDelayed(); } @Test - public void onRebuildComplete_shouldCancelDelayedRunnable() { + public void onRebuildComplete_shouldHideLoadingView() { final Context context = RuntimeEnvironment.application; final ManageApplications fragment = mock(ManageApplications.class); final View loadingContainer = mock(View.class); @@ -223,12 +223,12 @@ public class ManageApplicationsTest { 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); + final LoadingViewController loadingViewController = + mock(LoadingViewController.class); + ReflectionHelpers.setField(adapter, "mLoadingViewController", loadingViewController); ReflectionHelpers.setField(adapter, "mFgHandler", handler); ReflectionHelpers.setField(adapter, "mFilterMode", -1); @@ -244,7 +244,7 @@ public class ManageApplicationsTest { adapter.onRebuildComplete(null); - verify(handler).removeCallbacks(showLoadingContainerRunnable); + verify(loadingViewController).showContent(true /* animate */); } private void setUpOptionMenus() { diff --git a/tests/robotests/src/com/android/settings/widget/LoadingViewControllerTest.java b/tests/robotests/src/com/android/settings/widget/LoadingViewControllerTest.java new file mode 100644 index 00000000000..09b52c8b83e --- /dev/null +++ b/tests/robotests/src/com/android/settings/widget/LoadingViewControllerTest.java @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settings.widget; + +import android.content.Context; +import android.os.Handler; +import android.view.View; + +import com.android.settings.TestConfig; +import com.android.settings.testutils.SettingsRobolectricTestRunner; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; +import org.robolectric.util.ReflectionHelpers; + +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyLong; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +@RunWith(SettingsRobolectricTestRunner.class) +@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION) +public class LoadingViewControllerTest { + + private Context mContext; + private View mLoadingView; + private View mContentView; + + private LoadingViewController mController; + + @Before + public void setUp() { + mContext = RuntimeEnvironment.application; + mLoadingView = new View(mContext); + mContentView = new View(mContext); + + mController = new LoadingViewController(mLoadingView, mContentView); + } + + @Test + public void showContent_shouldSetContentVisible() { + mController.showContent(false /* animate */); + + assertThat(mContentView.getVisibility()).isEqualTo(View.VISIBLE); + } + + @Test + public void showLoadingViewDelayed_shouldPostRunnable() { + final Handler handler = mock(Handler.class); + ReflectionHelpers.setField(mController, "mFgHandler", handler); + mController.showLoadingViewDelayed(); + + verify(handler).postDelayed(any(Runnable.class), anyLong()); + } + +}