Fix flicker for Mobile data & Wi-Fi page

Both "Mobile data usage" & "Non-carrier data usage".

By,
1. Add header in onCreate() instead of onViewCreated()
2. Keep the space for Spinner, and preload initial cycles
3. Keep the space for 3 usage summaries

Bug: 191730864
Test: manual
Change-Id: I8c309c5f51ce6290383a2d10f75e41d0f207d61a
This commit is contained in:
Chaohui Wang
2022-04-28 19:09:02 +08:00
parent 3663986909
commit ddedb31f6e
5 changed files with 53 additions and 160 deletions

View File

@@ -21,8 +21,7 @@
android:title="@string/data_usage_app_summary_title">
<com.android.settings.datausage.SpinnerPreference
android:key="cycle"
settings:isPreferenceVisible="false" />
android:key="cycle" />
<PreferenceCategory
android:key="app_data_usage_summary_category">
@@ -31,19 +30,22 @@
android:key="total_usage"
android:title="@string/total_size_label"
android:selectable="false"
android:layout="@layout/horizontal_preference" />
android:layout="@layout/horizontal_preference"
android:summary="@string/summary_placeholder" />
<Preference
android:key="foreground_usage"
android:title="@string/data_usage_label_foreground"
android:selectable="false"
android:layout="@layout/horizontal_preference" />
android:layout="@layout/horizontal_preference"
android:summary="@string/summary_placeholder" />
<Preference
android:key="background_usage"
android:title="@string/data_usage_label_background"
android:selectable="false"
android:layout="@layout/horizontal_preference" />
android:layout="@layout/horizontal_preference"
android:summary="@string/summary_placeholder" />
</PreferenceCategory>

View File

@@ -144,8 +144,7 @@ public class AppDataUsage extends DataUsageBaseFragment implements OnPreferenceC
mForegroundUsage = findPreference(KEY_FOREGROUND_USAGE);
mBackgroundUsage = findPreference(KEY_BACKGROUND_USAGE);
mCycle = findPreference(KEY_CYCLE);
mCycleAdapter = new CycleAdapter(mContext, mCycle, mCycleListener);
initCycle();
final UidDetailProvider uidDetailProvider = getUidDetailProvider();
@@ -211,6 +210,8 @@ public class AppDataUsage extends DataUsageBaseFragment implements OnPreferenceC
removePreference(KEY_RESTRICT_BACKGROUND);
removePreference(KEY_APP_LIST);
}
addEntityHeader();
}
@Override
@@ -276,6 +277,17 @@ public class AppDataUsage extends DataUsageBaseFragment implements OnPreferenceC
return new UidDetailProvider(mContext);
}
private void initCycle() {
mCycle = findPreference(KEY_CYCLE);
mCycleAdapter = new CycleAdapter(mContext, mCycle, mCycleListener);
if (mCycles != null) {
// If coming from a page like DataUsageList where already has a selected cycle, display
// that before loading to reduce flicker.
mCycleAdapter.setInitialCycleList(mCycles, mSelectedCycle);
mCycle.setHasCycles(true);
}
}
private void updatePrefs(boolean restrictBackground, boolean unrestrictData) {
final EnforcedAdmin admin = RestrictedLockUtilsInternal.checkIfMeteredDataRestricted(
mContext, mPackageName, UserHandle.getUserId(mAppItem.key));
@@ -308,9 +320,9 @@ public class AppDataUsage extends DataUsageBaseFragment implements OnPreferenceC
final long backgroundBytes, foregroundBytes;
if (mUsageData == null || position >= mUsageData.size()) {
backgroundBytes = foregroundBytes = 0;
mCycle.setVisible(false);
mCycle.setHasCycles(false);
} else {
mCycle.setVisible(true);
mCycle.setHasCycles(true);
final NetworkCycleDataForUid data = mUsageData.get(position);
backgroundBytes = data.getBackgroudUsage();
foregroundBytes = data.getForegroudUsage();
@@ -335,10 +347,8 @@ public class AppDataUsage extends DataUsageBaseFragment implements OnPreferenceC
return false;
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
@VisibleForTesting
void addEntityHeader() {
String pkg = mPackages.size() != 0 ? mPackages.valueAt(0) : null;
int uid = 0;
if (pkg != null) {

View File

@@ -13,24 +13,13 @@
*/
package com.android.settings.datausage;
import android.annotation.NonNull;
import android.app.usage.NetworkStats;
import android.content.Context;
import android.net.NetworkPolicy;
import android.net.NetworkPolicyManager;
import android.text.format.DateUtils;
import android.util.Pair;
import android.util.Range;
import android.widget.AdapterView;
import com.android.net.module.util.NetworkStatsUtils;
import com.android.settings.Utils;
import com.android.settingslib.net.ChartData;
import com.android.settingslib.net.NetworkCycleData;
import com.android.settingslib.widget.SettingsSpinnerAdapter;
import java.time.ZonedDateTime;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
@@ -45,7 +34,6 @@ public class CycleAdapter extends SettingsSpinnerAdapter<CycleAdapter.CycleItem>
mSpinner = spinner;
mListener = listener;
mSpinner.setAdapter(this);
mSpinner.setOnItemSelectedListener(mListener);
}
/**
@@ -65,128 +53,14 @@ public class CycleAdapter extends SettingsSpinnerAdapter<CycleAdapter.CycleItem>
return 0;
}
protected static long getTotalBytesForTimeRange(List<NetworkStats.Bucket> stats,
Range<Long> range) {
long bytes = 0L;
for (NetworkStats.Bucket bucket : stats) {
final Range<Long> bucketSpan = new Range<>(
bucket.getStartTimeStamp(), bucket.getEndTimeStamp());
// Only record bytes that overlapped with the given time range. For partially
// overlapped bucket, record rational bytes assuming the traffic is uniform
// distributed within the bucket.
try {
final Range<Long> overlapped = range.intersect(bucketSpan);
final long totalOfBucket = bucket.getRxBytes() + bucket.getTxBytes();
bytes += NetworkStatsUtils.multiplySafeByRational(totalOfBucket,
overlapped.getUpper() - overlapped.getLower(),
bucketSpan.getUpper() - bucketSpan.getLower());
} catch (IllegalArgumentException e) {
// Range disjoint, ignore.
continue;
}
}
return bytes;
}
@NonNull
private Range getTimeRangeOf(@NonNull List<NetworkStats.Bucket> stats) {
long start = Long.MAX_VALUE;
long end = Long.MIN_VALUE;
for (NetworkStats.Bucket bucket : stats) {
start = Math.min(start, bucket.getStartTimeStamp());
end = Math.max(end, bucket.getEndTimeStamp());
}
return new Range(start, end);
}
/**
* Rebuild list based on {@link NetworkPolicy} and available
* {@link List<NetworkStats.Bucket>} data. Always selects the newest item,
* updating the inspection range on chartData.
*/
@Deprecated
public boolean updateCycleList(NetworkPolicy policy, ChartData chartData) {
// stash away currently selected cycle to try restoring below
final CycleAdapter.CycleItem previousItem = (CycleAdapter.CycleItem)
mSpinner.getSelectedItem();
void setInitialCycleList(List<Long> cycles, long selectedCycle) {
clear();
final Context context = getContext();
long historyStart;
long historyEnd;
try {
final Range<Long> historyTimeRange = getTimeRangeOf(chartData.network);
historyStart = historyTimeRange.getLower();
historyEnd = historyTimeRange.getUpper();
} catch (IllegalArgumentException e) {
// Empty history.
final long now = System.currentTimeMillis();
historyStart = now;
historyEnd = now + 1;
}
boolean hasCycles = false;
if (policy != null) {
final Iterator<Pair<ZonedDateTime, ZonedDateTime>> it = NetworkPolicyManager
.cycleIterator(policy);
while (it.hasNext()) {
final Pair<ZonedDateTime, ZonedDateTime> cycle = it.next();
final long cycleStart = cycle.first.toInstant().toEpochMilli();
final long cycleEnd = cycle.second.toInstant().toEpochMilli();
final boolean includeCycle;
if (chartData != null) {
final long bytesInCycle = getTotalBytesForTimeRange(chartData.network,
new Range<>(cycleStart, cycleEnd));
includeCycle = bytesInCycle > 0;
} else {
includeCycle = true;
}
if (includeCycle) {
add(new CycleAdapter.CycleItem(context, cycleStart, cycleEnd));
hasCycles = true;
}
for (int i = 0; i < cycles.size() - 1; i++) {
add(new CycleAdapter.CycleItem(getContext(), cycles.get(i + 1), cycles.get(i)));
if (cycles.get(i) == selectedCycle) {
mSpinner.setSelection(i);
}
}
if (!hasCycles) {
// no policy defined cycles; show entry for each four-week period
long cycleEnd = historyEnd;
while (cycleEnd > historyStart) {
final long cycleStart = cycleEnd - (DateUtils.WEEK_IN_MILLIS * 4);
final boolean includeCycle;
if (chartData != null) {
final long bytesInCycle = getTotalBytesForTimeRange(chartData.network,
new Range<>(cycleStart, cycleEnd));
includeCycle = bytesInCycle > 0;
} else {
includeCycle = true;
}
if (includeCycle) {
add(new CycleAdapter.CycleItem(context, cycleStart, cycleEnd));
}
cycleEnd = cycleStart;
}
}
// force pick the current cycle (first item)
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;
}
/**
@@ -194,6 +68,7 @@ public class CycleAdapter extends SettingsSpinnerAdapter<CycleAdapter.CycleItem>
* updating the inspection range on chartData.
*/
public boolean updateCycleList(List<? extends NetworkCycleData> cycleData) {
mSpinner.setOnItemSelectedListener(mListener);
// stash away currently selected cycle to try restoring below
final CycleAdapter.CycleItem previousItem = (CycleAdapter.CycleItem)
mSpinner.getSelectedItem();
@@ -228,10 +103,6 @@ public class CycleAdapter extends SettingsSpinnerAdapter<CycleAdapter.CycleItem>
public long start;
public long end;
public CycleItem(CharSequence label) {
this.label = label;
}
public CycleItem(Context context, long start, long end) {
this.label = Utils.formatDateRange(context, start, end);
this.start = start;

View File

@@ -31,6 +31,8 @@ public class SpinnerPreference extends Preference implements CycleAdapter.Spinne
private AdapterView.OnItemSelectedListener mListener;
private Object mCurrentObject;
private int mPosition;
private View mItemView;
private boolean mItemViewVisible = false;
public SpinnerPreference(Context context, AttributeSet attrs) {
super(context, attrs);
@@ -63,12 +65,24 @@ public class SpinnerPreference extends Preference implements CycleAdapter.Spinne
@Override
public void onBindViewHolder(PreferenceViewHolder holder) {
super.onBindViewHolder(holder);
mItemView = holder.itemView;
mItemView.setVisibility(mItemViewVisible ? View.VISIBLE : View.INVISIBLE);
Spinner spinner = (Spinner) holder.findViewById(R.id.cycles_spinner);
spinner.setAdapter(mAdapter);
spinner.setSelection(mPosition);
spinner.setOnItemSelectedListener(mOnSelectedListener);
}
void setHasCycles(boolean hasData) {
setVisible(hasData);
if (hasData) {
mItemViewVisible = true;
if (mItemView != null) {
mItemView.setVisibility(View.VISIBLE);
}
}
}
@Override
protected void performClick(View view) {
view.findViewById(R.id.cycles_spinner).performClick();

View File

@@ -43,7 +43,6 @@ import android.os.Process;
import android.telephony.SubscriptionManager;
import android.text.format.DateUtils;
import android.util.ArraySet;
import android.view.View;
import androidx.fragment.app.FragmentActivity;
import androidx.preference.Preference;
@@ -96,6 +95,10 @@ public class AppDataUsageTest {
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
ShadowEntityHeaderController.setUseMock(mHeaderController);
when(mHeaderController.setRecyclerView(any(), any())).thenReturn(mHeaderController);
when(mHeaderController.setUid(anyInt())).thenReturn(mHeaderController);
}
@After
@@ -163,10 +166,6 @@ public class AppDataUsageTest {
@Test
public void bindAppHeader_allWorkApps_shouldNotShowAppInfoLink() {
ShadowEntityHeaderController.setUseMock(mHeaderController);
when(mHeaderController.setRecyclerView(any(), any())).thenReturn(mHeaderController);
when(mHeaderController.setUid(anyInt())).thenReturn(mHeaderController);
mFragment = spy(new AppDataUsage());
when(mFragment.getPreferenceManager())
@@ -174,7 +173,7 @@ public class AppDataUsageTest {
doReturn(mock(PreferenceScreen.class)).when(mFragment).getPreferenceScreen();
ReflectionHelpers.setField(mFragment, "mAppItem", mock(AppItem.class));
mFragment.onViewCreated(new View(RuntimeEnvironment.application), new Bundle());
mFragment.addEntityHeader();
verify(mHeaderController).setHasAppInfoLink(false);
}
@@ -196,16 +195,13 @@ public class AppDataUsageTest {
when(mPackageManager.getPackageUidAsUser(anyString(), anyInt()))
.thenReturn(fakeUserId);
ShadowEntityHeaderController.setUseMock(mHeaderController);
when(mHeaderController.setRecyclerView(any(), any())).thenReturn(mHeaderController);
when(mHeaderController.setUid(fakeUserId)).thenReturn(mHeaderController);
when(mHeaderController.setHasAppInfoLink(anyBoolean())).thenReturn(mHeaderController);
when(mFragment.getPreferenceManager())
.thenReturn(mock(PreferenceManager.class, RETURNS_DEEP_STUBS));
doReturn(mock(PreferenceScreen.class)).when(mFragment).getPreferenceScreen();
mFragment.onViewCreated(new View(RuntimeEnvironment.application), new Bundle());
mFragment.addEntityHeader();
verify(mHeaderController).setHasAppInfoLink(true);
verify(mHeaderController).setUid(fakeUserId);
@@ -268,7 +264,7 @@ public class AppDataUsageTest {
mFragment.bindData(0 /* position */);
verify(cycle).setVisible(false);
verify(cycle).setHasCycles(false);
}
@Test
@@ -293,7 +289,7 @@ public class AppDataUsageTest {
mFragment.bindData(0 /* position */);
verify(cycle).setVisible(true);
verify(cycle).setHasCycles(true);
verify(totalPref).setSummary(
DataUsageUtils.formatDataUsage(context, backgroundBytes + foregroundBytes));
verify(backgroundPref).setSummary(DataUsageUtils.formatDataUsage(context, backgroundBytes));