Snap for 6859016 from 19843031aef4219cc79ff2e5a494365ee49f4055 to rvc-qpr1-release

Change-Id: I3f201714ebfa3396ae3a359ba1b288fbc3147cbc
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/res/xml/battery_saver_settings.xml b/res/xml/battery_saver_settings.xml
index 966034e..29b82ef 100644
--- a/res/xml/battery_saver_settings.xml
+++ b/res/xml/battery_saver_settings.xml
@@ -34,7 +34,7 @@
         settings:controller="com.android.settings.fuelgauge.batterysaver.BatterySaverStickyPreferenceController"/>
 
     <com.android.settings.widget.TwoStateButtonPreference
-        android:key="battery_saver_button"
+        android:key="battery_saver"
         android:title="@string/battery_saver"
         android:selectable="false"
         android:summary="@string/battery_saver_turn_on_summary"
diff --git a/src/com/android/settings/bluetooth/DevicePickerFragment.java b/src/com/android/settings/bluetooth/DevicePickerFragment.java
index 02625bb..ab8eea5 100644
--- a/src/com/android/settings/bluetooth/DevicePickerFragment.java
+++ b/src/com/android/settings/bluetooth/DevicePickerFragment.java
@@ -18,6 +18,7 @@
 
 import static android.os.UserManager.DISALLOW_CONFIG_BLUETOOTH;
 
+import android.Manifest;
 import android.app.settings.SettingsEnums;
 import android.bluetooth.BluetoothAdapter;
 import android.bluetooth.BluetoothDevice;
@@ -192,6 +193,6 @@
         if (mLaunchPackage != null && mLaunchClass != null) {
             intent.setClassName(mLaunchPackage, mLaunchClass);
         }
-        getActivity().sendBroadcast(intent);
+        getActivity().sendBroadcast(intent, Manifest.permission.BLUETOOTH_ADMIN);
     }
 }
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/panel/PanelFragment.java b/src/com/android/settings/panel/PanelFragment.java
index dfbba66..6f2d59b 100644
--- a/src/com/android/settings/panel/PanelFragment.java
+++ b/src/com/android/settings/panel/PanelFragment.java
@@ -98,6 +98,7 @@
     private TextView mHeaderSubtitle;
     private int mMaxHeight;
     private View mFooterDivider;
+    private boolean mPanelCreating;
 
     private final Map<Uri, LiveData<Slice>> mSliceLiveData = new LinkedHashMap<>();
 
@@ -128,6 +129,7 @@
                     if (mPanelSlices != null) {
                         mPanelSlices.getViewTreeObserver().removeOnGlobalLayoutListener(this);
                     }
+                    mPanelCreating = false;
                 }
             };
 
@@ -141,6 +143,7 @@
         mLayoutView.getViewTreeObserver()
                 .addOnGlobalLayoutListener(mPanelLayoutListener);
         mMaxHeight = getResources().getDimensionPixelSize(R.dimen.output_switcher_slice_max_height);
+        mPanelCreating = true;
         createPanelContent();
         return mLayoutView;
     }
@@ -154,6 +157,7 @@
      * Call createPanelContent() once animation end.
      */
     void updatePanelWithAnimation() {
+        mPanelCreating = true;
         final View panelContent = mLayoutView.findViewById(R.id.panel_container);
         final AnimatorSet animatorSet = buildAnimatorSet(mLayoutView,
                 0.0f /* startY */, panelContent.getHeight() /* endY */,
@@ -172,11 +176,16 @@
         animatorSet.start();
     }
 
+    boolean isPanelCreating() {
+        return mPanelCreating;
+    }
+
     private void createPanelContent() {
         final FragmentActivity activity = getActivity();
         if (mLayoutView == null) {
             activity.finish();
         }
+
         final ViewGroup.LayoutParams params = mLayoutView.getLayoutParams();
         params.height = ViewGroup.LayoutParams.WRAP_CONTENT;
         mLayoutView.setLayoutParams(params);
diff --git a/src/com/android/settings/panel/SettingsPanelActivity.java b/src/com/android/settings/panel/SettingsPanelActivity.java
index 68cb8d5..b7b1519 100644
--- a/src/com/android/settings/panel/SettingsPanelActivity.java
+++ b/src/com/android/settings/panel/SettingsPanelActivity.java
@@ -21,6 +21,7 @@
 import android.content.Intent;
 import android.content.res.Configuration;
 import android.os.Bundle;
+import android.text.TextUtils;
 import android.util.Log;
 import android.view.Gravity;
 import android.view.Window;
@@ -41,12 +42,14 @@
  */
 public class SettingsPanelActivity extends FragmentActivity {
 
-    private final String TAG = "panel_activity";
+    private static final String TAG = "SettingsPanelActivity";
 
     @VisibleForTesting
     final Bundle mBundle = new Bundle();
     @VisibleForTesting
     boolean mForceCreation = false;
+    @VisibleForTesting
+    PanelFragment mPanelFragment;
 
     /**
      * Key specifying which Panel the app is requesting.
@@ -87,7 +90,9 @@
     @Override
     protected void onStop() {
         super.onStop();
-        mForceCreation = true;
+        if (mPanelFragment != null && !mPanelFragment.isPanelCreating()) {
+            mForceCreation = true;
+        }
     }
 
     @Override
@@ -104,10 +109,10 @@
             return;
         }
 
+        final String action = callingIntent.getAction();
         // We will use it once media output switch panel support remote device.
         final String mediaPackageName = callingIntent.getStringExtra(EXTRA_PACKAGE_NAME);
-
-        mBundle.putString(KEY_PANEL_TYPE_ARGUMENT, callingIntent.getAction());
+        mBundle.putString(KEY_PANEL_TYPE_ARGUMENT, action);
         mBundle.putString(KEY_CALLING_PACKAGE_NAME, getCallingPackage());
         mBundle.putString(KEY_MEDIA_PACKAGE_NAME, mediaPackageName);
 
@@ -116,9 +121,21 @@
 
         // If fragment already exists and visible, we will need to update panel with animation.
         if (!shouldForceCreation && fragment != null && fragment instanceof PanelFragment) {
-            final PanelFragment panelFragment = (PanelFragment) fragment;
-            panelFragment.setArguments(mBundle);
-            panelFragment.updatePanelWithAnimation();
+            mPanelFragment = (PanelFragment) fragment;
+            if (mPanelFragment.isPanelCreating()) {
+                Log.w(TAG, "A panel is creating, skip " + action);
+                return;
+            }
+
+            final Bundle bundle = fragment.getArguments();
+            if (bundle != null
+                    && TextUtils.equals(action, bundle.getString(KEY_PANEL_TYPE_ARGUMENT))) {
+                Log.w(TAG, "Panel is showing the same action, skip " + action);
+                return;
+            }
+
+            mPanelFragment.setArguments(new Bundle(mBundle));
+            mPanelFragment.updatePanelWithAnimation();
         } else {
             setContentView(R.layout.settings_panel);
 
@@ -127,9 +144,9 @@
             window.setGravity(Gravity.BOTTOM);
             window.setLayout(WindowManager.LayoutParams.MATCH_PARENT,
                     WindowManager.LayoutParams.WRAP_CONTENT);
-            final PanelFragment panelFragment = new PanelFragment();
-            panelFragment.setArguments(mBundle);
-            fragmentManager.beginTransaction().add(R.id.main_content, panelFragment).commit();
+            mPanelFragment = new PanelFragment();
+            mPanelFragment.setArguments(new Bundle(mBundle));
+            fragmentManager.beginTransaction().add(R.id.main_content, mPanelFragment).commit();
         }
     }
 }
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/panel/SettingsPanelActivityTest.java b/tests/robotests/src/com/android/settings/panel/SettingsPanelActivityTest.java
index 833d510..4a14798 100644
--- a/tests/robotests/src/com/android/settings/panel/SettingsPanelActivityTest.java
+++ b/tests/robotests/src/com/android/settings/panel/SettingsPanelActivityTest.java
@@ -26,6 +26,7 @@
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
@@ -36,6 +37,9 @@
 import android.view.Window;
 import android.view.WindowManager;
 
+import androidx.fragment.app.FragmentManager;
+
+import com.android.settings.R;
 import com.android.settings.core.HideNonSystemOverlayMixin;
 import com.android.settings.testutils.FakeFeatureFactory;
 
@@ -43,6 +47,7 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 import org.robolectric.Robolectric;
 import org.robolectric.RobolectricTestRunner;
@@ -56,6 +61,10 @@
     private FakeSettingsPanelActivity mSettingsPanelActivity;
     private PanelFeatureProvider mPanelFeatureProvider;
     private FakePanelContent mFakePanelContent;
+    @Mock
+    private PanelFragment mPanelFragment;
+    @Mock
+    private FragmentManager mFragmentManager;
 
     @Before
     public void setUp() {
@@ -67,6 +76,10 @@
         mFakeFeatureFactory.panelFeatureProvider = mPanelFeatureProvider;
         mFakePanelContent = new FakePanelContent();
         doReturn(mFakePanelContent).when(mPanelFeatureProvider).getPanel(any(), any());
+
+        mSettingsPanelActivity.mPanelFragment = mPanelFragment;
+        when(mFragmentManager.findFragmentById(R.id.main_content)).thenReturn(mPanelFragment);
+        when(mSettingsPanelActivity.getSupportFragmentManager()).thenReturn(mFragmentManager);
     }
 
     @Test
@@ -142,10 +155,61 @@
     }
 
     @Test
+    public void onStop_panelIsNotCreating_shouldForceUpdate() {
+        mSettingsPanelActivity.mForceCreation = false;
+        when(mPanelFragment.isPanelCreating()).thenReturn(false);
+        mSettingsPanelActivity.mPanelFragment = mPanelFragment;
+
+        mSettingsPanelActivity.onStop();
+
+        assertThat(mSettingsPanelActivity.mForceCreation).isTrue();
+    }
+
+    @Test
+    public void onStop_panelIsCreating_shouldNotForceUpdate() {
+        mSettingsPanelActivity.mForceCreation = false;
+        when(mPanelFragment.isPanelCreating()).thenReturn(true);
+        mSettingsPanelActivity.mPanelFragment = mPanelFragment;
+
+        mSettingsPanelActivity.onStop();
+
+        assertThat(mSettingsPanelActivity.mForceCreation).isFalse();
+    }
+
+    @Test
     public void onConfigurationChanged_shouldForceUpdate() {
         mSettingsPanelActivity.mForceCreation = false;
+
         mSettingsPanelActivity.onConfigurationChanged(new Configuration());
 
         assertThat(mSettingsPanelActivity.mForceCreation).isTrue();
     }
+
+    @Test
+    public void onNewIntent_panelIsNotCreating_shouldUpdatePanel() {
+        when(mPanelFragment.isPanelCreating()).thenReturn(false);
+
+        mSettingsPanelActivity.onNewIntent(mSettingsPanelActivity.getIntent());
+
+        verify(mPanelFragment).updatePanelWithAnimation();
+    }
+
+    @Test
+    public void onNewIntent_panelIsCreating_shouldNotUpdatePanel() {
+        when(mPanelFragment.isPanelCreating()).thenReturn(true);
+
+        mSettingsPanelActivity.onNewIntent(mSettingsPanelActivity.getIntent());
+
+        verify(mPanelFragment, never()).updatePanelWithAnimation();
+    }
+
+    @Test
+    public void onNewIntent_panelIsShowingTheSameAction_shouldNotUpdatePanel() {
+        when(mPanelFragment.isPanelCreating()).thenReturn(false);
+        when(mPanelFragment.getArguments()).thenReturn(mSettingsPanelActivity.mBundle);
+
+        mSettingsPanelActivity.onNewIntent(mSettingsPanelActivity.getIntent());
+
+        verify(mPanelFragment, never()).updatePanelWithAnimation();
+    }
 }
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;
+        }
+    }
+}