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
diff --git a/res/xml/app_data_usage.xml b/res/xml/app_data_usage.xml
index e64a1c5..e94c4ff 100644
--- a/res/xml/app_data_usage.xml
+++ b/res/xml/app_data_usage.xml
@@ -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>
 
diff --git a/src/com/android/settings/datausage/AppDataUsage.java b/src/com/android/settings/datausage/AppDataUsage.java
index 460f390..531a341 100644
--- a/src/com/android/settings/datausage/AppDataUsage.java
+++ b/src/com/android/settings/datausage/AppDataUsage.java
@@ -144,8 +144,7 @@
         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 @@
             removePreference(KEY_RESTRICT_BACKGROUND);
             removePreference(KEY_APP_LIST);
         }
+
+        addEntityHeader();
     }
 
     @Override
@@ -276,6 +277,17 @@
         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 @@
         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 @@
         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) {
diff --git a/src/com/android/settings/datausage/CycleAdapter.java b/src/com/android/settings/datausage/CycleAdapter.java
index 2cabd8d..b41b6aa 100644
--- a/src/com/android/settings/datausage/CycleAdapter.java
+++ b/src/com/android/settings/datausage/CycleAdapter.java
@@ -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 @@
         mSpinner = spinner;
         mListener = listener;
         mSpinner.setAdapter(this);
-        mSpinner.setOnItemSelectedListener(mListener);
     }
 
     /**
@@ -65,128 +53,14 @@
         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 @@
      * 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 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;
diff --git a/src/com/android/settings/datausage/SpinnerPreference.java b/src/com/android/settings/datausage/SpinnerPreference.java
index 867930b..c4b7a4e 100644
--- a/src/com/android/settings/datausage/SpinnerPreference.java
+++ b/src/com/android/settings/datausage/SpinnerPreference.java
@@ -31,6 +31,8 @@
     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 @@
     @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();
diff --git a/tests/robotests/src/com/android/settings/datausage/AppDataUsageTest.java b/tests/robotests/src/com/android/settings/datausage/AppDataUsageTest.java
index 09c5734..f043ec7 100644
--- a/tests/robotests/src/com/android/settings/datausage/AppDataUsageTest.java
+++ b/tests/robotests/src/com/android/settings/datausage/AppDataUsageTest.java
@@ -43,7 +43,6 @@
 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 @@
     @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 @@
 
     @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 @@
         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 @@
         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 @@
 
         mFragment.bindData(0 /* position */);
 
-        verify(cycle).setVisible(false);
+        verify(cycle).setHasCycles(false);
     }
 
     @Test
@@ -293,7 +289,7 @@
 
         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));