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 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; + } + } +}