[DataUsage] Adjusting the control of display sequence

Showing a usage graph before end of statistic would lead to incorrect
height of usage graph, and another update would lead to layout moved a
little bit.

This change tries to improve it through:
1. Start the loading animation earlier, and stop animation when
   statistics loaded.
   (Only effective when UI re-create.)
2. Update the UI only when statistics are ready.

Bug: 187019210
Test: robotest ChartDataUsagePreferenceTest DataUsageListTest
Change-Id: Ic83f2422b6c6d55948110d652ee24234f43b6445
This commit is contained in:
Bonian Chen
2022-03-14 09:45:33 +08:00
parent ec9cee3c27
commit b8491032d7
4 changed files with 102 additions and 56 deletions

View File

@@ -48,8 +48,8 @@ public class ChartDataUsagePreference extends Preference {
// Set to half a meg for now. // Set to half a meg for now.
private static final long RESOLUTION = DataUnit.MEBIBYTES.toBytes(1) / 2; private static final long RESOLUTION = DataUnit.MEBIBYTES.toBytes(1) / 2;
private final int mWarningColor; private int mWarningColor;
private final int mLimitColor; private int mLimitColor;
private Resources mResources; private Resources mResources;
private NetworkPolicy mPolicy; private NetworkPolicy mPolicy;
@@ -58,24 +58,25 @@ public class ChartDataUsagePreference extends Preference {
private NetworkCycleChartData mNetworkCycleChartData; private NetworkCycleChartData mNetworkCycleChartData;
private int mSecondaryColor; private int mSecondaryColor;
private int mSeriesColor; private int mSeriesColor;
private UsageView mUsageView;
private boolean mSuspendUiUpdate; // Suppress UI updates to save some CPU time.
public ChartDataUsagePreference(Context context, AttributeSet attrs) { public ChartDataUsagePreference(Context context, AttributeSet attrs) {
super(context, attrs); super(context, attrs);
mResources = context.getResources();
setSelectable(false); 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 @Override
public void onBindViewHolder(PreferenceViewHolder holder) { public void onBindViewHolder(PreferenceViewHolder holder) {
super.onBindViewHolder(holder); super.onBindViewHolder(holder);
final UsageView chart = (UsageView) holder.findViewById(R.id.data_usage); mUsageView = (UsageView) holder.findViewById(R.id.data_usage);
if (mNetworkCycleChartData == null) {
return;
} }
private void onUpdateView() {
UsageView chart = mUsageView;
if ((chart == null) || (mNetworkCycleChartData == null)) {
return;
}
final int top = getTop(); final int top = getTop();
chart.clearPaths(); chart.clearPaths();
chart.configureGraph(toInt(mEnd - mStart), top); chart.configureGraph(toInt(mEnd - mStart), top);
@@ -291,10 +292,17 @@ public class ChartDataUsagePreference extends Preference {
return new SpannableStringBuilder().append(label, new ForegroundColorSpan(mLimitColor), 0); return new SpannableStringBuilder().append(label, new ForegroundColorSpan(mLimitColor), 0);
} }
public void onPreparingChartData() {
mSuspendUiUpdate = true;
}
public void setNetworkPolicy(NetworkPolicy policy) { public void setNetworkPolicy(NetworkPolicy policy) {
mPolicy = policy; mPolicy = policy;
if ((!mSuspendUiUpdate) && (mResources != null)) {
onUpdateView();
notifyChanged(); notifyChanged();
} }
}
public long getInspectStart() { public long getInspectStart() {
return mStart; return mStart;
@@ -305,15 +313,31 @@ public class ChartDataUsagePreference extends Preference {
} }
public void setNetworkCycleData(NetworkCycleChartData data) { public void setNetworkCycleData(NetworkCycleChartData data) {
if (data == null) {
return;
}
mNetworkCycleChartData = data; mNetworkCycleChartData = data;
mStart = data.getStartTime(); mStart = data.getStartTime();
mEnd = data.getEndTime(); 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(); notifyChanged();
mSuspendUiUpdate = false;
} }
public void setColors(int seriesColor, int secondaryColor) { public void setColors(int seriesColor, int secondaryColor) {
mSeriesColor = seriesColor; mSeriesColor = seriesColor;
mSecondaryColor = secondaryColor; mSecondaryColor = secondaryColor;
if ((!mSuspendUiUpdate) && (mResources != null)) {
onUpdateView();
notifyChanged(); notifyChanged();
} }
} }
}

View File

@@ -151,7 +151,20 @@ public class DataUsageList extends DataUsageBaseFragment
public void onViewCreated(View v, Bundle savedInstanceState) { public void onViewCreated(View v, Bundle savedInstanceState) {
super.onViewCreated(v, 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); mHeader = setPinnedHeaderView(R.layout.apps_filter_spinner);
mCycleSpinner = mHeader.findViewById(R.id.filter_spinner);
mHeader.findViewById(R.id.filter_settings).setOnClickListener(btn -> { mHeader.findViewById(R.id.filter_settings).setOnClickListener(btn -> {
final Bundle args = new Bundle(); final Bundle args = new Bundle();
args.putParcelable(DataUsageList.EXTRA_NETWORK_TEMPLATE, mTemplate); args.putParcelable(DataUsageList.EXTRA_NETWORK_TEMPLATE, mTemplate);
@@ -162,8 +175,6 @@ public class DataUsageList extends DataUsageBaseFragment
.setArguments(args) .setArguments(args)
.launch(); .launch();
}); });
mCycleSpinner = mHeader.findViewById(R.id.filter_spinner);
mCycleSpinner.setVisibility(View.GONE);
mCycleAdapter = new CycleAdapter(mCycleSpinner.getContext(), new SpinnerInterface() { mCycleAdapter = new CycleAdapter(mCycleSpinner.getContext(), new SpinnerInterface() {
@Override @Override
public void setAdapter(CycleAdapter cycleAdapter) { public void setAdapter(CycleAdapter cycleAdapter) {
@@ -195,10 +206,6 @@ public class DataUsageList extends DataUsageBaseFragment
super.sendAccessibilityEvent(host, eventType); super.sendAccessibilityEvent(host, eventType);
} }
}); });
mLoadingViewController = new LoadingViewController(
getView().findViewById(R.id.loading_container), getListView());
mLoadingViewController.showLoadingViewDelayed();
} }
@Override @Override
@@ -206,6 +213,10 @@ public class DataUsageList extends DataUsageBaseFragment
super.onResume(); super.onResume();
mDataStateListener.start(mSubId); mDataStateListener.start(mSubId);
if (mChart != null) {
mChart.onPreparingChartData();
}
// kick off loader for network history // kick off loader for network history
// TODO: consider chaining two loaders together instead of reloading // TODO: consider chaining two loaders together instead of reloading
// network history when showing app detail. // network history when showing app detail.
@@ -526,11 +537,13 @@ public class DataUsageList extends DataUsageBaseFragment
@Override @Override
public void onLoadFinished(Loader<List<NetworkCycleChartData>> loader, public void onLoadFinished(Loader<List<NetworkCycleChartData>> loader,
List<NetworkCycleChartData> data) { List<NetworkCycleChartData> data) {
onEndOfLoading();
if (mLoadingViewController != null) {
mLoadingViewController.showContent(false /* animate */); mLoadingViewController.showContent(false /* animate */);
}
mCycleData = data; mCycleData = data;
// calculate policy cycles based on available data // calculate policy cycles based on available data
updatePolicy(); updatePolicy();
mCycleSpinner.setVisibility(View.VISIBLE);
} }
@Override @Override

View File

@@ -17,7 +17,10 @@ package com.android.settings.datausage;
import static com.google.common.truth.Truth.assertThat; 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.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy; import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
@@ -163,23 +166,32 @@ public class ChartDataUsagePreferenceTest {
@Test @Test
public void notifyChange_nonEmptyDataUsage_shouldHaveSingleContentDescription() { public void notifyChange_nonEmptyDataUsage_shouldHaveSingleContentDescription() {
final UsageView chart = (UsageView) mHolder.findViewById(R.id.data_usage); final ArgumentCaptor<CharSequence> contentCaptor =
final TextView labelTop = (TextView) mHolder.findViewById(R.id.label_top); ArgumentCaptor.forClass(CharSequence.class);
final TextView labelMiddle = (TextView) mHolder.findViewById(R.id.label_middle); final UsageView chart = mock(UsageView.class);
final TextView labelBottom = (TextView) mHolder.findViewById(R.id.label_bottom); doReturn(chart).when(mHolder).findViewById(R.id.data_usage);
final TextView labelStart = (TextView) mHolder.findViewById(R.id.label_start); final TextView labelTop = mock(TextView.class);
final TextView labelEnd = (TextView) mHolder.findViewById(R.id.label_end); 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);
createTestNetworkData(); createTestNetworkData();
mPreference.setNetworkCycleData(mNetworkCycleChartData);
mPreference.onBindViewHolder(mHolder); mPreference.onBindViewHolder(mHolder);
mPreference.setNetworkCycleData(mNetworkCycleChartData);
assertThat(chart.getContentDescription()).isNotNull(); verify(chart).setContentDescription(contentCaptor.capture());
assertThat(labelTop.getContentDescription()).isNull(); assertThat(contentCaptor.getValue()).isNotNull();
assertThat(labelMiddle.getContentDescription()).isNull(); verify(labelTop, never()).setContentDescription(any());
assertThat(labelBottom.getContentDescription()).isNull(); verify(labelMiddle, never()).setContentDescription(any());
assertThat(labelStart.getContentDescription()).isNull(); verify(labelBottom, never()).setContentDescription(any());
assertThat(labelEnd.getContentDescription()).isNull(); verify(labelStart, never()).setContentDescription(any());
verify(labelEnd, never()).setContentDescription(any());
} }
@Test @Test

View File

@@ -192,32 +192,23 @@ public class DataUsageListTest {
} }
@Test @Test
public void onViewCreated_shouldHideCycleSpinner() { public void onViewCreated_shouldNotSetCycleSpinner() {
final View view = new View(mActivity); View view = constructRootView();
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); mDataUsageList.onViewCreated(view, null);
assertThat(spinner.getVisibility()).isEqualTo(View.GONE); assertThat(getSpinner(view)).isNull();
} }
@Test @Test
public void onLoadFinished_networkCycleDataCallback_shouldShowCycleSpinner() { public void onLoadFinished_networkCycleDataCallback_shouldSetCycleSpinner() {
final LoadingViewController loadingViewController = mock(LoadingViewController.class); final View view = constructRootView();
mDataUsageList.mLoadingViewController = loadingViewController;
final Spinner spinner = getSpinner(getHeader());
spinner.setVisibility(View.INVISIBLE);
mDataUsageList.mCycleSpinner = spinner;
assertThat(spinner.getVisibility()).isEqualTo(View.INVISIBLE);
doNothing().when(mDataUsageList).updatePolicy(); doNothing().when(mDataUsageList).updatePolicy();
doReturn(setupHeaderView(view)).when(mDataUsageList).setPinnedHeaderView(anyInt());
mDataUsageList.mNetworkCycleDataCallbacks.onLoadFinished(null, null); mDataUsageList.mNetworkCycleDataCallbacks.onLoadFinished(null, null);
assertThat(spinner.getVisibility()).isEqualTo(View.VISIBLE); assertThat(getSpinner(view)).isNotNull();
} }
@Test @Test
@@ -228,18 +219,24 @@ public class DataUsageListTest {
verify(mLoaderManager).destroyLoader(DataUsageList.LOADER_SUMMARY); verify(mLoaderManager).destroyLoader(DataUsageList.LOADER_SUMMARY);
} }
private View getHeader() { private View constructRootView() {
final View rootView = LayoutInflater.from(mActivity) View rootView = LayoutInflater.from(mActivity)
.inflate(R.layout.preference_list_fragment, null, false); .inflate(R.layout.preference_list_fragment, null, false);
final FrameLayout pinnedHeader = rootView.findViewById(R.id.pinned_header); return rootView;
final View header = mActivity.getLayoutInflater() }
.inflate(R.layout.apps_filter_spinner, pinnedHeader, false);
private View setupHeaderView(View rootView) {
final FrameLayout pinnedHeader = (FrameLayout) rootView.findViewById(R.id.pinned_header);
final View header = mActivity.getLayoutInflater()
.inflate(R.layout.apps_filter_spinner, pinnedHeader, true);
return header; return header;
} }
private Spinner getSpinner(View header) { private Spinner getSpinner(View rootView) {
final Spinner spinner = header.findViewById(R.id.filter_spinner); final FrameLayout pinnedHeader = (FrameLayout) rootView.findViewById(R.id.pinned_header);
return spinner; if (pinnedHeader == null) {
return null;
}
return pinnedHeader.findViewById(R.id.filter_spinner);
} }
} }