Merge "Avoid settingslib.widget.preference.radio" into main
diff --git a/src/com/android/settings/datausage/AppDataUsage.java b/src/com/android/settings/datausage/AppDataUsage.java
index 1645586..c3be0aa 100644
--- a/src/com/android/settings/datausage/AppDataUsage.java
+++ b/src/com/android/settings/datausage/AppDataUsage.java
@@ -310,7 +310,7 @@
 
     private void initCycle() {
         mCycle = findPreference(KEY_CYCLE);
-        mCycleAdapter = new CycleAdapter(mContext, mCycle, mCycleListener);
+        mCycleAdapter = new CycleAdapter(mContext, mCycle);
         if (mCycles != null) {
             // If coming from a page like DataUsageList where already has a selected cycle, display
             // that before loading to reduce flicker.
@@ -435,7 +435,7 @@
         return SettingsEnums.APP_DATA_USAGE;
     }
 
-    private AdapterView.OnItemSelectedListener mCycleListener =
+    private final AdapterView.OnItemSelectedListener mCycleListener =
             new AdapterView.OnItemSelectedListener() {
         @Override
         public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
@@ -471,6 +471,7 @@
                 public void onLoadFinished(@NonNull Loader<List<NetworkCycleDataForUid>> loader,
                         List<NetworkCycleDataForUid> data) {
                     mUsageData = data;
+                    mCycle.setOnItemSelectedListener(mCycleListener);
                     mCycleAdapter.updateCycleList(data);
                     if (mSelectedCycle > 0L) {
                         final int numCycles = data.size();
diff --git a/src/com/android/settings/datausage/ChartDataUsagePreference.java b/src/com/android/settings/datausage/ChartDataUsagePreference.java
index e2a103e..fa467d2 100644
--- a/src/com/android/settings/datausage/ChartDataUsagePreference.java
+++ b/src/com/android/settings/datausage/ChartDataUsagePreference.java
@@ -294,14 +294,6 @@
         notifyChanged();
     }
 
-    public long getInspectStart() {
-        return mStart;
-    }
-
-    public long getInspectEnd() {
-        return mEnd;
-    }
-
     public void setNetworkCycleData(NetworkCycleChartData data) {
         mNetworkCycleChartData = data;
         mStart = data.getStartTime();
diff --git a/src/com/android/settings/datausage/CycleAdapter.java b/src/com/android/settings/datausage/CycleAdapter.java
index 2af4012..90a2035 100644
--- a/src/com/android/settings/datausage/CycleAdapter.java
+++ b/src/com/android/settings/datausage/CycleAdapter.java
@@ -14,7 +14,6 @@
 package com.android.settings.datausage;
 
 import android.content.Context;
-import android.widget.AdapterView;
 
 import com.android.settings.Utils;
 import com.android.settingslib.net.NetworkCycleData;
@@ -25,13 +24,10 @@
 public class CycleAdapter extends SettingsSpinnerAdapter<CycleAdapter.CycleItem> {
 
     private final SpinnerInterface mSpinner;
-    private final AdapterView.OnItemSelectedListener mListener;
 
-    public CycleAdapter(Context context, SpinnerInterface spinner,
-            AdapterView.OnItemSelectedListener listener) {
+    public CycleAdapter(Context context, SpinnerInterface spinner) {
         super(context);
         mSpinner = spinner;
-        mListener = listener;
         mSpinner.setAdapter(this);
     }
 
@@ -67,7 +63,6 @@
      * updating the inspection range on chartData.
      */
     public void 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();
@@ -122,8 +117,6 @@
     public interface SpinnerInterface {
         void setAdapter(CycleAdapter cycleAdapter);
 
-        void setOnItemSelectedListener(AdapterView.OnItemSelectedListener listener);
-
         Object getSelectedItem();
 
         void setSelection(int position);
diff --git a/src/com/android/settings/datausage/DataUsageList.java b/src/com/android/settings/datausage/DataUsageList.java
index 15a5603..f48aacd 100644
--- a/src/com/android/settings/datausage/DataUsageList.java
+++ b/src/com/android/settings/datausage/DataUsageList.java
@@ -28,11 +28,6 @@
 import android.util.EventLog;
 import android.util.Log;
 import android.view.View;
-import android.view.View.AccessibilityDelegate;
-import android.view.accessibility.AccessibilityEvent;
-import android.widget.AdapterView;
-import android.widget.AdapterView.OnItemSelectedListener;
-import android.widget.Spinner;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
@@ -43,8 +38,6 @@
 import androidx.preference.Preference;
 
 import com.android.settings.R;
-import com.android.settings.core.SubSettingLauncher;
-import com.android.settings.datausage.CycleAdapter.SpinnerInterface;
 import com.android.settings.datausage.lib.BillingCycleRepository;
 import com.android.settings.network.MobileDataEnabledListener;
 import com.android.settings.network.MobileNetworkRepository;
@@ -58,6 +51,8 @@
 import java.util.Objects;
 import java.util.Optional;
 
+import kotlin.Unit;
+
 /**
  * Panel showing data usage history across various networks, including options
  * to inspect based on usage cycle and control through {@link NetworkPolicy}.
@@ -90,8 +85,6 @@
     @VisibleForTesting
     int mNetworkType;
     @VisibleForTesting
-    Spinner mCycleSpinner;
-    @VisibleForTesting
     LoadingViewController mLoadingViewController;
 
     private ChartDataUsagePreference mChart;
@@ -102,13 +95,15 @@
     // 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 CycleAdapter mCycleAdapter;
     private Preference mUsageAmount;
-    private View mHeader;
     private MobileNetworkRepository mMobileNetworkRepository;
     private SubscriptionInfoEntity mSubscriptionInfoEntity;
     private DataUsageListAppsController mDataUsageListAppsController;
     private BillingCycleRepository mBillingCycleRepository;
+    @VisibleForTesting
+    DataUsageListHeaderController mDataUsageListHeaderController;
+
+    private boolean mIsBillingCycleModifiable = false;
 
     @Override
     public int getMetricsCategory() {
@@ -158,50 +153,15 @@
     public void onViewCreated(@NonNull View v, Bundle savedInstanceState) {
         super.onViewCreated(v, savedInstanceState);
 
-        mHeader = setPinnedHeaderView(R.layout.apps_filter_spinner);
-        mHeader.findViewById(R.id.filter_settings).setOnClickListener(btn -> {
-            final Bundle args = new Bundle();
-            args.putParcelable(DataUsageList.EXTRA_NETWORK_TEMPLATE, mTemplate);
-            new SubSettingLauncher(getContext())
-                    .setDestination(BillingCycleSettings.class.getName())
-                    .setTitleRes(R.string.billing_cycle)
-                    .setSourceMetricsCategory(getMetricsCategory())
-                    .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) {
-                mCycleSpinner.setAdapter(cycleAdapter);
-            }
-
-            @Override
-            public void setOnItemSelectedListener(OnItemSelectedListener listener) {
-                mCycleSpinner.setOnItemSelectedListener(listener);
-            }
-
-            @Override
-            public Object getSelectedItem() {
-                return mCycleSpinner.getSelectedItem();
-            }
-
-            @Override
-            public void setSelection(int position) {
-                mCycleSpinner.setSelection(position);
-            }
-        }, mCycleListener);
-        mCycleSpinner.setAccessibilityDelegate(new AccessibilityDelegate() {
-            @Override
-            public void sendAccessibilityEvent(View host, int eventType) {
-                if (eventType == AccessibilityEvent.TYPE_VIEW_SELECTED) {
-                    // Ignore TYPE_VIEW_SELECTED or TalkBack will speak for it at onResume.
-                    return;
+        mDataUsageListHeaderController = new DataUsageListHeaderController(
+                setPinnedHeaderView(R.layout.apps_filter_spinner),
+                mTemplate,
+                getMetricsCategory(),
+                (cycle, position) -> {
+                    updateSelectedCycle(cycle, position);
+                    return Unit.INSTANCE;
                 }
-                super.sendAccessibilityEvent(host, eventType);
-            }
-        });
+        );
 
         mLoadingViewController = new LoadingViewController(
                 getView().findViewById(R.id.loading_container), getListView());
@@ -213,6 +173,7 @@
         mLoadingViewController.showLoadingViewDelayed();
         mDataStateListener.start(mSubId);
         mLastDisplayedCycle = null;
+        updatePolicy();
 
         // kick off loader for network history
         // TODO: consider chaining two loaders together instead of reloading
@@ -291,36 +252,31 @@
      */
     @VisibleForTesting
     void updatePolicy() {
-        final NetworkPolicy policy = services.mPolicyEditor.getPolicy(mTemplate);
-        final View configureButton = mHeader.findViewById(R.id.filter_settings);
-        //SUB SELECT
-        if (policy != null && isMobileDataAvailable()) {
-            mChart.setNetworkPolicy(policy);
-            configureButton.setVisibility(View.VISIBLE);
+        mIsBillingCycleModifiable = isBillingCycleModifiable();
+        if (mIsBillingCycleModifiable) {
+            mChart.setNetworkPolicy(services.mPolicyEditor.getPolicy(mTemplate));
         } else {
-            // controls are disabled; don't bind warning/limit sweeps
-            mChart.setNetworkPolicy(null);
-            configureButton.setVisibility(View.GONE);
+            mChart.setNetworkPolicy(null);  // don't bind warning / limit sweeps
         }
-
-        // generate cycle list based on policy and available history
-        if (mCycleData != null) {
-            mCycleAdapter.updateCycleList(mCycleData);
-        }
-        mDataUsageListAppsController.setCycleData(mCycleData);
-        updateSelectedCycle();
+        updateConfigButtonVisibility();
     }
 
-    private boolean isMobileDataAvailable() {
+    @VisibleForTesting
+    boolean isBillingCycleModifiable() {
         return mBillingCycleRepository.isModifiable(mSubId)
                 && SubscriptionManager.from(requireContext())
                 .getActiveSubscriptionInfo(mSubId) != null;
     }
 
+    private void updateConfigButtonVisibility() {
+        mDataUsageListHeaderController.setConfigButtonVisible(
+                mIsBillingCycleModifiable && mCycleData != null);
+    }
+
     /**
      * Updates the chart and detail data when initial loaded or selected cycle changed.
      */
-    private void updateSelectedCycle() {
+    private void updateSelectedCycle(CycleAdapter.CycleItem cycle, int position) {
         // Avoid from updating UI after #onStop.
         if (!getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.STARTED)) {
             return;
@@ -332,11 +288,6 @@
             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;
@@ -350,9 +301,10 @@
 
         // update chart to show selected cycle, and update detail data
         // to match updated sweep bounds.
-        mChart.setNetworkCycleData(mCycleData.get(position));
+        NetworkCycleChartData cycleChartData = mCycleData.get(position);
+        mChart.setNetworkCycleData(cycleChartData);
 
-        updateDetailData();
+        updateDetailData(cycleChartData);
     }
 
     /**
@@ -360,34 +312,21 @@
      * current mode. Updates {@link #mAdapter} with sorted list
      * of applications data usage.
      */
-    private void updateDetailData() {
+    private void updateDetailData(NetworkCycleChartData cycleChartData) {
         if (LOGD) Log.d(TAG, "updateDetailData()");
 
         // kick off loader for detailed stats
         mDataUsageListAppsController.update(
                 mSubscriptionInfoEntity == null ? null : mSubscriptionInfoEntity.carrierId,
-                mChart.getInspectStart(),
-                mChart.getInspectEnd()
+                cycleChartData.getStartTime(),
+                cycleChartData.getEndTime()
         );
 
-        final long totalBytes = mCycleData != null && !mCycleData.isEmpty()
-                ? mCycleData.get(mCycleSpinner.getSelectedItemPosition()).getTotalUsage() : 0;
+        final long totalBytes = cycleChartData.getTotalUsage();
         final CharSequence totalPhrase = DataUsageUtils.formatDataUsage(getActivity(), totalBytes);
         mUsageAmount.setTitle(getString(R.string.data_used_template, totalPhrase));
     }
 
-    private final OnItemSelectedListener mCycleListener = new OnItemSelectedListener() {
-        @Override
-        public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
-            updateSelectedCycle();
-        }
-
-        @Override
-        public void onNothingSelected(AdapterView<?> parent) {
-            // ignored
-        }
-    };
-
     @VisibleForTesting
     final LoaderCallbacks<List<NetworkCycleChartData>> mNetworkCycleDataCallbacks =
             new LoaderCallbacks<>() {
@@ -404,9 +343,9 @@
                         List<NetworkCycleChartData> data) {
                     mLoadingViewController.showContent(false /* animate */);
                     mCycleData = data;
-                    // calculate policy cycles based on available data
-                    updatePolicy();
-                    mCycleSpinner.setVisibility(View.VISIBLE);
+                    mDataUsageListHeaderController.updateCycleData(mCycleData);
+                    updateConfigButtonVisibility();
+                    mDataUsageListAppsController.setCycleData(mCycleData);
                 }
 
                 @Override
diff --git a/src/com/android/settings/datausage/DataUsageListHeaderController.kt b/src/com/android/settings/datausage/DataUsageListHeaderController.kt
new file mode 100644
index 0000000..e295a4c
--- /dev/null
+++ b/src/com/android/settings/datausage/DataUsageListHeaderController.kt
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2023 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.datausage
+
+import android.net.NetworkTemplate
+import android.os.Bundle
+import android.view.View
+import android.view.accessibility.AccessibilityEvent
+import android.widget.AdapterView
+import android.widget.Spinner
+import androidx.annotation.OpenForTesting
+import com.android.settings.R
+import com.android.settings.core.SubSettingLauncher
+import com.android.settings.datausage.CycleAdapter.CycleItem
+import com.android.settings.datausage.CycleAdapter.SpinnerInterface
+import com.android.settingslib.net.NetworkCycleChartData
+
+@OpenForTesting
+open class DataUsageListHeaderController(
+    header: View,
+    template: NetworkTemplate,
+    sourceMetricsCategory: Int,
+    private val onItemSelected: (cycleItem: CycleItem, position: Int) -> Unit,
+) {
+    private val context = header.context
+    private val configureButton: View = header.requireViewById(R.id.filter_settings)
+    private val cycleSpinner: Spinner = header.requireViewById(R.id.filter_spinner)
+    private val cycleAdapter = CycleAdapter(context, object : SpinnerInterface {
+        override fun setAdapter(cycleAdapter: CycleAdapter) {
+            cycleSpinner.adapter = cycleAdapter
+        }
+
+        override fun getSelectedItem() = cycleSpinner.selectedItem
+
+        override fun setSelection(position: Int) {
+            cycleSpinner.setSelection(position)
+        }
+    })
+
+    private val cycleListener = object : AdapterView.OnItemSelectedListener {
+        override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
+            if (0 <= position && position < cycleAdapter.count) {
+                cycleAdapter.getItem(position)?.let { cycleItem ->
+                    onItemSelected(cycleItem, position)
+                }
+            }
+        }
+
+        override fun onNothingSelected(parent: AdapterView<*>?) {
+            // ignored
+        }
+    }
+
+    init {
+        configureButton.setOnClickListener {
+            val args = Bundle().apply {
+                putParcelable(DataUsageList.EXTRA_NETWORK_TEMPLATE, template)
+            }
+            SubSettingLauncher(context).apply {
+                setDestination(BillingCycleSettings::class.java.name)
+                setTitleRes(R.string.billing_cycle)
+                setSourceMetricsCategory(sourceMetricsCategory)
+                setArguments(args)
+            }.launch()
+        }
+        cycleSpinner.visibility = View.GONE
+        cycleSpinner.accessibilityDelegate = object : View.AccessibilityDelegate() {
+            override fun sendAccessibilityEvent(host: View, eventType: Int) {
+                if (eventType == AccessibilityEvent.TYPE_VIEW_SELECTED) {
+                    // Ignore TYPE_VIEW_SELECTED or TalkBack will speak for it at onResume.
+                    return
+                }
+                super.sendAccessibilityEvent(host, eventType)
+            }
+        }
+    }
+
+    open fun setConfigButtonVisible(visible: Boolean) {
+        configureButton.visibility = if (visible) View.VISIBLE else View.GONE
+    }
+
+    open fun updateCycleData(cycleData: List<NetworkCycleChartData>) {
+        cycleSpinner.onItemSelectedListener = cycleListener
+        // calculate policy cycles based on available data
+        // generate cycle list based on policy and available history
+        cycleAdapter.updateCycleList(cycleData)
+        cycleSpinner.visibility = View.VISIBLE
+    }
+}
diff --git a/src/com/android/settings/datausage/SpinnerPreference.java b/src/com/android/settings/datausage/SpinnerPreference.java
index c6b5f9f..a705079 100644
--- a/src/com/android/settings/datausage/SpinnerPreference.java
+++ b/src/com/android/settings/datausage/SpinnerPreference.java
@@ -47,7 +47,6 @@
         notifyChanged();
     }
 
-    @Override
     public void setOnItemSelectedListener(AdapterView.OnItemSelectedListener listener) {
         mListener = listener;
     }
diff --git a/tests/robotests/src/com/android/settings/datausage/DataUsageListTest.java b/tests/robotests/src/com/android/settings/datausage/DataUsageListTest.java
index 5eee615..cb2b278 100644
--- a/tests/robotests/src/com/android/settings/datausage/DataUsageListTest.java
+++ b/tests/robotests/src/com/android/settings/datausage/DataUsageListTest.java
@@ -34,17 +34,12 @@
 import android.os.Bundle;
 import android.os.UserManager;
 import android.provider.Settings;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.widget.FrameLayout;
-import android.widget.Spinner;
 
 import androidx.annotation.NonNull;
-import androidx.fragment.app.FragmentActivity;
 import androidx.loader.app.LoaderManager;
+import androidx.preference.Preference;
 import androidx.preference.PreferenceManager;
 
-import com.android.settings.R;
 import com.android.settings.datausage.lib.BillingCycleRepository;
 import com.android.settings.network.MobileDataEnabledListener;
 import com.android.settings.testutils.FakeFeatureFactory;
@@ -52,6 +47,7 @@
 import com.android.settingslib.NetworkPolicyEditor;
 import com.android.settingslib.core.AbstractPreferenceController;
 import com.android.settingslib.core.instrumentation.VisibilityLoggerMixin;
+import com.android.settingslib.net.NetworkCycleChartData;
 
 import org.junit.Before;
 import org.junit.Rule;
@@ -69,7 +65,11 @@
 import org.robolectric.annotation.Implements;
 import org.robolectric.util.ReflectionHelpers;
 
+import java.util.Collections;
+import java.util.List;
+
 @RunWith(RobolectricTestRunner.class)
+@Config(shadows = DataUsageListTest.ShadowDataUsageBaseFragment.class)
 public class DataUsageListTest {
     @Rule
     public MockitoRule mMockitoRule = MockitoJUnit.rule();
@@ -84,6 +84,8 @@
     private UserManager mUserManager;
     @Mock
     private BillingCycleRepository mBillingCycleRepository;
+    @Mock
+    private DataUsageListHeaderController mDataUsageListHeaderController;
 
     private Activity mActivity;
 
@@ -98,7 +100,6 @@
         mActivity = spy(mActivityController.get());
         mNetworkServices.mPolicyEditor = mock(NetworkPolicyEditor.class);
         mDataUsageList.mDataStateListener = mMobileDataEnabledListener;
-        mDataUsageList.mTemplate = mock(NetworkTemplate.class);
 
         doReturn(mActivity).when(mDataUsageList).getContext();
         doReturn(mUserManager).when(mActivity).getSystemService(UserManager.class);
@@ -110,11 +111,12 @@
         mDataUsageList.mLoadingViewController = mock(LoadingViewController.class);
         doNothing().when(mDataUsageList).updateSubscriptionInfoEntity();
         when(mBillingCycleRepository.isBandwidthControlEnabled()).thenReturn(true);
+        mDataUsageList.mDataUsageListHeaderController = mDataUsageListHeaderController;
     }
 
     @Test
-    @Config(shadows = ShadowDataUsageBaseFragment.class)
     public void onCreate_isNotGuestUser_shouldNotFinish() {
+        mDataUsageList.mTemplate = mock(NetworkTemplate.class);
         doReturn(false).when(mUserManager).isGuestUser();
         doNothing().when(mDataUsageList).processArgument();
 
@@ -124,7 +126,6 @@
     }
 
     @Test
-    @Config(shadows = ShadowDataUsageBaseFragment.class)
     public void onCreate_isGuestUser_shouldFinish() {
         doReturn(true).when(mUserManager).isGuestUser();
 
@@ -135,6 +136,7 @@
 
     @Test
     public void resume_shouldListenDataStateChange() {
+        mDataUsageList.onCreate(null);
         ReflectionHelpers.setField(
                 mDataUsageList, "mVisibilityLoggerMixin", mock(VisibilityLoggerMixin.class));
         ReflectionHelpers.setField(
@@ -149,6 +151,7 @@
 
     @Test
     public void pause_shouldUnlistenDataStateChange() {
+        mDataUsageList.onCreate(null);
         ReflectionHelpers.setField(
                 mDataUsageList, "mVisibilityLoggerMixin", mock(VisibilityLoggerMixin.class));
         ReflectionHelpers.setField(
@@ -187,12 +190,10 @@
 
     @Test
     public void processArgument_fromIntent_shouldGetTemplateFromIntent() {
-        final FragmentActivity activity = mock(FragmentActivity.class);
         final Intent intent = new Intent();
         intent.putExtra(Settings.EXTRA_NETWORK_TEMPLATE, mock(NetworkTemplate.class));
         intent.putExtra(Settings.EXTRA_SUB_ID, 3);
-        when(activity.getIntent()).thenReturn(intent);
-        doReturn(activity).when(mDataUsageList).getActivity();
+        doReturn(intent).when(mDataUsageList).getIntent();
 
         mDataUsageList.processArgument();
 
@@ -201,30 +202,16 @@
     }
 
     @Test
-    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(spinner.getVisibility()).isEqualTo(View.GONE);
-    }
-
-    @Test
     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();
+        mDataUsageList.mTemplate = mock(NetworkTemplate.class);
+        mDataUsageList.onCreate(null);
+        mDataUsageList.updatePolicy();
+        List<NetworkCycleChartData> mockData = Collections.emptyList();
 
-        mDataUsageList.mNetworkCycleDataCallbacks.onLoadFinished(null, null);
+        mDataUsageList.mNetworkCycleDataCallbacks.onLoadFinished(null, mockData);
 
-        assertThat(spinner.getVisibility()).isEqualTo(View.VISIBLE);
+        verify(mDataUsageListHeaderController).updateCycleData(mockData);
+        verify(mDataUsageListHeaderController).setConfigButtonVisible(true);
     }
 
     @Test
@@ -234,19 +221,6 @@
         verify(mLoaderManager).destroyLoader(DataUsageList.LOADER_CHART_DATA);
     }
 
-    private View getHeader() {
-        final View rootView = LayoutInflater.from(mActivity)
-                .inflate(R.layout.preference_list_fragment, null, false);
-        final FrameLayout pinnedHeader = rootView.findViewById(R.id.pinned_header);
-
-        return mActivity.getLayoutInflater()
-                .inflate(R.layout.apps_filter_spinner, pinnedHeader, false);
-    }
-
-    private Spinner getSpinner(View header) {
-        return header.findViewById(R.id.filter_spinner);
-    }
-
     @Implements(DataUsageBaseFragment.class)
     public static class ShadowDataUsageBaseFragment {
         @Implementation
@@ -261,10 +235,28 @@
             return mock(clazz);
         }
 
+        @Override
+        public <T extends Preference> T findPreference(CharSequence key) {
+            if (key.toString().equals("chart_data")) {
+                return (T) mock(ChartDataUsagePreference.class);
+            }
+            return (T) mock(Preference.class);
+        }
+
+        @Override
+        public Intent getIntent() {
+            return new Intent();
+        }
+
         @NonNull
         @Override
         BillingCycleRepository createBillingCycleRepository() {
             return mBillingCycleRepository;
         }
+
+        @Override
+        boolean isBillingCycleModifiable() {
+            return true;
+        }
     }
 }
diff --git a/tests/spa_unit/src/com/android/settings/datausage/DataUsageListHeaderControllerTest.kt b/tests/spa_unit/src/com/android/settings/datausage/DataUsageListHeaderControllerTest.kt
new file mode 100644
index 0000000..a1eebe7
--- /dev/null
+++ b/tests/spa_unit/src/com/android/settings/datausage/DataUsageListHeaderControllerTest.kt
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2023 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.datausage
+
+import android.content.Context
+import android.net.NetworkTemplate
+import android.view.LayoutInflater
+import android.view.View
+import android.widget.Spinner
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.settings.R
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doNothing
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.spy
+import org.mockito.kotlin.whenever
+
+@RunWith(AndroidJUnit4::class)
+class DataUsageListHeaderControllerTest {
+
+    private val context: Context = spy(ApplicationProvider.getApplicationContext()) {
+        doNothing().whenever(mock).startActivity(any())
+    }
+
+    private val header =
+        LayoutInflater.from(context).inflate(R.layout.apps_filter_spinner, null, false)
+
+    private val configureButton: View = header.requireViewById(R.id.filter_settings)
+
+    private val spinner: Spinner = header.requireViewById(R.id.filter_spinner)
+
+    private val controller = DataUsageListHeaderController(
+        header = header,
+        template = mock<NetworkTemplate>(),
+        sourceMetricsCategory = 0,
+        onItemSelected = { _, _ -> },
+    )
+
+    @Test
+    fun onViewCreated_shouldHideCycleSpinner() {
+        assertThat(spinner.visibility).isEqualTo(View.GONE)
+    }
+
+    @Test
+    fun updateCycleData_shouldShowCycleSpinner() {
+        controller.updateCycleData(emptyList())
+
+        assertThat(spinner.visibility).isEqualTo(View.VISIBLE)
+    }
+
+    @Test
+    fun setConfigButtonVisible_setToTrue_shouldShowConfigureButton() {
+        controller.setConfigButtonVisible(true)
+
+        assertThat(configureButton.visibility).isEqualTo(View.VISIBLE)
+    }
+
+    @Test
+    fun setConfigButtonVisible_setToFalse_shouldHideConfigureButton() {
+        controller.setConfigButtonVisible(false)
+
+        assertThat(configureButton.visibility).isEqualTo(View.GONE)
+    }
+}