Merge "Remove the char limit of unrestricted_app" into main
diff --git a/src/com/android/settings/datausage/lib/NetworkStatsRepository.kt b/src/com/android/settings/datausage/lib/NetworkStatsRepository.kt
index 56b1931..484a8a3 100644
--- a/src/com/android/settings/datausage/lib/NetworkStatsRepository.kt
+++ b/src/com/android/settings/datausage/lib/NetworkStatsRepository.kt
@@ -93,13 +93,15 @@
             val buckets = mutableListOf<Bucket>()
             val bucket = NetworkStats.Bucket()
             while (getNextBucket(bucket)) {
-                buckets += Bucket(
-                    uid = bucket.uid,
-                    bytes = bucket.bytes,
-                    state = bucket.state,
-                    startTimeStamp = bucket.startTimeStamp,
-                    endTimeStamp = bucket.endTimeStamp,
-                )
+                if (bucket.bytes > 0) {
+                    buckets += Bucket(
+                        uid = bucket.uid,
+                        bytes = bucket.bytes,
+                        state = bucket.state,
+                        startTimeStamp = bucket.startTimeStamp,
+                        endTimeStamp = bucket.endTimeStamp,
+                    )
+                }
             }
             buckets
         }
diff --git a/src/com/android/settings/widget/HighlightablePreferenceGroupAdapter.java b/src/com/android/settings/widget/HighlightablePreferenceGroupAdapter.java
index 7145460..82ef58b 100644
--- a/src/com/android/settings/widget/HighlightablePreferenceGroupAdapter.java
+++ b/src/com/android/settings/widget/HighlightablePreferenceGroupAdapter.java
@@ -40,6 +40,7 @@
 
 import com.android.settings.R;
 import com.android.settings.SettingsPreferenceFragment;
+import com.android.settings.accessibility.AccessibilityUtil;
 
 import com.google.android.material.appbar.AppBarLayout;
 
@@ -50,6 +51,8 @@
     static final long DELAY_COLLAPSE_DURATION_MILLIS = 300L;
     @VisibleForTesting
     static final long DELAY_HIGHLIGHT_DURATION_MILLIS = 600L;
+    @VisibleForTesting
+    static final long DELAY_HIGHLIGHT_DURATION_MILLIS_A11Y = 300L;
     private static final long HIGHLIGHT_DURATION = 15000L;
     private static final long HIGHLIGHT_FADE_OUT_DURATION = 500L;
     private static final long HIGHLIGHT_FADE_IN_DURATION = 200L;
@@ -59,6 +62,7 @@
     @VisibleForTesting
     boolean mFadeInAnimated;
 
+    private final Context mContext;
     private final int mNormalBackgroundRes;
     private final String mHighlightKey;
     private boolean mHighlightRequested;
@@ -102,12 +106,12 @@
         super(preferenceGroup);
         mHighlightKey = key;
         mHighlightRequested = highlightRequested;
-        final Context context = preferenceGroup.getContext();
+        mContext = preferenceGroup.getContext();
         final TypedValue outValue = new TypedValue();
-        context.getTheme().resolveAttribute(android.R.attr.selectableItemBackground,
+        mContext.getTheme().resolveAttribute(android.R.attr.selectableItemBackground,
                 outValue, true /* resolveRefs */);
         mNormalBackgroundRes = outValue.resourceId;
-        mHighlightColor = context.getColor(R.color.preference_highlight_color);
+        mHighlightColor = mContext.getColor(R.color.preference_highlight_color);
     }
 
     @Override
@@ -121,12 +125,11 @@
         View v = holder.itemView;
         if (position == mHighlightPosition
                 && (mHighlightKey != null
-                && TextUtils.equals(mHighlightKey, getItem(position).getKey()))) {
+                && TextUtils.equals(mHighlightKey, getItem(position).getKey()))
+                && v.isShown()) {
             // This position should be highlighted. If it's highlighted before - skip animation.
+            v.requestAccessibilityFocus();
             addHighlightBackground(holder, !mFadeInAnimated);
-            if (v != null) {
-                v.requestAccessibilityFocus();
-            }
         } else if (Boolean.TRUE.equals(v.getTag(R.id.preference_highlighted))) {
             // View with highlight is reused for a view that should not have highlight
             removeHighlightBackground(holder, false /* animate */);
@@ -157,13 +160,14 @@
 
         // Remove the animator as early as possible to avoid a RecyclerView crash.
         recyclerView.setItemAnimator(null);
-        // Scroll to correct position after 600 milliseconds.
+        // Scroll to correct position after a short delay.
         root.postDelayed(() -> {
             if (ensureHighlightPosition()) {
                 recyclerView.smoothScrollToPosition(mHighlightPosition);
                 highlightAndFocusTargetItem(recyclerView, mHighlightPosition);
             }
-        }, DELAY_HIGHLIGHT_DURATION_MILLIS);
+        }, AccessibilityUtil.isTouchExploreEnabled(mContext)
+                ? DELAY_HIGHLIGHT_DURATION_MILLIS_A11Y : DELAY_HIGHLIGHT_DURATION_MILLIS);
     }
 
     private void highlightAndFocusTargetItem(RecyclerView recyclerView, int highlightPosition) {
diff --git a/tests/robotests/src/com/android/settings/widget/HighlightablePreferenceGroupAdapterTest.java b/tests/robotests/src/com/android/settings/widget/HighlightablePreferenceGroupAdapterTest.java
index 29560ab..03684ad 100644
--- a/tests/robotests/src/com/android/settings/widget/HighlightablePreferenceGroupAdapterTest.java
+++ b/tests/robotests/src/com/android/settings/widget/HighlightablePreferenceGroupAdapterTest.java
@@ -33,6 +33,7 @@
 import android.graphics.drawable.ColorDrawable;
 import android.os.Bundle;
 import android.view.View;
+import android.view.accessibility.AccessibilityManager;
 
 import androidx.preference.Preference;
 import androidx.preference.PreferenceCategory;
@@ -54,6 +55,8 @@
 import org.robolectric.RobolectricTestRunner;
 import org.robolectric.RuntimeEnvironment;
 import org.robolectric.annotation.Config;
+import org.robolectric.shadow.api.Shadow;
+import org.robolectric.shadows.ShadowAccessibilityManager;
 import org.robolectric.util.ReflectionHelpers;
 
 @RunWith(RobolectricTestRunner.class)
@@ -67,7 +70,7 @@
     @Mock
     private View mRoot;
     @Mock
-    private PreferenceCategory mPreferenceCatetory;
+    private PreferenceCategory mPreferenceCategory;
     @Mock
     private SettingsPreferenceFragment mFragment;
 
@@ -82,8 +85,8 @@
         mContext = RuntimeEnvironment.application;
         mPreference = new Preference(mContext);
         mPreference.setKey(TEST_KEY);
-        when(mPreferenceCatetory.getContext()).thenReturn(mContext);
-        mAdapter = spy(new HighlightablePreferenceGroupAdapter(mPreferenceCatetory, TEST_KEY,
+        when(mPreferenceCategory.getContext()).thenReturn(mContext);
+        mAdapter = spy(new HighlightablePreferenceGroupAdapter(mPreferenceCategory, TEST_KEY,
                 false /* highlighted*/));
         when(mAdapter.getItem(anyInt())).thenReturn(mPreference);
         mViewHolder = PreferenceViewHolder.createInstanceForTests(
@@ -102,6 +105,18 @@
     }
 
     @Test
+    public void requestHighlight_enableTouchExploration_shouldHaveA11yHighlightDelay() {
+        ShadowAccessibilityManager am = Shadow.extract(AccessibilityManager.getInstance(mContext));
+        am.setTouchExplorationEnabled(true);
+        when(mAdapter.getPreferenceAdapterPosition(anyString())).thenReturn(1);
+        mAdapter.requestHighlight(mRoot, mock(RecyclerView.class), mock(AppBarLayout.class));
+
+        // DELAY_HIGHLIGHT_DURATION_MILLIS_A11Y = DELAY_COLLAPSE_DURATION_MILLIS
+        verify(mRoot, times(2)).postDelayed(any(),
+                eq(HighlightablePreferenceGroupAdapter.DELAY_HIGHLIGHT_DURATION_MILLIS_A11Y));
+    }
+
+    @Test
     public void requestHighlight_noKey_highlightedBefore_noRecyclerView_shouldNotRequest() {
         ReflectionHelpers.setField(mAdapter, "mHighlightKey", null);
         ReflectionHelpers.setField(mAdapter, "mHighlightRequested", false);
@@ -178,12 +193,24 @@
         assertThat(mViewHolder.itemView.getTag(R.id.preference_highlighted)).isNull();
     }
 
+    @Test
+    public void updateBackground_itemViewIsInvisible_shouldNotSetHighlightedTag() {
+        ReflectionHelpers.setField(mAdapter, "mHighlightPosition", 10);
+        ReflectionHelpers.setField(mViewHolder, "itemView", spy(mViewHolder.itemView));
+        when(mViewHolder.itemView.isShown()).thenReturn(false);
+
+        mAdapter.updateBackground(mViewHolder, 0);
+
+        assertThat(mViewHolder.itemView.getTag(R.id.preference_highlighted)).isNull();
+    }
+
     /**
      * When background is being updated, we also request the a11y focus on the preference
      */
     @Test
     public void updateBackground_shouldRequestAccessibilityFocus() {
         View viewItem = mock(View.class);
+        when(viewItem.isShown()).thenReturn(true);
         mViewHolder = PreferenceViewHolder.createInstanceForTests(viewItem);
         ReflectionHelpers.setField(mAdapter, "mHighlightPosition", 10);
 
@@ -195,6 +222,8 @@
     @Test
     public void updateBackground_highlight_shouldAnimateBackgroundAndSetHighlightedTag() {
         ReflectionHelpers.setField(mAdapter, "mHighlightPosition", 10);
+        ReflectionHelpers.setField(mViewHolder, "itemView", spy(mViewHolder.itemView));
+        when(mViewHolder.itemView.isShown()).thenReturn(true);
         assertThat(mAdapter.mFadeInAnimated).isFalse();
 
         mAdapter.updateBackground(mViewHolder, 10);
@@ -206,9 +235,21 @@
     }
 
     @Test
+    public void updateBackground_highlight_itemViewIsInvisible_shouldNotAnimate() {
+        ReflectionHelpers.setField(mAdapter, "mHighlightPosition", 10);
+        ReflectionHelpers.setField(mViewHolder, "itemView", spy(mViewHolder.itemView));
+        when(mViewHolder.itemView.isShown()).thenReturn(false);
+
+        mAdapter.updateBackground(mViewHolder, 10);
+
+        assertThat(mAdapter.mFadeInAnimated).isFalse();
+    }
+
+    @Test
     public void updateBackgroundTwice_highlight_shouldAnimateOnce() {
         ReflectionHelpers.setField(mAdapter, "mHighlightPosition", 10);
         ReflectionHelpers.setField(mViewHolder, "itemView", spy(mViewHolder.itemView));
+        when(mViewHolder.itemView.isShown()).thenReturn(true);
         assertThat(mAdapter.mFadeInAnimated).isFalse();
         mAdapter.updateBackground(mViewHolder, 10);
         // mFadeInAnimated change from false to true - indicating background change is scheduled
diff --git a/tests/spa_unit/src/com/android/settings/datausage/lib/AppDataUsageDetailsRepositoryTest.kt b/tests/spa_unit/src/com/android/settings/datausage/lib/AppDataUsageDetailsRepositoryTest.kt
index fda3dc9..8476a49 100644
--- a/tests/spa_unit/src/com/android/settings/datausage/lib/AppDataUsageDetailsRepositoryTest.kt
+++ b/tests/spa_unit/src/com/android/settings/datausage/lib/AppDataUsageDetailsRepositoryTest.kt
@@ -125,6 +125,32 @@
         )
     }
 
+    @Test
+    fun queryDetailsForCycles_appWithZeroUsage_filtered(): Unit = runBlocking {
+        networkStatsRepository.stub {
+            on { queryBuckets(CYCLE1_END_TIME, CYCLE2_END_TIME) } doReturn listOf(
+                Bucket(
+                    uid = UID,
+                    bytes = 0L,
+                    startTimeStamp = 0L,
+                    endTimeStamp = 0L,
+                ),
+            )
+        }
+        val repository = AppDataUsageDetailsRepository(
+            context = context,
+            cycles = null,
+            template = template,
+            uids = listOf(UID),
+            networkCycleDataRepository = networkCycleDataRepository,
+            networkStatsRepository = networkStatsRepository,
+        )
+
+        val detailsForCycles = repository.queryDetailsForCycles()
+
+        assertThat(detailsForCycles).isEmpty()
+    }
+
     private companion object {
         const val CYCLE1_START_TIME = 1694444444000L
         const val CYCLE1_END_TIME = 1695555555000L