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