Fix IntentReceiver leaked on Sound & vibration

The root cause is that PreferenceController migrated to Catalyst will be
removed by DashboardFragment and VolumeSeekBarPreferenceController
onPause() is not invoked.

NO_IFTTT=Controller only

Fix: 386162594
Flag: EXEMPT bugfix
Test: atest&manual
Change-Id: I13f8588c8259ed4ddb9895ffc60e209c99c05269
This commit is contained in:
Jacky Wang
2025-01-24 15:24:23 +08:00
parent a7fa6efe8f
commit 402e5bb38b
8 changed files with 62 additions and 106 deletions

View File

@@ -28,17 +28,18 @@ import android.os.Message;
import android.service.notification.NotificationListenerService; import android.service.notification.NotificationListenerService;
import android.view.View; import android.view.View;
import androidx.lifecycle.OnLifecycleEvent; import androidx.annotation.NonNull;
import androidx.lifecycle.DefaultLifecycleObserver;
import androidx.lifecycle.LifecycleOwner;
import androidx.preference.PreferenceScreen; import androidx.preference.PreferenceScreen;
import com.android.settings.R; import com.android.settings.R;
import com.android.settingslib.core.lifecycle.Lifecycle;
/** /**
* Update notification volume icon in Settings in response to user adjusting volume. * Update notification volume icon in Settings in response to user adjusting volume.
*/ */
public class NotificationVolumePreferenceController extends public class NotificationVolumePreferenceController extends
RingerModeAffectedVolumePreferenceController { RingerModeAffectedVolumePreferenceController implements DefaultLifecycleObserver {
private static final String KEY_NOTIFICATION_VOLUME = "notification_volume"; private static final String KEY_NOTIFICATION_VOLUME = "notification_volume";
private static final String TAG = "NotificationVolumePreferenceController"; private static final String TAG = "NotificationVolumePreferenceController";
@@ -80,17 +81,13 @@ public class NotificationVolumePreferenceController extends
updateEnabledState(); updateEnabledState();
} }
@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
@Override @Override
public void onResume() { public void onResume(@NonNull LifecycleOwner owner) {
super.onResume();
mReceiver.register(true); mReceiver.register(true);
} }
@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
@Override @Override
public void onPause() { public void onPause(@NonNull LifecycleOwner owner) {
super.onPause();
mReceiver.register(false); mReceiver.register(false);
} }

View File

@@ -49,8 +49,7 @@ public class RemoteVolumeSeekBarPreference extends VolumeSeekBarPreference {
} }
@Override @Override
protected void init() { protected void onBindViewHolder() {
if (mSeekBar == null) return;
setContinuousUpdates(true); setContinuousUpdates(true);
updateIconView(); updateIconView();
updateSuppressionText(); updateSuppressionText();

View File

@@ -27,16 +27,17 @@ import android.os.Looper;
import android.os.Message; import android.os.Message;
import android.service.notification.NotificationListenerService; import android.service.notification.NotificationListenerService;
import androidx.lifecycle.OnLifecycleEvent; import androidx.annotation.NonNull;
import androidx.lifecycle.DefaultLifecycleObserver;
import androidx.lifecycle.LifecycleOwner;
import com.android.settings.R; import com.android.settings.R;
import com.android.settingslib.core.lifecycle.Lifecycle;
/** /**
* This slider represents both ring and notification * This slider represents both ring and notification
*/ */
public class RingVolumePreferenceController extends public class RingVolumePreferenceController extends
RingerModeAffectedVolumePreferenceController { RingerModeAffectedVolumePreferenceController implements DefaultLifecycleObserver {
private static final String KEY_RING_VOLUME = "ring_volume"; private static final String KEY_RING_VOLUME = "ring_volume";
private static final String TAG = "RingVolumePreferenceController"; private static final String TAG = "RingVolumePreferenceController";
@@ -58,10 +59,8 @@ public class RingVolumePreferenceController extends
updateRingerMode(); updateRingerMode();
} }
@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
@Override @Override
public void onResume() { public void onResume(@NonNull LifecycleOwner owner) {
super.onResume();
mReceiver.register(true); mReceiver.register(true);
updateEffectsSuppressor(); updateEffectsSuppressor();
selectPreferenceIconState(); selectPreferenceIconState();
@@ -71,10 +70,8 @@ public class RingVolumePreferenceController extends
} }
} }
@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
@Override @Override
public void onPause() { public void onPause(@NonNull LifecycleOwner owner) {
super.onPause();
mReceiver.register(false); mReceiver.register(false);
} }

View File

@@ -27,17 +27,18 @@ import android.os.Looper;
import android.os.Message; import android.os.Message;
import android.service.notification.NotificationListenerService; import android.service.notification.NotificationListenerService;
import androidx.lifecycle.OnLifecycleEvent; import androidx.annotation.NonNull;
import androidx.lifecycle.DefaultLifecycleObserver;
import androidx.lifecycle.LifecycleOwner;
import com.android.settings.R; import com.android.settings.R;
import com.android.settingslib.core.lifecycle.Lifecycle;
/** /**
* This slider is used to represent ring volume when ring is separated from notification * This slider is used to represent ring volume when ring is separated from notification
*/ */
// LINT.IfChange // LINT.IfChange
public class SeparateRingVolumePreferenceController extends public class SeparateRingVolumePreferenceController extends
RingerModeAffectedVolumePreferenceController { RingerModeAffectedVolumePreferenceController implements DefaultLifecycleObserver {
private static final String KEY_SEPARATE_RING_VOLUME = "separate_ring_volume"; private static final String KEY_SEPARATE_RING_VOLUME = "separate_ring_volume";
private static final String TAG = "SeparateRingVolumePreferenceController"; private static final String TAG = "SeparateRingVolumePreferenceController";
@@ -59,10 +60,8 @@ public class SeparateRingVolumePreferenceController extends
updateRingerMode(); updateRingerMode();
} }
@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
@Override @Override
public void onResume() { public void onResume(@NonNull LifecycleOwner owner) {
super.onResume();
mReceiver.register(true); mReceiver.register(true);
updateEffectsSuppressor(); updateEffectsSuppressor();
selectPreferenceIconState(); selectPreferenceIconState();
@@ -73,10 +72,8 @@ public class SeparateRingVolumePreferenceController extends
} }
} }
@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
@Override @Override
public void onPause() { public void onPause(@NonNull LifecycleOwner owner) {
super.onPause();
mReceiver.register(false); mReceiver.register(false);
} }

View File

@@ -62,7 +62,6 @@ public class VolumeSeekBarPreference extends SeekBarPreference {
private boolean mZenMuted; private boolean mZenMuted;
private int mIconResId; private int mIconResId;
private int mMuteIconResId; private int mMuteIconResId;
private boolean mStopped;
@VisibleForTesting @VisibleForTesting
AudioManager mAudioManager; AudioManager mAudioManager;
private Locale mLocale; private Locale mLocale;
@@ -114,18 +113,13 @@ public class VolumeSeekBarPreference extends SeekBarPreference {
mListener = listener; mListener = listener;
} }
public void onActivityResume() { @Override
if (mStopped) { public void onDetached() {
init();
}
}
public void onActivityPause() {
mStopped = true;
if (mVolumizer != null) { if (mVolumizer != null) {
mVolumizer.stop(); mVolumizer.stop();
mVolumizer = null; mVolumizer = null;
} }
super.onDetached();
} }
@Override @Override
@@ -135,16 +129,25 @@ public class VolumeSeekBarPreference extends SeekBarPreference {
mIconView = (ImageView) view.findViewById(com.android.internal.R.id.icon); mIconView = (ImageView) view.findViewById(com.android.internal.R.id.icon);
mSuppressionTextView = (TextView) view.findViewById(R.id.suppression_text); mSuppressionTextView = (TextView) view.findViewById(R.id.suppression_text);
mTitle = (TextView) view.findViewById(com.android.internal.R.id.title); mTitle = (TextView) view.findViewById(com.android.internal.R.id.title);
init(); onBindViewHolder();
} }
protected void init() { protected void onBindViewHolder() {
if (mSeekBar == null) return; boolean isEnabled = isEnabled();
// It's unnecessary to set up relevant volumizer configuration if preference is disabled. mSeekBar.setEnabled(isEnabled);
if (!isEnabled()) { if (mVolumizer == null) {
mSeekBar.setEnabled(false); createSeekBarVolumizer();
return;
} }
mVolumizer.setSeekBar(mSeekBar);
updateIconView();
updateSuppressionText();
if (isEnabled && mListener != null) {
mListener.onUpdateMuteState();
}
}
@SuppressWarnings("NullAway")
protected void createSeekBarVolumizer() {
final SeekBarVolumizer.Callback sbvc = new SeekBarVolumizer.Callback() { final SeekBarVolumizer.Callback sbvc = new SeekBarVolumizer.Callback() {
@Override @Override
public void onSampleStarting(SeekBarVolumizer sbv) { public void onSampleStarting(SeekBarVolumizer sbv) {
@@ -184,16 +187,8 @@ public class VolumeSeekBarPreference extends SeekBarPreference {
} }
}; };
final Uri sampleUri = mStream == AudioManager.STREAM_MUSIC ? getMediaVolumeUri() : null; final Uri sampleUri = mStream == AudioManager.STREAM_MUSIC ? getMediaVolumeUri() : null;
if (mVolumizer == null) {
mVolumizer = mSeekBarVolumizerFactory.create(mStream, sampleUri, sbvc); mVolumizer = mSeekBarVolumizerFactory.create(mStream, sampleUri, sbvc);
}
mVolumizer.start(); mVolumizer.start();
mVolumizer.setSeekBar(mSeekBar);
updateIconView();
updateSuppressionText();
if (mListener != null) {
mListener.onUpdateMuteState();
}
} }
protected void updateIconView() { protected void updateIconView() {

View File

@@ -19,18 +19,15 @@ package com.android.settings.notification;
import android.content.Context; import android.content.Context;
import androidx.annotation.VisibleForTesting; import androidx.annotation.VisibleForTesting;
import androidx.lifecycle.LifecycleObserver;
import androidx.lifecycle.OnLifecycleEvent;
import androidx.preference.PreferenceScreen; import androidx.preference.PreferenceScreen;
import com.android.settings.R; import com.android.settings.R;
import com.android.settingslib.core.lifecycle.Lifecycle;
/** /**
* Base class for preference controller that handles VolumeSeekBarPreference * Base class for preference controller that handles VolumeSeekBarPreference
*/ */
public abstract class VolumeSeekBarPreferenceController extends public abstract class VolumeSeekBarPreferenceController extends
AdjustVolumeRestrictedPreferenceController implements LifecycleObserver { AdjustVolumeRestrictedPreferenceController {
protected VolumeSeekBarPreference mPreference; protected VolumeSeekBarPreference mPreference;
protected AudioHelper mHelper; protected AudioHelper mHelper;
@@ -61,20 +58,6 @@ public abstract class VolumeSeekBarPreferenceController extends
mPreference.setMuteIcon(getMuteIcon()); mPreference.setMuteIcon(getMuteIcon());
} }
@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
public void onResume() {
if (mPreference != null) {
mPreference.onActivityResume();
}
}
@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
public void onPause() {
if (mPreference != null) {
mPreference.onActivityPause();
}
}
@Override @Override
public int getSliceHighlightMenuRes() { public int getSliceHighlightMenuRes() {
return R.string.menu_key_sound; return R.string.menu_key_sound;

View File

@@ -88,24 +88,6 @@ public class VolumeSeekBarPreferenceControllerTest {
verify(mPreference, never()).setListener(mListener); verify(mPreference, never()).setListener(mListener);
} }
@Test
public void onResume_shouldResumePreference() {
mController.displayPreference(mScreen);
mController.onResume();
verify(mPreference).onActivityResume();
}
@Test
public void onPause_shouldPausePreference() {
mController.displayPreference(mScreen);
mController.onPause();
verify(mPreference).onActivityPause();
}
@Test @Test
public void sliderMethods_handleNullPreference() { public void sliderMethods_handleNullPreference() {
when(mHelper.getStreamVolume(mController.getAudioStream())).thenReturn(4); when(mHelper.getStreamVolume(mController.getAudioStream())).thenReturn(4);

View File

@@ -32,12 +32,14 @@ import android.preference.SeekBarVolumizer;
import android.widget.SeekBar; import android.widget.SeekBar;
import org.junit.Before; import org.junit.Before;
import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor; import org.mockito.ArgumentCaptor;
import org.mockito.Captor; import org.mockito.Captor;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.MockitoAnnotations; import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import org.robolectric.RobolectricTestRunner; import org.robolectric.RobolectricTestRunner;
import java.util.Locale; import java.util.Locale;
@@ -47,6 +49,10 @@ public class VolumeSeekBarPreferenceTest {
private static final CharSequence CONTENT_DESCRIPTION = "TEST"; private static final CharSequence CONTENT_DESCRIPTION = "TEST";
private static final int STREAM = 5; private static final int STREAM = 5;
@Rule
public final MockitoRule mMockitoRule = MockitoJUnit.rule();
@Mock @Mock
private AudioManager mAudioManager; private AudioManager mAudioManager;
@Mock @Mock
@@ -70,10 +76,10 @@ public class VolumeSeekBarPreferenceTest {
@Before @Before
public void setUp() { public void setUp() {
MockitoAnnotations.initMocks(this);
when(mContext.getSystemService(Context.AUDIO_SERVICE)).thenReturn(mAudioManager); when(mContext.getSystemService(Context.AUDIO_SERVICE)).thenReturn(mAudioManager);
when(mSeekBarVolumizerFactory.create(eq(STREAM), eq(null), mSbvc.capture())) when(mSeekBarVolumizerFactory.create(eq(STREAM), eq(null), mSbvc.capture()))
.thenReturn(mVolumizer); .thenReturn(mVolumizer);
doCallRealMethod().when(mPreference).createSeekBarVolumizer();
doCallRealMethod().when(mPreference).setStream(anyInt()); doCallRealMethod().when(mPreference).setStream(anyInt());
doCallRealMethod().when(mPreference).updateContentDescription(CONTENT_DESCRIPTION); doCallRealMethod().when(mPreference).updateContentDescription(CONTENT_DESCRIPTION);
mPreference.mSeekBar = mSeekBar; mPreference.mSeekBar = mSeekBar;
@@ -99,50 +105,50 @@ public class VolumeSeekBarPreferenceTest {
} }
@Test @Test
public void init_listenerIsCalled() { public void onBindViewHolder_listenerIsCalled() {
when(mPreference.isEnabled()).thenReturn(true); when(mPreference.isEnabled()).thenReturn(true);
doCallRealMethod().when(mPreference).setListener(mListener); doCallRealMethod().when(mPreference).setListener(mListener);
doCallRealMethod().when(mPreference).init(); doCallRealMethod().when(mPreference).onBindViewHolder();
mPreference.setStream(STREAM); mPreference.setStream(STREAM);
mPreference.setListener(mListener); mPreference.setListener(mListener);
mPreference.init(); mPreference.onBindViewHolder();
verify(mPreference).updateContentDescription(CONTENT_DESCRIPTION); verify(mPreference).updateContentDescription(CONTENT_DESCRIPTION);
} }
@Test @Test
public void init_listenerNotSet_noException() { public void onBindViewHolder_listenerNotSet_noException() {
when(mPreference.isEnabled()).thenReturn(true); when(mPreference.isEnabled()).thenReturn(true);
doCallRealMethod().when(mPreference).init(); doCallRealMethod().when(mPreference).onBindViewHolder();
mPreference.setStream(STREAM); mPreference.setStream(STREAM);
mPreference.init(); mPreference.onBindViewHolder();
verify(mPreference, never()).updateContentDescription(CONTENT_DESCRIPTION); verify(mPreference, never()).updateContentDescription(CONTENT_DESCRIPTION);
} }
@Test @Test
public void init_preferenceIsDisabled_shouldNotInvokeListener() { public void onBindViewHolder_preferenceIsDisabled_shouldNotInvokeListener() {
when(mPreference.isEnabled()).thenReturn(false); when(mPreference.isEnabled()).thenReturn(false);
doCallRealMethod().when(mPreference).setListener(mListener); doCallRealMethod().when(mPreference).setListener(mListener);
doCallRealMethod().when(mPreference).init(); doCallRealMethod().when(mPreference).onBindViewHolder();
mPreference.setStream(STREAM); mPreference.setStream(STREAM);
mPreference.init(); mPreference.onBindViewHolder();
verify(mPreference, never()).updateContentDescription(CONTENT_DESCRIPTION); verify(mPreference, never()).updateContentDescription(CONTENT_DESCRIPTION);
} }
@Test @Test
public void init_changeProgress_overrideStateDescriptionCalled() { public void onBindViewHolder_changeProgress_overrideStateDescriptionCalled() {
final int progress = 4; final int progress = 4;
when(mPreference.isEnabled()).thenReturn(true); when(mPreference.isEnabled()).thenReturn(true);
when(mPreference.formatStateDescription(progress)).thenReturn(CONTENT_DESCRIPTION); when(mPreference.formatStateDescription(progress)).thenReturn(CONTENT_DESCRIPTION);
doCallRealMethod().when(mPreference).init(); doCallRealMethod().when(mPreference).onBindViewHolder();
mPreference.setStream(STREAM); mPreference.setStream(STREAM);
mPreference.init(); mPreference.onBindViewHolder();
verify(mSeekBarVolumizerFactory).create(eq(STREAM), eq(null), mSbvc.capture()); verify(mSeekBarVolumizerFactory).create(eq(STREAM), eq(null), mSbvc.capture());
@@ -166,10 +172,10 @@ public class VolumeSeekBarPreferenceTest {
when(mContext.getResources()).thenReturn(mRes); when(mContext.getResources()).thenReturn(mRes);
when(mRes.getConfiguration()).thenReturn(mConfig); when(mRes.getConfiguration()).thenReturn(mConfig);
when(mConfig.getLocales()).thenReturn(new LocaleList(Locale.US)); when(mConfig.getLocales()).thenReturn(new LocaleList(Locale.US));
doCallRealMethod().when(mPreference).init(); doCallRealMethod().when(mPreference).onBindViewHolder();
mPreference.setStream(STREAM); mPreference.setStream(STREAM);
mPreference.init(); mPreference.onBindViewHolder();
verify(mSeekBarVolumizerFactory).create(eq(STREAM), eq(null), mSbvc.capture()); verify(mSeekBarVolumizerFactory).create(eq(STREAM), eq(null), mSbvc.capture());