Fix the ANR in panel when changing volume continuously

When users open volume panel and keep on changing the volume slider for
a while, the panel starts to defer the slider updating, and finally gets
stuck and causes an ANR.

Root cause:
Volume panel has four volume adjusting slices. Each of them registers
a broadcast receiver to listen to the volume changed and muted events.
However, when the media volume changes, AudioManager will send four
broadcasts (music, assistant, accessibility, tts) to every receiver, and
each of them will reload slice four times. Thus, one media volume
changed event will lead to 16 (4*4) UI updates. Consequently, keeping on
sliding the volume bar will trigger hundreds of broadcasts and UI
updates, which makes the system busy and getting stuck.

Solution:
Introduce a VolumeSliceHelper to integrate the broadcasts of the volume
slices specifically.
1. Only register one broadcast receiver to reduce the broadcast loading
   since the four slices are listening to the same signal.
2. Filter the only one eligible broadcast among the multiple concurrent
   ones, and then relay it to the registered slice.
3. Listen to one more action STREAM_DEVICES_CHANGED_ACTION to update the
   volume panel when audio output device changes.

Test: robotest, visual
Bug: 144134209
Bug: 160489394
Change-Id: I780b9eee35802b19a5f0ab0a7d07bd3e081f5556
This commit is contained in:
Jason Chiu
2020-08-20 16:22:44 +08:00
parent b802230a44
commit 2c7b77dad7
8 changed files with 451 additions and 3 deletions

View File

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