Merge "Fix the ANR in panel when changing volume continuously" into rvc-qpr-dev
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 2f53cc1..9260201 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -3258,6 +3258,11 @@
             android:permission="android.permission.MANAGE_SLICE_PERMISSIONS"
             android:exported="true" />
 
+        <receiver
+            android:name=".slices.VolumeSliceRelayReceiver"
+            android:permission="android.permission.MANAGE_SLICE_PERMISSIONS"
+            android:exported="true" />
+
         <!-- Couldn't be triggered from outside of settings. Statsd can trigger it because we send
              PendingIntent to it-->
         <receiver android:name=".fuelgauge.batterytip.AnomalyDetectionReceiver"
diff --git a/src/com/android/settings/notification/AdjustVolumeRestrictedPreferenceController.java b/src/com/android/settings/notification/AdjustVolumeRestrictedPreferenceController.java
index bed25cd..f75fd4b 100644
--- a/src/com/android/settings/notification/AdjustVolumeRestrictedPreferenceController.java
+++ b/src/com/android/settings/notification/AdjustVolumeRestrictedPreferenceController.java
@@ -64,6 +64,7 @@
         filter.addAction(AudioManager.VOLUME_CHANGED_ACTION);
         filter.addAction(AudioManager.STREAM_MUTE_CHANGED_ACTION);
         filter.addAction(AudioManager.MASTER_MUTE_CHANGED_ACTION);
+        filter.addAction(AudioManager.STREAM_DEVICES_CHANGED_ACTION);
         return filter;
     }
 }
diff --git a/src/com/android/settings/notification/VolumeSeekBarPreferenceController.java b/src/com/android/settings/notification/VolumeSeekBarPreferenceController.java
index f7bf75f..b32f922 100644
--- a/src/com/android/settings/notification/VolumeSeekBarPreferenceController.java
+++ b/src/com/android/settings/notification/VolumeSeekBarPreferenceController.java
@@ -107,7 +107,10 @@
         return mHelper.getMinVolume(getAudioStream());
     }
 
-    protected abstract int getAudioStream();
+    /**
+     * @return the audio stream type
+     */
+    public abstract int getAudioStream();
 
     protected abstract int getMuteIcon();
 
diff --git a/src/com/android/settings/slices/CustomSliceRegistry.java b/src/com/android/settings/slices/CustomSliceRegistry.java
index a5768d3..bb7def6 100644
--- a/src/com/android/settings/slices/CustomSliceRegistry.java
+++ b/src/com/android/settings/slices/CustomSliceRegistry.java
@@ -217,6 +217,16 @@
             .build();
 
     /**
+     * Full {@link Uri} for the all volume Slices.
+     */
+    public static final Uri VOLUME_SLICES_URI = new Uri.Builder()
+            .scheme(ContentResolver.SCHEME_CONTENT)
+            .authority(SettingsSliceProvider.SLICE_AUTHORITY)
+            .appendPath(SettingsSlicesContract.PATH_SETTING_ACTION)
+            .appendPath("volume_slices")
+            .build();
+
+    /**
      * Full {@link Uri} for the Wifi Calling Slice.
      */
     public static final Uri WIFI_CALLING_URI = new Uri.Builder()
diff --git a/src/com/android/settings/slices/SettingsSliceProvider.java b/src/com/android/settings/slices/SettingsSliceProvider.java
index c22d001..5a1c424 100644
--- a/src/com/android/settings/slices/SettingsSliceProvider.java
+++ b/src/com/android/settings/slices/SettingsSliceProvider.java
@@ -47,6 +47,7 @@
 import com.android.settings.Utils;
 import com.android.settings.bluetooth.BluetoothSliceBuilder;
 import com.android.settings.core.BasePreferenceController;
+import com.android.settings.notification.VolumeSeekBarPreferenceController;
 import com.android.settings.notification.zen.ZenModeSliceBuilder;
 import com.android.settings.overlay.FeatureFactory;
 import com.android.settingslib.SliceBroadcastRelay;
@@ -184,7 +185,10 @@
 
     @Override
     public void onSliceUnpinned(Uri sliceUri) {
-        SliceBroadcastRelay.unregisterReceivers(getContext(), sliceUri);
+        final Context context = getContext();
+        if (!VolumeSliceHelper.unregisterUri(context, sliceUri)) {
+            SliceBroadcastRelay.unregisterReceivers(context, sliceUri);
+        }
         ThreadUtils.postOnMainThread(() -> stopBackgroundWorker(sliceUri));
     }
 
@@ -390,7 +394,13 @@
 
         final IntentFilter filter = controller.getIntentFilter();
         if (filter != null) {
-            registerIntentToUri(filter, uri);
+            if (controller instanceof VolumeSeekBarPreferenceController) {
+                // Register volume slices to a broadcast relay to reduce unnecessary UI updates
+                VolumeSliceHelper.registerIntentToUri(getContext(), filter, uri,
+                        ((VolumeSeekBarPreferenceController) controller).getAudioStream());
+            } else {
+                registerIntentToUri(filter, uri);
+            }
         }
 
         ThreadUtils.postOnMainThread(() -> startBackgroundWorker(controller, uri));
diff --git a/src/com/android/settings/slices/VolumeSliceHelper.java b/src/com/android/settings/slices/VolumeSliceHelper.java
new file mode 100644
index 0000000..bcf02e5
--- /dev/null
+++ b/src/com/android/settings/slices/VolumeSliceHelper.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2020 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.slices;
+
+import static com.android.settings.slices.CustomSliceRegistry.VOLUME_SLICES_URI;
+
+import android.content.ContentProvider;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.media.AudioManager;
+import android.net.Uri;
+import android.util.ArrayMap;
+import android.util.Log;
+
+import androidx.annotation.VisibleForTesting;
+
+import com.android.settingslib.SliceBroadcastRelay;
+
+import java.util.Map;
+
+/**
+ * This helper is to handle the broadcasts of volume slices
+ */
+public class VolumeSliceHelper {
+
+    private static final String TAG = "VolumeSliceHelper";
+
+    @VisibleForTesting
+    static Map<Uri, Integer> sRegisteredUri = new ArrayMap<>();
+    @VisibleForTesting
+    static IntentFilter sIntentFilter;
+
+    static void registerIntentToUri(Context context, IntentFilter intentFilter, Uri sliceUri,
+            int audioStream) {
+        Log.d(TAG, "Registering uri for broadcast relay: " + sliceUri);
+        synchronized (sRegisteredUri) {
+            if (sRegisteredUri.isEmpty()) {
+                SliceBroadcastRelay.registerReceiver(context, VOLUME_SLICES_URI,
+                        VolumeSliceRelayReceiver.class, intentFilter);
+                sIntentFilter = intentFilter;
+            }
+            sRegisteredUri.put(sliceUri, audioStream);
+        }
+    }
+
+    static boolean unregisterUri(Context context, Uri sliceUri) {
+        if (!sRegisteredUri.containsKey(sliceUri)) {
+            return false;
+        }
+
+        Log.d(TAG, "Unregistering uri broadcast relay: " + sliceUri);
+        synchronized (sRegisteredUri) {
+            sRegisteredUri.remove(sliceUri);
+            if (sRegisteredUri.isEmpty()) {
+                sIntentFilter = null;
+                SliceBroadcastRelay.unregisterReceivers(context, VOLUME_SLICES_URI);
+            }
+        }
+        return true;
+    }
+
+    static void onReceive(Context context, Intent intent) {
+        final String action = intent.getAction();
+        if (sIntentFilter == null || action == null || !sIntentFilter.hasAction(action)) {
+            return;
+        }
+
+        final String uriString = intent.getStringExtra(SliceBroadcastRelay.EXTRA_URI);
+        if (uriString == null) {
+            return;
+        }
+
+        final Uri uri = Uri.parse(uriString);
+        if (!VOLUME_SLICES_URI.equals(ContentProvider.getUriWithoutUserId(uri))) {
+            Log.w(TAG, "Invalid uri: " + uriString);
+            return;
+        }
+
+        if (AudioManager.VOLUME_CHANGED_ACTION.equals(action)) {
+            handleVolumeChanged(context, intent);
+        } else if (AudioManager.STREAM_MUTE_CHANGED_ACTION.equals(action)
+                || AudioManager.STREAM_DEVICES_CHANGED_ACTION.equals(action)) {
+            handleStreamChanged(context, intent);
+        } else {
+            notifyAllStreamsChanged(context);
+        }
+    }
+
+    private static void handleVolumeChanged(Context context, Intent intent) {
+        final int vol = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_VALUE, -1);
+        final int prevVol = intent.getIntExtra(AudioManager.EXTRA_PREV_VOLUME_STREAM_VALUE, -1);
+        if (vol != prevVol) {
+            handleStreamChanged(context, intent);
+        }
+    }
+
+    private static void handleStreamChanged(Context context, Intent intent) {
+        final int inputType = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE, -1);
+        for (Map.Entry<Uri, Integer> entry : sRegisteredUri.entrySet()) {
+            if (entry.getValue() == inputType) {
+                context.getContentResolver().notifyChange(entry.getKey(), null /* observer */);
+                break;
+            }
+        }
+    }
+
+    private static void notifyAllStreamsChanged(Context context) {
+        sRegisteredUri.forEach((uri, audioStream) -> {
+            context.getContentResolver().notifyChange(uri, null /* observer */);
+        });
+    }
+}
diff --git a/src/com/android/settings/slices/VolumeSliceRelayReceiver.java b/src/com/android/settings/slices/VolumeSliceRelayReceiver.java
new file mode 100644
index 0000000..f6088d0
--- /dev/null
+++ b/src/com/android/settings/slices/VolumeSliceRelayReceiver.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2020 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.slices;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+
+/**
+ * Receives broadcasts to notify that Settings volume Slices are potentially stale.
+ */
+public class VolumeSliceRelayReceiver extends BroadcastReceiver {
+
+    @Override
+    public void onReceive(Context context, Intent intent) {
+        VolumeSliceHelper.onReceive(context, intent);
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/slices/VolumeSliceHelperTest.java b/tests/robotests/src/com/android/settings/slices/VolumeSliceHelperTest.java
new file mode 100644
index 0000000..5e22adf
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/slices/VolumeSliceHelperTest.java
@@ -0,0 +1,260 @@
+/*
+ * Copyright (C) 2020 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.slices;
+
+import static com.android.settings.slices.CustomSliceRegistry.VOLUME_SLICES_URI;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.BroadcastReceiver;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.media.AudioManager;
+import android.net.Uri;
+
+import com.android.settings.notification.MediaVolumePreferenceController;
+import com.android.settings.notification.RingVolumePreferenceController;
+import com.android.settings.notification.VolumeSeekBarPreferenceController;
+import com.android.settingslib.SliceBroadcastRelay;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.annotation.Resetter;
+
+@RunWith(RobolectricTestRunner.class)
+@Config(shadows = VolumeSliceHelperTest.ShadowSliceBroadcastRelay.class)
+public class VolumeSliceHelperTest {
+
+    @Mock
+    private ContentResolver mResolver;
+
+    private Context mContext;
+    private Intent mIntent;
+    private VolumeSeekBarPreferenceController mMediaController;
+    private VolumeSeekBarPreferenceController mRingController;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        mContext = spy(RuntimeEnvironment.application);
+        when(mContext.getContentResolver()).thenReturn(mResolver);
+
+        mMediaController = new MediaVolumePreferenceController(mContext);
+        mRingController = new RingVolumePreferenceController(mContext);
+
+        mIntent = createIntent(AudioManager.VOLUME_CHANGED_ACTION)
+                .putExtra(AudioManager.EXTRA_VOLUME_STREAM_VALUE, 1)
+                .putExtra(AudioManager.EXTRA_PREV_VOLUME_STREAM_VALUE, 2)
+                .putExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE, mMediaController.getAudioStream());
+    }
+
+    @After
+    public void cleanUp() {
+        ShadowSliceBroadcastRelay.reset();
+        VolumeSliceHelper.sRegisteredUri.clear();
+        VolumeSliceHelper.sIntentFilter = null;
+    }
+
+    @Test
+    public void registerIntentToUri_volumeController_shouldRegisterReceiver() {
+        registerIntentToUri(mMediaController);
+
+        assertThat(ShadowSliceBroadcastRelay.getRegisteredCount()).isEqualTo(1);
+        assertThat(VolumeSliceHelper.sRegisteredUri)
+                .containsKey((mMediaController.getSliceUri()));
+    }
+
+    @Test
+    public void registerIntentToUri_doubleVolumeControllers_shouldRegisterReceiverOnce() {
+        registerIntentToUri(mMediaController);
+
+        registerIntentToUri(mRingController);
+
+        assertThat(ShadowSliceBroadcastRelay.getRegisteredCount()).isEqualTo(1);
+        assertThat(VolumeSliceHelper.sRegisteredUri)
+                .containsKey((mRingController.getSliceUri()));
+    }
+
+    @Test
+    public void unregisterUri_notFinalUri_shouldNotUnregisterReceiver() {
+        registerIntentToUri(mMediaController);
+        registerIntentToUri(mRingController);
+
+        VolumeSliceHelper.unregisterUri(mContext, mMediaController.getSliceUri());
+
+        assertThat(ShadowSliceBroadcastRelay.getRegisteredCount()).isEqualTo(1);
+        assertThat(VolumeSliceHelper.sRegisteredUri)
+                .doesNotContainKey((mMediaController.getSliceUri()));
+    }
+
+    @Test
+    public void unregisterUri_finalUri_shouldUnregisterReceiver() {
+        registerIntentToUri(mMediaController);
+
+        VolumeSliceHelper.unregisterUri(mContext, mMediaController.getSliceUri());
+
+        assertThat(ShadowSliceBroadcastRelay.getRegisteredCount()).isEqualTo(0);
+        assertThat(VolumeSliceHelper.sRegisteredUri)
+                .doesNotContainKey((mMediaController.getSliceUri()));
+    }
+
+    @Test
+    public void unregisterUri_unregisterTwice_shouldUnregisterReceiverOnce() {
+        registerIntentToUri(mMediaController);
+
+        VolumeSliceHelper.unregisterUri(mContext, mMediaController.getSliceUri());
+        VolumeSliceHelper.unregisterUri(mContext, mMediaController.getSliceUri());
+
+        assertThat(ShadowSliceBroadcastRelay.getRegisteredCount()).isEqualTo(0);
+    }
+
+    @Test
+    public void unregisterUri_notRegistered_shouldNotUnregisterReceiver() {
+        registerIntentToUri(mMediaController);
+
+        VolumeSliceHelper.unregisterUri(mContext, mRingController.getSliceUri());
+
+        assertThat(ShadowSliceBroadcastRelay.getRegisteredCount()).isEqualTo(1);
+        assertThat(VolumeSliceHelper.sRegisteredUri)
+                .containsKey((mMediaController.getSliceUri()));
+    }
+
+    @Test
+    public void onReceive_audioStreamRegistered_shouldNotifyChange() {
+        registerIntentToUri(mMediaController);
+
+        VolumeSliceHelper.onReceive(mContext, mIntent);
+
+        verify(mResolver).notifyChange(mMediaController.getSliceUri(), null);
+    }
+
+    @Test
+    public void onReceive_audioStreamNotRegistered_shouldNotNotifyChange() {
+        VolumeSliceHelper.onReceive(mContext, mIntent);
+
+        verify(mResolver, never()).notifyChange(mMediaController.getSliceUri(), null);
+    }
+
+    @Test
+    public void onReceive_audioStreamNotMatched_shouldNotNotifyChange() {
+        registerIntentToUri(mMediaController);
+        mIntent.putExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE, AudioManager.STREAM_DTMF);
+
+        VolumeSliceHelper.onReceive(mContext, mIntent);
+
+        verify(mResolver, never()).notifyChange(mMediaController.getSliceUri(), null);
+    }
+
+    @Test
+    public void onReceive_mediaVolumeNotChanged_shouldNotNotifyChange() {
+        mIntent.putExtra(AudioManager.EXTRA_VOLUME_STREAM_VALUE, 1)
+                .putExtra(AudioManager.EXTRA_PREV_VOLUME_STREAM_VALUE, 1);
+        registerIntentToUri(mMediaController);
+
+        VolumeSliceHelper.onReceive(mContext, mIntent);
+
+        verify(mResolver, never()).notifyChange(mMediaController.getSliceUri(), null);
+    }
+
+    @Test
+    public void onReceive_streamVolumeMuted_shouldNotifyChange() {
+        final Intent intent = createIntent(AudioManager.STREAM_MUTE_CHANGED_ACTION)
+                .putExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE, mMediaController.getAudioStream());
+        registerIntentToUri(mMediaController);
+        registerIntentToUri(mRingController);
+
+        VolumeSliceHelper.onReceive(mContext, intent);
+
+        verify(mResolver).notifyChange(mMediaController.getSliceUri(), null);
+    }
+
+    @Test
+    public void onReceive_streamDevicesChanged_shouldNotifyChange() {
+        final Intent intent = createIntent(AudioManager.STREAM_DEVICES_CHANGED_ACTION)
+                .putExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE, mRingController.getAudioStream());
+        registerIntentToUri(mMediaController);
+        registerIntentToUri(mRingController);
+
+        VolumeSliceHelper.onReceive(mContext, intent);
+
+        verify(mResolver).notifyChange(mRingController.getSliceUri(), null);
+    }
+
+    @Test
+    public void onReceive_primaryMutedChanged_shouldNotifyChangeAll() {
+        final Intent intent = createIntent(AudioManager.MASTER_MUTE_CHANGED_ACTION);
+        registerIntentToUri(mMediaController);
+        registerIntentToUri(mRingController);
+
+        VolumeSliceHelper.onReceive(mContext, intent);
+
+        verify(mResolver).notifyChange(mMediaController.getSliceUri(), null);
+        verify(mResolver).notifyChange(mRingController.getSliceUri(), null);
+    }
+
+    private void registerIntentToUri(VolumeSeekBarPreferenceController controller) {
+        VolumeSliceHelper.registerIntentToUri(mContext, controller.getIntentFilter(),
+                controller.getSliceUri(), controller.getAudioStream());
+    }
+
+    private Intent createIntent(String action) {
+        return new Intent(action)
+                .putExtra(SliceBroadcastRelay.EXTRA_URI, VOLUME_SLICES_URI.toString());
+    }
+
+    @Implements(SliceBroadcastRelay.class)
+    public static class ShadowSliceBroadcastRelay {
+
+        private static int sRegisteredCount;
+
+        @Implementation
+        public static void registerReceiver(Context context, Uri sliceUri,
+                Class<? extends BroadcastReceiver> receiver, IntentFilter filter) {
+            sRegisteredCount++;
+        }
+
+        @Implementation
+        public static void unregisterReceivers(Context context, Uri sliceUri) {
+            sRegisteredCount--;
+        }
+
+        @Resetter
+        static void reset() {
+            sRegisteredCount = 0;
+        }
+
+        static int getRegisteredCount() {
+            return sRegisteredCount;
+        }
+    }
+}