diff --git a/src/com/android/settings/network/ActiveSubsciptionsListener.java b/src/com/android/settings/network/ActiveSubsciptionsListener.java index cb3e0618a1f..d9d0bb575d5 100644 --- a/src/com/android/settings/network/ActiveSubsciptionsListener.java +++ b/src/com/android/settings/network/ActiveSubsciptionsListener.java @@ -20,6 +20,8 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.os.Handler; +import android.os.Looper; import android.telephony.CarrierConfigManager; import android.telephony.SubscriptionInfo; import android.telephony.SubscriptionManager; @@ -31,6 +33,7 @@ import androidx.annotation.VisibleForTesting; import com.android.internal.telephony.TelephonyIntents; import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; /** * A listener for active subscription change @@ -43,18 +46,26 @@ public abstract class ActiveSubsciptionsListener /** * Constructor * - * @param context of this listener + * @param looper {@code Looper} of this listener + * @param context {@code Context} of this listener */ - public ActiveSubsciptionsListener(Context context) { + public ActiveSubsciptionsListener(Looper looper, Context context) { + mLooper = looper; mContext = context; + mCacheState = new AtomicInteger(STATE_NOT_LISTENING); + mMaxActiveSubscriptionInfos = new AtomicInteger(MAX_SUBSCRIPTION_UNKNOWN); + mSubscriptionChangeIntentFilter = new IntentFilter(); mSubscriptionChangeIntentFilter.addAction( CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED); mSubscriptionChangeIntentFilter.addAction( TelephonyIntents.ACTION_RADIO_TECHNOLOGY_CHANGED); + } - mSubscriptionChangeReceiver = new BroadcastReceiver() { + @VisibleForTesting + BroadcastReceiver getSubscriptionChangeReceiver() { + return new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { if (isInitialStickyBroadcast()) { @@ -77,18 +88,24 @@ public abstract class ActiveSubsciptionsListener }; } + private Looper mLooper; private Context mContext; - private boolean mIsMonitoringDataChange; - private boolean mIsCachedDataAvailable; + private static final int STATE_NOT_LISTENING = 0; + private static final int STATE_STOPPING = 1; + private static final int STATE_PREPARING = 2; + private static final int STATE_LISTENING = 3; + private static final int STATE_DATA_CACHED = 4; + + private AtomicInteger mCacheState; private SubscriptionManager mSubscriptionManager; private IntentFilter mSubscriptionChangeIntentFilter; + private BroadcastReceiver mSubscriptionChangeReceiver; - @VisibleForTesting - BroadcastReceiver mSubscriptionChangeReceiver; + private static final int MAX_SUBSCRIPTION_UNKNOWN = -1; - private Integer mMaxActiveSubscriptionInfos; + private AtomicInteger mMaxActiveSubscriptionInfos; private List mCachedActiveSubscriptionInfo; /** @@ -135,16 +152,14 @@ public abstract class ActiveSubsciptionsListener * @return max. number of active subscription info(s) */ public int getActiveSubscriptionInfoCountMax() { - int count = 0; - if (mMaxActiveSubscriptionInfos == null) { - count = getSubscriptionManager().getActiveSubscriptionInfoCountMax(); - if (mIsMonitoringDataChange) { - mMaxActiveSubscriptionInfos = count; - } - } else { - count = mMaxActiveSubscriptionInfos.intValue(); + int cacheState = mCacheState.get(); + if (cacheState < STATE_LISTENING) { + return getSubscriptionManager().getActiveSubscriptionInfoCountMax(); } - return count; + + mMaxActiveSubscriptionInfos.compareAndSet(MAX_SUBSCRIPTION_UNKNOWN, + getSubscriptionManager().getActiveSubscriptionInfoCountMax()); + return mMaxActiveSubscriptionInfos.get(); } /** @@ -153,11 +168,11 @@ public abstract class ActiveSubsciptionsListener * @return A list of active subscription info */ public List getActiveSubscriptionsInfo() { - if (mIsCachedDataAvailable) { + if (mCacheState.get() >= STATE_DATA_CACHED) { return mCachedActiveSubscriptionInfo; } - mIsCachedDataAvailable = mIsMonitoringDataChange; mCachedActiveSubscriptionInfo = getSubscriptionManager().getActiveSubscriptionInfoList(); + mCacheState.compareAndSet(STATE_LISTENING, STATE_DATA_CACHED); if ((mCachedActiveSubscriptionInfo == null) || (mCachedActiveSubscriptionInfo.size() <= 0)) { @@ -208,7 +223,7 @@ public abstract class ActiveSubsciptionsListener * @return A subscription info which is accessible list */ public SubscriptionInfo getAccessibleSubscriptionInfo(int subId) { - if (mIsCachedDataAvailable) { + if (mCacheState.get() >= STATE_DATA_CACHED) { final SubscriptionInfo activeSubInfo = getActiveSubscriptionInfo(subId); if (activeSubInfo != null) { return activeSubInfo; @@ -231,37 +246,49 @@ public abstract class ActiveSubsciptionsListener * Clear data cached within listener */ public void clearCache() { - mIsCachedDataAvailable = false; - mMaxActiveSubscriptionInfos = null; + mMaxActiveSubscriptionInfos.set(MAX_SUBSCRIPTION_UNKNOWN); + mCacheState.compareAndSet(STATE_DATA_CACHED, STATE_LISTENING); mCachedActiveSubscriptionInfo = null; } private void monitorSubscriptionsChange(boolean on) { - if (mIsMonitoringDataChange == on) { + if (on) { + if (!mCacheState.compareAndSet(STATE_NOT_LISTENING, STATE_PREPARING)) { + return; + } + + if (mSubscriptionChangeReceiver == null) { + mSubscriptionChangeReceiver = getSubscriptionChangeReceiver(); + } + mContext.registerReceiver(mSubscriptionChangeReceiver, + mSubscriptionChangeIntentFilter, null, new Handler(mLooper)); + getSubscriptionManager().addOnSubscriptionsChangedListener(this); + mCacheState.compareAndSet(STATE_PREPARING, STATE_LISTENING); return; } - mIsMonitoringDataChange = on; - if (on) { - mContext.registerReceiver(mSubscriptionChangeReceiver, - mSubscriptionChangeIntentFilter); - getSubscriptionManager().addOnSubscriptionsChangedListener(this); - listenerNotify(); - } else { - mContext.unregisterReceiver(mSubscriptionChangeReceiver); - getSubscriptionManager().removeOnSubscriptionsChangedListener(this); - clearCache(); + + final int currentState = mCacheState.getAndSet(STATE_STOPPING); + if (currentState <= STATE_STOPPING) { + mCacheState.compareAndSet(STATE_STOPPING, currentState); + return; } + if (mSubscriptionChangeReceiver != null) { + mContext.unregisterReceiver(mSubscriptionChangeReceiver); + } + getSubscriptionManager().removeOnSubscriptionsChangedListener(this); + clearCache(); + mCacheState.compareAndSet(STATE_STOPPING, STATE_NOT_LISTENING); } private void listenerNotify() { - if (!mIsMonitoringDataChange) { + if (mCacheState.get() < STATE_LISTENING) { return; } onChanged(); } private boolean clearCachedSubId(int subId) { - if (!mIsCachedDataAvailable) { + if (mCacheState.get() < STATE_DATA_CACHED) { return false; } if (mCachedActiveSubscriptionInfo == null) { diff --git a/src/com/android/settings/network/GlobalSettingsChangeListener.java b/src/com/android/settings/network/GlobalSettingsChangeListener.java index 89d374b582e..4c58c609ee5 100644 --- a/src/com/android/settings/network/GlobalSettingsChangeListener.java +++ b/src/com/android/settings/network/GlobalSettingsChangeListener.java @@ -24,12 +24,15 @@ import android.content.Context; import android.database.ContentObserver; import android.net.Uri; import android.os.Handler; +import android.os.Looper; import android.provider.Settings; import androidx.lifecycle.Lifecycle; import androidx.lifecycle.LifecycleObserver; import androidx.lifecycle.OnLifecycleEvent; +import java.util.concurrent.atomic.AtomicBoolean; + /** * A listener for Settings.Global configuration change, with support of Lifecycle */ @@ -39,19 +42,33 @@ public abstract class GlobalSettingsChangeListener extends ContentObserver /** * Constructor * - * @param context of this listener + * @param context {@code Context} of this listener * @param field field of Global Settings */ public GlobalSettingsChangeListener(Context context, String field) { - super(new Handler()); + this(Looper.getMainLooper(), context, field); + } + + /** + * Constructor + * + * @param looper {@code Looper} for processing callback + * @param context {@code Context} of this listener + * @param field field of Global Settings + */ + public GlobalSettingsChangeListener(Looper looper, Context context, String field) { + super(new Handler(looper)); mContext = context; mField = field; + mUri = Settings.Global.getUriFor(field); + mListening = new AtomicBoolean(false); monitorUri(true); } private Context mContext; private String mField; private Uri mUri; + private AtomicBoolean mListening; private Lifecycle mLifecycle; /** @@ -75,7 +92,7 @@ public abstract class GlobalSettingsChangeListener extends ContentObserver } public void onChange(boolean selfChange) { - if (!isMonitoring()) { + if (!mListening.get()) { return; } onChanged(mField); @@ -104,20 +121,16 @@ public abstract class GlobalSettingsChangeListener extends ContentObserver notifyChangeBasedOn(null); } - private boolean isMonitoring() { - return (mUri != null); - } - private void monitorUri(boolean on) { - if (isMonitoring() == on) { + if (!mListening.compareAndSet(!on, on)) { return; } - if (mUri == null) { - mUri = Settings.Global.getUriFor(mField); + + if (on) { mContext.getContentResolver().registerContentObserver(mUri, false, this); return; } - mUri = null; + mContext.getContentResolver().unregisterContentObserver(this); } } diff --git a/src/com/android/settings/network/ProxySubscriptionManager.java b/src/com/android/settings/network/ProxySubscriptionManager.java index 18b4ac9a101..8f3f38541ed 100644 --- a/src/com/android/settings/network/ProxySubscriptionManager.java +++ b/src/com/android/settings/network/ProxySubscriptionManager.java @@ -21,6 +21,7 @@ import static androidx.lifecycle.Lifecycle.Event.ON_START; import static androidx.lifecycle.Lifecycle.Event.ON_STOP; import android.content.Context; +import android.os.Looper; import android.provider.Settings; import android.telephony.SubscriptionInfo; import android.telephony.SubscriptionManager; @@ -71,18 +72,18 @@ public class ProxySubscriptionManager implements LifecycleObserver { private static ProxySubscriptionManager sSingleton; private ProxySubscriptionManager(Context context) { - mContext = context; + final Looper looper = Looper.getMainLooper(); mActiveSubscriptionsListeners = new ArrayList(); - mSubsciptionsMonitor = new ActiveSubsciptionsListener(context) { + mSubsciptionsMonitor = new ActiveSubsciptionsListener(looper, context) { public void onChanged() { notifyAllListeners(); } }; - mAirplaneModeMonitor = new GlobalSettingsChangeListener(context, - Settings.Global.AIRPLANE_MODE_ON) { + mAirplaneModeMonitor = new GlobalSettingsChangeListener(looper, + context, Settings.Global.AIRPLANE_MODE_ON) { public void onChanged(String field) { mSubsciptionsMonitor.clearCache(); notifyAllListeners(); @@ -93,7 +94,6 @@ public class ProxySubscriptionManager implements LifecycleObserver { } private Lifecycle mLifecycle; - private Context mContext; private ActiveSubsciptionsListener mSubsciptionsMonitor; private GlobalSettingsChangeListener mAirplaneModeMonitor; @@ -140,7 +140,7 @@ public class ProxySubscriptionManager implements LifecycleObserver { @OnLifecycleEvent(ON_DESTROY) void onDestroy() { - mSubsciptionsMonitor.stop(); + mAirplaneModeMonitor.close(); if (mLifecycle != null) { mLifecycle.removeObserver(this); diff --git a/tests/robotests/src/com/android/settings/network/ActiveSubsciptionsListenerTest.java b/tests/robotests/src/com/android/settings/network/ActiveSubsciptionsListenerTest.java index e124eb76f70..7f23699455b 100644 --- a/tests/robotests/src/com/android/settings/network/ActiveSubsciptionsListenerTest.java +++ b/tests/robotests/src/com/android/settings/network/ActiveSubsciptionsListenerTest.java @@ -16,111 +16,157 @@ package com.android.settings.network; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import static org.robolectric.Shadows.shadowOf; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; +import android.os.Looper; import android.telephony.CarrierConfigManager; import android.telephony.SubscriptionInfo; import android.telephony.SubscriptionManager; +import android.telephony.TelephonyManager; + +import androidx.test.ext.junit.runners.AndroidJUnit4; import com.android.internal.telephony.TelephonyIntents; +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.shadow.api.Shadow; +import org.robolectric.shadows.ShadowBroadcastReceiver; +import org.robolectric.shadows.ShadowContextImpl; +import org.robolectric.shadows.ShadowSubscriptionManager; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; -@RunWith(RobolectricTestRunner.class) +@RunWith(AndroidJUnit4.class) public class ActiveSubsciptionsListenerTest { + private static final int SUB_ID1 = 3; + private static final int SUB_ID2 = 7; - @Mock - private SubscriptionManager mSubscriptionManager; - @Mock - private SubscriptionInfo mSubscriptionInfo1; - @Mock - private SubscriptionInfo mSubscriptionInfo2; + private static final Intent INTENT_RADIO_TECHNOLOGY_CHANGED = + new Intent(TelephonyIntents.ACTION_RADIO_TECHNOLOGY_CHANGED); + + private static final Intent INTENT_MULTI_SIM_CONFIG_CHANGED = + new Intent(TelephonyManager.ACTION_MULTI_SIM_CONFIG_CHANGED); + + private static final Intent INTENT_CARRIER_CONFIG_CHANGED = + new Intent(CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED); private Context mContext; - private ActiveSubsciptionsListener mListener; + private ShadowContextImpl mShadowContextImpl; + private SubscriptionManager mSubscriptionManager; + private ShadowSubscriptionManager mShadowSubscriptionManager; private List mActiveSubscriptions; - private BroadcastReceiver mSubscriptionChangeReceiver; + + private ActiveSubsciptionsListenerImpl mListener; + private BroadcastReceiver mReceiver; + private ShadowBroadcastReceiver mShadowReceiver; @Before public void setUp() { MockitoAnnotations.initMocks(this); - mContext = spy(RuntimeEnvironment.application); - when(mContext.getSystemService(SubscriptionManager.class)).thenReturn(mSubscriptionManager); + + mContext = RuntimeEnvironment.application.getBaseContext(); + mShadowContextImpl = Shadow.extract(mContext); + + mSubscriptionManager = spy(mContext.getSystemService(SubscriptionManager.class)); + mShadowSubscriptionManager = shadowOf(mSubscriptionManager); + mActiveSubscriptions = new ArrayList(); + mActiveSubscriptions.add(ShadowSubscriptionManager.SubscriptionInfoBuilder + .newBuilder().setId(SUB_ID1).buildSubscriptionInfo()); + mActiveSubscriptions.add(ShadowSubscriptionManager.SubscriptionInfoBuilder + .newBuilder().setId(SUB_ID2).buildSubscriptionInfo()); + mShadowSubscriptionManager.setActiveSubscriptionInfoList(mActiveSubscriptions); + + mListener = spy(new ActiveSubsciptionsListenerImpl(Looper.getMainLooper(), mContext)); + doReturn(mSubscriptionManager).when(mListener).getSubscriptionManager(); + mReceiver = mListener.getSubscriptionChangeReceiver(); + mShadowReceiver = shadowOf(mReceiver); + doReturn(mReceiver).when(mListener).getSubscriptionChangeReceiver(); + } + + @After + public void cleanUp() { + mListener.stop(); + } + + private class ActiveSubsciptionsListenerImpl extends ActiveSubsciptionsListener { + private ActiveSubsciptionsListenerImpl(Looper looper, Context context) { + super(looper, context); + } + public void onChanged() {} + } + + private void sendIntentToReceiver(Intent intent) { + mShadowReceiver.onReceive(mContext, intent, new AtomicBoolean(false)); } @Test public void constructor_noListeningWasSetup() { - mListener = spy(new ActiveSubsciptionsListener(mContext) { - public void onChanged() {} - }); - verify(mSubscriptionManager, never()).addOnSubscriptionsChangedListener(any()); - verify(mContext, never()).registerReceiver(any(), any()); verify(mListener, never()).onChanged(); } @Test - public void start_onChangedShouldAlwaysBeCalled() { - mListener = spy(new ActiveSubsciptionsListener(mContext) { - public void onChanged() {} - }); - mSubscriptionChangeReceiver = spy(mListener.mSubscriptionChangeReceiver); - when(mSubscriptionChangeReceiver.isInitialStickyBroadcast()).thenReturn(false); - - mActiveSubscriptions.add(mSubscriptionInfo1); - mActiveSubscriptions.add(mSubscriptionInfo2); - when(mSubscriptionManager.getActiveSubscriptionInfoList()).thenReturn(mActiveSubscriptions); - - final Intent intentSubscription = - new Intent(CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED); - final Intent intentRadioTech = - new Intent(TelephonyIntents.ACTION_RADIO_TECHNOLOGY_CHANGED); - - mSubscriptionChangeReceiver.onReceive(mContext, intentSubscription); - mSubscriptionChangeReceiver.onReceive(mContext, intentRadioTech); + public void start_configChangedIntent_onChangedShouldBeCalled() { + sendIntentToReceiver(INTENT_RADIO_TECHNOLOGY_CHANGED); + sendIntentToReceiver(INTENT_MULTI_SIM_CONFIG_CHANGED); verify(mListener, never()).onChanged(); mListener.start(); - mSubscriptionChangeReceiver.onReceive(mContext, intentSubscription); - verify(mListener, atLeastOnce()).onChanged(); + sendIntentToReceiver(INTENT_RADIO_TECHNOLOGY_CHANGED); + verify(mListener, times(1)).onChanged(); - mSubscriptionChangeReceiver.onReceive(mContext, intentRadioTech); + sendIntentToReceiver(INTENT_MULTI_SIM_CONFIG_CHANGED); + verify(mListener, times(2)).onChanged(); + + mListener.stop(); + + sendIntentToReceiver(INTENT_RADIO_TECHNOLOGY_CHANGED); + sendIntentToReceiver(INTENT_MULTI_SIM_CONFIG_CHANGED); + verify(mListener, times(2)).onChanged(); + } + + @Test + public void start_carrierConfigChangedIntent_onChangedWhenSubIdBeenCached() { + sendIntentToReceiver(INTENT_CARRIER_CONFIG_CHANGED); + verify(mListener, never()).onChanged(); + + mListener.start(); + + mListener.getActiveSubscriptionsInfo(); + + sendIntentToReceiver(INTENT_CARRIER_CONFIG_CHANGED); + verify(mListener, never()).onChanged(); + + INTENT_CARRIER_CONFIG_CHANGED.putExtra(CarrierConfigManager.EXTRA_SUBSCRIPTION_INDEX, + SUB_ID2); + sendIntentToReceiver(INTENT_CARRIER_CONFIG_CHANGED); verify(mListener, times(1)).onChanged(); mListener.stop(); - mContext.sendStickyBroadcast(intentSubscription); - mContext.sendStickyBroadcast(intentRadioTech); + sendIntentToReceiver(INTENT_CARRIER_CONFIG_CHANGED); verify(mListener, times(1)).onChanged(); } + @Test public void start_alwaysFetchAndCacheResult() { - mListener = spy(new ActiveSubsciptionsListener(mContext) { - public void onChanged() {} - }); - mActiveSubscriptions.add(mSubscriptionInfo1); - mActiveSubscriptions.add(mSubscriptionInfo2); - mListener.start(); List subInfoList = null; @@ -130,8 +176,7 @@ public class ActiveSubsciptionsListenerTest { if (mActiveSubscriptions.size() > numberOfSubInfo) { mActiveSubscriptions.remove(numberOfSubInfo); } - when(mSubscriptionManager.getActiveSubscriptionInfoList()) - .thenReturn(mActiveSubscriptions); + mShadowSubscriptionManager.setActiveSubscriptionInfoList(mActiveSubscriptions); // fetch twice and test if they generated access to SubscriptionManager only once subInfoList = mListener.getActiveSubscriptionsInfo(); @@ -143,7 +188,7 @@ public class ActiveSubsciptionsListenerTest { mListener.clearCache(); } - when(mSubscriptionManager.getActiveSubscriptionInfoList()).thenReturn(null); + mShadowSubscriptionManager.setActiveSubscriptionInfoList(null); // fetch twice and test if they generated access to SubscriptionManager only once subInfoList = mListener.getActiveSubscriptionsInfo(); diff --git a/tests/robotests/src/com/android/settings/network/GlobalSettingsChangeListenerTest.java b/tests/robotests/src/com/android/settings/network/GlobalSettingsChangeListenerTest.java index 8f41b4a7e75..e4190714a50 100644 --- a/tests/robotests/src/com/android/settings/network/GlobalSettingsChangeListenerTest.java +++ b/tests/robotests/src/com/android/settings/network/GlobalSettingsChangeListenerTest.java @@ -22,6 +22,7 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import android.content.Context; +import android.os.Looper; import android.provider.Settings; import androidx.lifecycle.Lifecycle; @@ -49,7 +50,8 @@ public class GlobalSettingsChangeListenerTest { public void setUp() { MockitoAnnotations.initMocks(this); mContext = spy(RuntimeEnvironment.application); - mListener = spy(new GlobalSettingsChangeListener(mContext, SETTINGS_FIELD) { + mListener = spy(new GlobalSettingsChangeListener(Looper.getMainLooper(), + mContext, SETTINGS_FIELD) { public void onChanged(String field) {} });