[Settings] Enabling first stage of threading
Settings app would move PreferenceController to run
in background thread in the near future.
To support this, change some of the design within these
essential components to allow:
1. Handler to run on non-main thread
2. Minimum multi-thread reentrance support
Bug: 146045802
Test: make RunSettingsRoboTests -j ROBOTEST_FILTER=GlobalSettingsChangeListenerTest
Test: make RunSettingsRoboTests -j ROBOTEST_FILTER=ActiveSubsciptionsListenerTest
Change-Id: Ica5c942b919007d7d260feee632752d3353a6f18
diff --git a/src/com/android/settings/network/ActiveSubsciptionsListener.java b/src/com/android/settings/network/ActiveSubsciptionsListener.java
index cb3e061..d9d0bb5 100644
--- a/src/com/android/settings/network/ActiveSubsciptionsListener.java
+++ b/src/com/android/settings/network/ActiveSubsciptionsListener.java
@@ -20,6 +20,8 @@
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 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 @@
/**
* 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 @@
};
}
+ 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<SubscriptionInfo> mCachedActiveSubscriptionInfo;
/**
@@ -135,16 +152,14 @@
* @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 @@
* @return A list of active subscription info
*/
public List<SubscriptionInfo> 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 @@
* @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 @@
* 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 89d374b..4c58c60 100644
--- a/src/com/android/settings/network/GlobalSettingsChangeListener.java
+++ b/src/com/android/settings/network/GlobalSettingsChangeListener.java
@@ -24,12 +24,15 @@
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 @@
/**
* 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 void onChange(boolean selfChange) {
- if (!isMonitoring()) {
+ if (!mListening.get()) {
return;
}
onChanged(mField);
@@ -104,20 +121,16 @@
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 18b4ac9..8f3f385 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_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 @@
private static ProxySubscriptionManager sSingleton;
private ProxySubscriptionManager(Context context) {
- mContext = context;
+ final Looper looper = Looper.getMainLooper();
mActiveSubscriptionsListeners =
new ArrayList<OnActiveSubscriptionChangedListener>();
- 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 @@
}
private Lifecycle mLifecycle;
- private Context mContext;
private ActiveSubsciptionsListener mSubsciptionsMonitor;
private GlobalSettingsChangeListener mAirplaneModeMonitor;
@@ -140,7 +140,7 @@
@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 e124eb7..7f23699 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<SubscriptionInfo> 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<SubscriptionInfo>();
+ 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<SubscriptionInfo> subInfoList = null;
@@ -130,8 +176,7 @@
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 @@
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 8f41b4a..e419071 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.verify;
import android.content.Context;
+import android.os.Looper;
import android.provider.Settings;
import androidx.lifecycle.Lifecycle;
@@ -49,7 +50,8 @@
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) {}
});