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:
@@ -3416,6 +3416,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"
|
||||
|
@@ -64,6 +64,7 @@ public abstract class AdjustVolumeRestrictedPreferenceController extends
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@@ -107,7 +107,10 @@ public abstract class VolumeSeekBarPreferenceController extends
|
||||
return mHelper.getMinVolume(getAudioStream());
|
||||
}
|
||||
|
||||
protected abstract int getAudioStream();
|
||||
/**
|
||||
* @return the audio stream type
|
||||
*/
|
||||
public abstract int getAudioStream();
|
||||
|
||||
protected abstract int getMuteIcon();
|
||||
|
||||
|
@@ -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.
|
||||
*/
|
||||
|
@@ -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,8 +394,14 @@ public class SettingsSliceProvider extends SliceProvider {
|
||||
|
||||
final IntentFilter filter = controller.getIntentFilter();
|
||||
if (filter != null) {
|
||||
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));
|
||||
|
||||
|
127
src/com/android/settings/slices/VolumeSliceHelper.java
Normal file
127
src/com/android/settings/slices/VolumeSliceHelper.java
Normal file
@@ -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 */);
|
||||
});
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user