diff --git a/res/xml/data_usage_list.xml b/res/xml/data_usage_list.xml index 9ea6a914a77..644fca4faf7 100644 --- a/res/xml/data_usage_list.xml +++ b/res/xml/data_usage_list.xml @@ -17,7 +17,8 @@ + android:key="usage_amount" + android:title="@string/summary_placeholder"> diff --git a/src/com/android/settings/datausage/ChartDataUsagePreference.java b/src/com/android/settings/datausage/ChartDataUsagePreference.java index dedeb7b5d16..12fb03b6258 100644 --- a/src/com/android/settings/datausage/ChartDataUsagePreference.java +++ b/src/com/android/settings/datausage/ChartDataUsagePreference.java @@ -48,8 +48,8 @@ public class ChartDataUsagePreference extends Preference { // Set to half a meg for now. private static final long RESOLUTION = DataUnit.MEBIBYTES.toBytes(1) / 2; - private int mWarningColor; - private int mLimitColor; + private final int mWarningColor; + private final int mLimitColor; private Resources mResources; private NetworkPolicy mPolicy; @@ -58,35 +58,35 @@ public class ChartDataUsagePreference extends Preference { private NetworkCycleChartData mNetworkCycleChartData; private int mSecondaryColor; private int mSeriesColor; - private UsageView mUsageView; - private boolean mSuspendUiUpdate; // Suppress UI updates to save some CPU time. public ChartDataUsagePreference(Context context, AttributeSet attrs) { super(context, attrs); + mResources = context.getResources(); setSelectable(false); + mLimitColor = Utils.getColorAttrDefaultColor(context, android.R.attr.colorError); + mWarningColor = Utils.getColorAttrDefaultColor(context, android.R.attr.textColorSecondary); + setLayoutResource(R.layout.data_usage_graph); } @Override public void onBindViewHolder(PreferenceViewHolder holder) { super.onBindViewHolder(holder); - mUsageView = (UsageView) holder.findViewById(R.id.data_usage); - } - - private void onUpdateView() { - if ((mUsageView == null) || (mNetworkCycleChartData == null)) { + final UsageView chart = (UsageView) holder.findViewById(R.id.data_usage); + if (mNetworkCycleChartData == null) { return; } + final int top = getTop(); - mUsageView.clearPaths(); - mUsageView.configureGraph(toInt(mEnd - mStart), top); - calcPoints(mUsageView, mNetworkCycleChartData.getUsageBuckets()); - setupContentDescription(mUsageView, mNetworkCycleChartData.getUsageBuckets()); - mUsageView.setBottomLabels(new CharSequence[] { + chart.clearPaths(); + chart.configureGraph(toInt(mEnd - mStart), top); + calcPoints(chart, mNetworkCycleChartData.getUsageBuckets()); + setupContentDescription(chart, mNetworkCycleChartData.getUsageBuckets()); + chart.setBottomLabels(new CharSequence[] { Utils.formatDateRange(getContext(), mStart, mStart), Utils.formatDateRange(getContext(), mEnd, mEnd), }); - bindNetworkPolicy(mUsageView, mPolicy, top); + bindNetworkPolicy(chart, mPolicy, top); } public int getTop() { @@ -291,16 +291,9 @@ public class ChartDataUsagePreference extends Preference { return new SpannableStringBuilder().append(label, new ForegroundColorSpan(mLimitColor), 0); } - public void onPreparingChartData() { - mSuspendUiUpdate = true; - } - public void setNetworkPolicy(NetworkPolicy policy) { mPolicy = policy; - if ((!mSuspendUiUpdate) && (mResources != null)) { - onUpdateView(); - notifyChanged(); - } + notifyChanged(); } public long getInspectStart() { @@ -312,31 +305,15 @@ public class ChartDataUsagePreference extends Preference { } public void setNetworkCycleData(NetworkCycleChartData data) { - if (data == null) { - return; - } mNetworkCycleChartData = data; mStart = data.getStartTime(); mEnd = data.getEndTime(); - if (mResources == null) { - Context context = getContext(); - mResources = context.getResources(); - mLimitColor = Utils.getColorAttrDefaultColor(context, android.R.attr.colorError); - mWarningColor = Utils.getColorAttrDefaultColor(context, - android.R.attr.textColorSecondary); - setLayoutResource(R.layout.data_usage_graph); - } - onUpdateView(); notifyChanged(); - mSuspendUiUpdate = false; } public void setColors(int seriesColor, int secondaryColor) { mSeriesColor = seriesColor; mSecondaryColor = secondaryColor; - if ((!mSuspendUiUpdate) && (mResources != null)) { - onUpdateView(); - notifyChanged(); - } + notifyChanged(); } } diff --git a/src/com/android/settings/datausage/CycleAdapter.java b/src/com/android/settings/datausage/CycleAdapter.java index b41b6aad91b..2af40120864 100644 --- a/src/com/android/settings/datausage/CycleAdapter.java +++ b/src/com/android/settings/datausage/CycleAdapter.java @@ -21,7 +21,6 @@ import com.android.settingslib.net.NetworkCycleData; import com.android.settingslib.widget.SettingsSpinnerAdapter; import java.util.List; -import java.util.Objects; public class CycleAdapter extends SettingsSpinnerAdapter { @@ -67,7 +66,7 @@ public class CycleAdapter extends SettingsSpinnerAdapter * Rebuild list based on network data. Always selects the newest item, * updating the inspection range on chartData. */ - public boolean updateCycleList(List cycleData) { + public void updateCycleList(List cycleData) { mSpinner.setOnItemSelectedListener(mListener); // stash away currently selected cycle to try restoring below final CycleAdapter.CycleItem previousItem = (CycleAdapter.CycleItem) @@ -83,16 +82,7 @@ public class CycleAdapter extends SettingsSpinnerAdapter if (getCount() > 0) { final int position = findNearestPosition(previousItem); mSpinner.setSelection(position); - - // only force-update cycle when changed; skipping preserves any - // user-defined inspection region. - final CycleAdapter.CycleItem selectedItem = getItem(position); - if (!Objects.equals(selectedItem, previousItem)) { - mListener.onItemSelected(null, null, position, 0); - return false; - } } - return true; } /** diff --git a/src/com/android/settings/datausage/DataUsageList.java b/src/com/android/settings/datausage/DataUsageList.java index c0ccb7638dc..1855c2e44e9 100644 --- a/src/com/android/settings/datausage/DataUsageList.java +++ b/src/com/android/settings/datausage/DataUsageList.java @@ -75,6 +75,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Objects; import java.util.Optional; /** @@ -118,7 +119,11 @@ public class DataUsageList extends DataUsageBaseFragment private ChartDataUsagePreference mChart; private List mCycleData; + // Caches the cycles for startAppDataUsage usage, which need be cleared when resumed. private ArrayList mCycles; + // Spinner will keep the selected cycle even after paused, this only keeps the displayed cycle, + // which need be cleared when resumed. + private CycleAdapter.CycleItem mLastDisplayedCycle; private UidDetailProvider mUidDetailProvider; private CycleAdapter mCycleAdapter; private Preference mUsageAmount; @@ -172,20 +177,7 @@ public class DataUsageList extends DataUsageBaseFragment public void onViewCreated(View v, Bundle savedInstanceState) { super.onViewCreated(v, savedInstanceState); - // Show loading - mLoadingViewController = new LoadingViewController( - v.findViewById(R.id.loading_container), getListView()); - mLoadingViewController.showLoadingViewDelayed(); - } - - private void onEndOfLoading() { - if (mHeader != null) { - return; - } mHeader = setPinnedHeaderView(R.layout.apps_filter_spinner); - - mCycleSpinner = mHeader.findViewById(R.id.filter_spinner); - mHeader.findViewById(R.id.filter_settings).setOnClickListener(btn -> { final Bundle args = new Bundle(); args.putParcelable(DataUsageList.EXTRA_NETWORK_TEMPLATE, mTemplate); @@ -196,6 +188,8 @@ public class DataUsageList extends DataUsageBaseFragment .setArguments(args) .launch(); }); + mCycleSpinner = mHeader.findViewById(R.id.filter_spinner); + mCycleSpinner.setVisibility(View.GONE); mCycleAdapter = new CycleAdapter(mCycleSpinner.getContext(), new SpinnerInterface() { @Override public void setAdapter(CycleAdapter cycleAdapter) { @@ -227,16 +221,18 @@ public class DataUsageList extends DataUsageBaseFragment super.sendAccessibilityEvent(host, eventType); } }); + + mLoadingViewController = new LoadingViewController( + getView().findViewById(R.id.loading_container), getListView()); } @Override public void onResume() { super.onResume(); + mLoadingViewController.showLoadingViewDelayed(); mDataStateListener.start(mSubId); - - if (mChart != null) { - mChart.onPreparingChartData(); - } + mCycles = null; + mLastDisplayedCycle = null; // kick off loader for network history // TODO: consider chaining two loaders together instead of reloading @@ -345,9 +341,6 @@ public class DataUsageList extends DataUsageBaseFragment */ @VisibleForTesting void updatePolicy() { - if (mHeader == null) { - return; - } final NetworkPolicy policy = services.mPolicyEditor.getPolicy(mTemplate); final View configureButton = mHeader.findViewById(R.id.filter_settings); //SUB SELECT @@ -362,9 +355,46 @@ public class DataUsageList extends DataUsageBaseFragment } // generate cycle list based on policy and available history - if (mCycleAdapter.updateCycleList(mCycleData)) { - updateDetailData(); + mCycleAdapter.updateCycleList(mCycleData); + updateSelectedCycle(); + } + + /** + * Updates the chart and detail data when initial loaded or selected cycle changed. + */ + private void updateSelectedCycle() { + // Avoid from updating UI after #onStop. + if (!getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.STARTED)) { + return; } + + // Avoid from updating UI when async query still on-going. + // This could happen when a request from #onMobileDataEnabledChange. + if (mCycleData == null) { + return; + } + + final int position = mCycleSpinner.getSelectedItemPosition(); + if (mCycleAdapter.getCount() == 0 || position < 0) { + return; + } + final CycleAdapter.CycleItem cycle = mCycleAdapter.getItem(position); + if (Objects.equals(cycle, mLastDisplayedCycle)) { + // Avoid duplicate update to avoid page flash. + return; + } + mLastDisplayedCycle = cycle; + + if (LOGD) { + Log.d(TAG, "showing cycle " + cycle + ", [start=" + cycle.start + ", end=" + + cycle.end + "]"); + } + + // update chart to show selected cycle, and update detail data + // to match updated sweep bounds. + mChart.setNetworkCycleData(mCycleData.get(position)); + + updateDetailData(); } /** @@ -554,33 +584,10 @@ public class DataUsageList extends DataUsageBaseFragment return Math.max(largest, item.total); } - private OnItemSelectedListener mCycleListener = new OnItemSelectedListener() { + private final OnItemSelectedListener mCycleListener = new OnItemSelectedListener() { @Override public void onItemSelected(AdapterView parent, View view, int position, long id) { - final CycleAdapter.CycleItem cycle = (CycleAdapter.CycleItem) - mCycleSpinner.getSelectedItem(); - - if (LOGD) { - Log.d(TAG, "showing cycle " + cycle + ", start=" + cycle.start + ", end=" - + cycle.end + "]"); - } - - // Avoid from updating UI after #onStop. - if (!getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.STARTED)) { - return; - } - - // Avoid from updating UI when async query still on-going. - // This could happen when a request from #onMobileDataEnabledChange. - if (mCycleData == null) { - return; - } - - // update chart to show selected cycle, and update detail data - // to match updated sweep bounds. - mChart.setNetworkCycleData(mCycleData.get(position)); - - updateDetailData(); + updateSelectedCycle(); } @Override @@ -602,13 +609,11 @@ public class DataUsageList extends DataUsageBaseFragment @Override public void onLoadFinished(Loader> loader, List data) { - onEndOfLoading(); - if (mLoadingViewController != null) { - mLoadingViewController.showContent(false /* animate */); - } + mLoadingViewController.showContent(false /* animate */); mCycleData = data; // calculate policy cycles based on available data updatePolicy(); + mCycleSpinner.setVisibility(View.VISIBLE); } @Override diff --git a/src/com/android/settings/datausage/SpinnerPreference.java b/src/com/android/settings/datausage/SpinnerPreference.java index c4b7a4e18da..c6b5f9f8ecc 100644 --- a/src/com/android/settings/datausage/SpinnerPreference.java +++ b/src/com/android/settings/datausage/SpinnerPreference.java @@ -14,6 +14,7 @@ package com.android.settings.datausage; +import android.annotation.Nullable; import android.content.Context; import android.util.AttributeSet; import android.view.View; @@ -28,6 +29,7 @@ import com.android.settings.R; public class SpinnerPreference extends Preference implements CycleAdapter.SpinnerInterface { private CycleAdapter mAdapter; + @Nullable private AdapterView.OnItemSelectedListener mListener; private Object mCurrentObject; private int mPosition; @@ -88,19 +90,24 @@ public class SpinnerPreference extends Preference implements CycleAdapter.Spinne view.findViewById(R.id.cycles_spinner).performClick(); } - private final AdapterView.OnItemSelectedListener mOnSelectedListener - = new AdapterView.OnItemSelectedListener() { - @Override - public void onItemSelected(AdapterView parent, View view, int position, long id) { - if (mPosition == position) return; - mPosition = position; - mCurrentObject = mAdapter.getItem(position); - mListener.onItemSelected(parent, view, position, id); - } + private final AdapterView.OnItemSelectedListener mOnSelectedListener = + new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected( + AdapterView parent, View view, int position, long id) { + if (mPosition == position) return; + mPosition = position; + mCurrentObject = mAdapter.getItem(position); + if (mListener != null) { + mListener.onItemSelected(parent, view, position, id); + } + } - @Override - public void onNothingSelected(AdapterView parent) { - mListener.onNothingSelected(parent); - } - }; + @Override + public void onNothingSelected(AdapterView parent) { + if (mListener != null) { + mListener.onNothingSelected(parent); + } + } + }; } diff --git a/tests/robotests/src/com/android/settings/datausage/ChartDataUsagePreferenceTest.java b/tests/robotests/src/com/android/settings/datausage/ChartDataUsagePreferenceTest.java index 3be652dbdde..aab67be1de9 100644 --- a/tests/robotests/src/com/android/settings/datausage/ChartDataUsagePreferenceTest.java +++ b/tests/robotests/src/com/android/settings/datausage/ChartDataUsagePreferenceTest.java @@ -17,10 +17,7 @@ package com.android.settings.datausage; import static com.google.common.truth.Truth.assertThat; -import static org.mockito.Mockito.any; -import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; @@ -166,32 +163,23 @@ public class ChartDataUsagePreferenceTest { @Test public void notifyChange_nonEmptyDataUsage_shouldHaveSingleContentDescription() { - final ArgumentCaptor contentCaptor = - ArgumentCaptor.forClass(CharSequence.class); - final UsageView chart = mock(UsageView.class); - doReturn(chart).when(mHolder).findViewById(R.id.data_usage); - final TextView labelTop = mock(TextView.class); - doReturn(labelTop).when(mHolder).findViewById(R.id.label_top); - final TextView labelMiddle = mock(TextView.class); - doReturn(labelMiddle).when(mHolder).findViewById(R.id.label_middle); - final TextView labelBottom = mock(TextView.class); - doReturn(labelBottom).when(mHolder).findViewById(R.id.label_bottom); - final TextView labelStart = mock(TextView.class); - doReturn(labelStart).when(mHolder).findViewById(R.id.label_start); - final TextView labelEnd = mock(TextView.class); - doReturn(labelEnd).when(mHolder).findViewById(R.id.label_end); + final UsageView chart = (UsageView) mHolder.findViewById(R.id.data_usage); + final TextView labelTop = (TextView) mHolder.findViewById(R.id.label_top); + final TextView labelMiddle = (TextView) mHolder.findViewById(R.id.label_middle); + final TextView labelBottom = (TextView) mHolder.findViewById(R.id.label_bottom); + final TextView labelStart = (TextView) mHolder.findViewById(R.id.label_start); + final TextView labelEnd = (TextView) mHolder.findViewById(R.id.label_end); createTestNetworkData(); - - mPreference.onBindViewHolder(mHolder); mPreference.setNetworkCycleData(mNetworkCycleChartData); - verify(chart).setContentDescription(contentCaptor.capture()); - assertThat(contentCaptor.getValue()).isNotNull(); - verify(labelTop, never()).setContentDescription(any()); - verify(labelMiddle, never()).setContentDescription(any()); - verify(labelBottom, never()).setContentDescription(any()); - verify(labelStart, never()).setContentDescription(any()); - verify(labelEnd, never()).setContentDescription(any()); + mPreference.onBindViewHolder(mHolder); + + assertThat(chart.getContentDescription()).isNotNull(); + assertThat(labelTop.getContentDescription()).isNull(); + assertThat(labelMiddle.getContentDescription()).isNull(); + assertThat(labelBottom.getContentDescription()).isNull(); + assertThat(labelStart.getContentDescription()).isNull(); + assertThat(labelEnd.getContentDescription()).isNull(); } @Test diff --git a/tests/robotests/src/com/android/settings/datausage/DataUsageListTest.java b/tests/robotests/src/com/android/settings/datausage/DataUsageListTest.java index f7db0d56db4..951829b1738 100644 --- a/tests/robotests/src/com/android/settings/datausage/DataUsageListTest.java +++ b/tests/robotests/src/com/android/settings/datausage/DataUsageListTest.java @@ -103,6 +103,7 @@ public class DataUsageListTest { mMobileDataEnabledListener); ReflectionHelpers.setField(mDataUsageList, "services", mNetworkServices); doReturn(mLoaderManager).when(mDataUsageList).getLoaderManager(); + mDataUsageList.mLoadingViewController = mock(LoadingViewController.class); } @Test @@ -222,23 +223,30 @@ public class DataUsageListTest { } @Test - public void onViewCreated_shouldNotSetCycleSpinner() { - View view = constructRootView(); + public void onViewCreated_shouldHideCycleSpinner() { + final View view = new View(mActivity); + final View header = getHeader(); + final Spinner spinner = getSpinner(header); + spinner.setVisibility(View.VISIBLE); + doReturn(header).when(mDataUsageList).setPinnedHeaderView(anyInt()); + doReturn(view).when(mDataUsageList).getView(); mDataUsageList.onViewCreated(view, null); - assertThat(getSpinner(view)).isNull(); + assertThat(spinner.getVisibility()).isEqualTo(View.GONE); } @Test - public void onLoadFinished_networkCycleDataCallback_shouldSetCycleSpinner() { - final View view = constructRootView(); + public void onLoadFinished_networkCycleDataCallback_shouldShowCycleSpinner() { + final Spinner spinner = getSpinner(getHeader()); + spinner.setVisibility(View.INVISIBLE); + mDataUsageList.mCycleSpinner = spinner; + assertThat(spinner.getVisibility()).isEqualTo(View.INVISIBLE); doNothing().when(mDataUsageList).updatePolicy(); - doReturn(setupHeaderView(view)).when(mDataUsageList).setPinnedHeaderView(anyInt()); mDataUsageList.mNetworkCycleDataCallbacks.onLoadFinished(null, null); - assertThat(getSpinner(view)).isNotNull(); + assertThat(spinner.getVisibility()).isEqualTo(View.VISIBLE); } @Test @@ -249,25 +257,19 @@ public class DataUsageListTest { verify(mLoaderManager).destroyLoader(DataUsageList.LOADER_SUMMARY); } - private View constructRootView() { - View rootView = LayoutInflater.from(mActivity) + private View getHeader() { + final View rootView = LayoutInflater.from(mActivity) .inflate(R.layout.preference_list_fragment, null, false); - return rootView; - } - - private View setupHeaderView(View rootView) { - final FrameLayout pinnedHeader = (FrameLayout) rootView.findViewById(R.id.pinned_header); + final FrameLayout pinnedHeader = rootView.findViewById(R.id.pinned_header); final View header = mActivity.getLayoutInflater() - .inflate(R.layout.apps_filter_spinner, pinnedHeader, true); + .inflate(R.layout.apps_filter_spinner, pinnedHeader, false); + return header; } - private Spinner getSpinner(View rootView) { - final FrameLayout pinnedHeader = (FrameLayout) rootView.findViewById(R.id.pinned_header); - if (pinnedHeader == null) { - return null; - } - return pinnedHeader.findViewById(R.id.filter_spinner); + private Spinner getSpinner(View header) { + final Spinner spinner = header.findViewById(R.id.filter_spinner); + return spinner; } @Implements(DataUsageBaseFragment.class)