diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 2f53cc1e7c8..9260201af6a 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -3258,6 +3258,11 @@
android:permission="android.permission.MANAGE_SLICE_PERMISSIONS"
android:exported="true" />
+
+
> mSliceLiveData = new LinkedHashMap<>();
@@ -128,6 +129,7 @@ public class PanelFragment extends Fragment {
if (mPanelSlices != null) {
mPanelSlices.getViewTreeObserver().removeOnGlobalLayoutListener(this);
}
+ mPanelCreating = false;
}
};
@@ -141,6 +143,7 @@ public class PanelFragment extends Fragment {
mLayoutView.getViewTreeObserver()
.addOnGlobalLayoutListener(mPanelLayoutListener);
mMaxHeight = getResources().getDimensionPixelSize(R.dimen.output_switcher_slice_max_height);
+ mPanelCreating = true;
createPanelContent();
return mLayoutView;
}
@@ -154,6 +157,7 @@ public class PanelFragment extends Fragment {
* 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 @@ public class PanelFragment extends Fragment {
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 68cb8d5163a..b7b15192354 100644
--- a/src/com/android/settings/panel/SettingsPanelActivity.java
+++ b/src/com/android/settings/panel/SettingsPanelActivity.java
@@ -21,6 +21,7 @@ import static com.android.settingslib.media.MediaOutputSliceConstants.EXTRA_PACK
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 @@ import com.android.settings.core.HideNonSystemOverlayMixin;
*/
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 @@ public class SettingsPanelActivity extends FragmentActivity {
@Override
protected void onStop() {
super.onStop();
- mForceCreation = true;
+ if (mPanelFragment != null && !mPanelFragment.isPanelCreating()) {
+ mForceCreation = true;
+ }
}
@Override
@@ -104,10 +109,10 @@ public class SettingsPanelActivity extends FragmentActivity {
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 @@ public class SettingsPanelActivity extends FragmentActivity {
// 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 @@ public class SettingsPanelActivity extends FragmentActivity {
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 a5768d3f9e6..bb7def6a887 100644
--- a/src/com/android/settings/slices/CustomSliceRegistry.java
+++ b/src/com/android/settings/slices/CustomSliceRegistry.java
@@ -216,6 +216,16 @@ public class CustomSliceRegistry {
.appendPath("ring_volume")
.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.
*/
diff --git a/src/com/android/settings/slices/SettingsSliceProvider.java b/src/com/android/settings/slices/SettingsSliceProvider.java
index c22d001a2d8..5a1c4246b83 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.R;
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 @@ public class SettingsSliceProvider extends SliceProvider {
@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 @@ public class SettingsSliceProvider extends SliceProvider {
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 00000000000..bcf02e50d7f
--- /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 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 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 00000000000..f6088d07142
--- /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 833d510b7ac..4a14798b7d7 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 com.google.common.truth.Truth.assertThat;
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.os.Build;
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.Before;
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 @@ public class SettingsPanelActivityTest {
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 @@ public class SettingsPanelActivityTest {
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
@@ -141,11 +154,62 @@ public class SettingsPanelActivityTest {
& SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS).isEqualTo(0);
}
+ @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 00000000000..5e22adfb10a
--- /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;
+ }
+ }
+}