Merge "[Settings] Settings within each SIM not been displayed to the user" into sc-dev
diff --git a/src/com/android/settings/network/ProxySubscriptionManager.java b/src/com/android/settings/network/ProxySubscriptionManager.java
index eb1a7d4..614491a 100644
--- a/src/com/android/settings/network/ProxySubscriptionManager.java
+++ b/src/com/android/settings/network/ProxySubscriptionManager.java
@@ -25,19 +25,30 @@
import android.provider.Settings;
import android.telephony.SubscriptionInfo;
import android.telephony.SubscriptionManager;
+import android.util.Log;
+import androidx.annotation.Keep;
+import androidx.annotation.VisibleForTesting;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.LifecycleObserver;
import androidx.lifecycle.OnLifecycleEvent;
import java.util.ArrayList;
import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
/**
* A proxy to the subscription manager
*/
public class ProxySubscriptionManager implements LifecycleObserver {
+ private static final String LOG_TAG = "ProxySubscriptionManager";
+
+ private static final int LISTENER_END_OF_LIFE = -1;
+ private static final int LISTENER_IS_INACTIVE = 0;
+ private static final int LISTENER_IS_ACTIVE = 1;
+
/**
* Interface for monitor active subscriptions list changing
*/
@@ -74,21 +85,35 @@
private ProxySubscriptionManager(Context context) {
final Looper looper = context.getMainLooper();
+ ActiveSubscriptionsListener subscriptionMonitor = new ActiveSubscriptionsListener(
+ looper, context) {
+ public void onChanged() {
+ notifySubscriptionInfoMightChanged();
+ }
+ };
+ GlobalSettingsChangeListener airplaneModeMonitor = new GlobalSettingsChangeListener(
+ looper, context, Settings.Global.AIRPLANE_MODE_ON) {
+ public void onChanged(String field) {
+ subscriptionMonitor.clearCache();
+ notifySubscriptionInfoMightChanged();
+ }
+ };
+
+ init(context, subscriptionMonitor, airplaneModeMonitor);
+ }
+
+ @Keep
+ @VisibleForTesting
+ protected void init(Context context, ActiveSubscriptionsListener activeSubscriptionsListener,
+ GlobalSettingsChangeListener airplaneModeOnSettingsChangeListener) {
+
mActiveSubscriptionsListeners =
new ArrayList<OnActiveSubscriptionChangedListener>();
+ mPendingNotifyListeners =
+ new ArrayList<OnActiveSubscriptionChangedListener>();
- mSubscriptionMonitor = new ActiveSubscriptionsListener(looper, context) {
- public void onChanged() {
- notifyAllListeners();
- }
- };
- mAirplaneModeMonitor = new GlobalSettingsChangeListener(looper,
- context, Settings.Global.AIRPLANE_MODE_ON) {
- public void onChanged(String field) {
- mSubscriptionMonitor.clearCache();
- notifyAllListeners();
- }
- };
+ mSubscriptionMonitor = activeSubscriptionsListener;
+ mAirplaneModeMonitor = airplaneModeOnSettingsChangeListener;
mSubscriptionMonitor.start();
}
@@ -98,15 +123,19 @@
private GlobalSettingsChangeListener mAirplaneModeMonitor;
private List<OnActiveSubscriptionChangedListener> mActiveSubscriptionsListeners;
+ private List<OnActiveSubscriptionChangedListener> mPendingNotifyListeners;
- private void notifyAllListeners() {
- for (OnActiveSubscriptionChangedListener listener : mActiveSubscriptionsListeners) {
- final Lifecycle lifecycle = listener.getLifecycle();
- if ((lifecycle == null)
- || (lifecycle.getCurrentState().isAtLeast(Lifecycle.State.STARTED))) {
- listener.onChanged();
- }
- }
+ @Keep
+ @VisibleForTesting
+ protected void notifySubscriptionInfoMightChanged() {
+ // create a merged list for processing all listeners
+ List<OnActiveSubscriptionChangedListener> listeners =
+ new ArrayList<OnActiveSubscriptionChangedListener>(mPendingNotifyListeners);
+ listeners.addAll(mActiveSubscriptionsListeners);
+
+ mActiveSubscriptionsListeners.clear();
+ mPendingNotifyListeners.clear();
+ processStatusChangeOnListeners(listeners);
}
/**
@@ -131,6 +160,11 @@
@OnLifecycleEvent(ON_START)
void onStart() {
mSubscriptionMonitor.start();
+
+ // callback notify those listener(s) which back to active state
+ List<OnActiveSubscriptionChangedListener> listeners = mPendingNotifyListeners;
+ mPendingNotifyListeners = new ArrayList<OnActiveSubscriptionChangedListener>();
+ processStatusChangeOnListeners(listeners);
}
@OnLifecycleEvent(ON_STOP)
@@ -215,12 +249,17 @@
}
/**
- * Add listener to active subscriptions monitor list
+ * Add listener to active subscriptions monitor list.
+ * Note: listener only take place when change happens.
+ * No immediate callback performed after the invoke of this method.
*
* @param listener listener to active subscriptions change
*/
+ @Keep
public void addActiveSubscriptionsListener(OnActiveSubscriptionChangedListener listener) {
- if (mActiveSubscriptionsListeners.contains(listener)) {
+ removeSpecificListenerAndCleanList(listener, mPendingNotifyListeners);
+ removeSpecificListenerAndCleanList(listener, mActiveSubscriptionsListeners);
+ if ((listener == null) || (getListenerState(listener) == LISTENER_END_OF_LIFE)) {
return;
}
mActiveSubscriptionsListeners.add(listener);
@@ -231,7 +270,51 @@
*
* @param listener listener to active subscriptions change
*/
+ @Keep
public void removeActiveSubscriptionsListener(OnActiveSubscriptionChangedListener listener) {
- mActiveSubscriptionsListeners.remove(listener);
+ removeSpecificListenerAndCleanList(listener, mPendingNotifyListeners);
+ removeSpecificListenerAndCleanList(listener, mActiveSubscriptionsListeners);
+ }
+
+ private int getListenerState(OnActiveSubscriptionChangedListener listener) {
+ Lifecycle lifecycle = listener.getLifecycle();
+ if (lifecycle == null) {
+ return LISTENER_IS_ACTIVE;
+ }
+ Lifecycle.State lifecycleState = lifecycle.getCurrentState();
+ if (lifecycleState == Lifecycle.State.DESTROYED) {
+ Log.d(LOG_TAG, "Listener dead detected - " + listener);
+ return LISTENER_END_OF_LIFE;
+ }
+ return lifecycleState.isAtLeast(Lifecycle.State.STARTED) ?
+ LISTENER_IS_ACTIVE : LISTENER_IS_INACTIVE;
+ }
+
+ private void removeSpecificListenerAndCleanList(OnActiveSubscriptionChangedListener listener,
+ List<OnActiveSubscriptionChangedListener> list) {
+ // also drop listener(s) which is end of life
+ list.removeIf(it -> (it == listener) || (getListenerState(it) == LISTENER_END_OF_LIFE));
+ }
+
+ private void processStatusChangeOnListeners(
+ List<OnActiveSubscriptionChangedListener> listeners) {
+ // categorize listener(s), and end of life listener(s) been ignored
+ Map<Integer, List<OnActiveSubscriptionChangedListener>> categorizedListeners =
+ listeners.stream()
+ .collect(Collectors.groupingBy(it -> getListenerState(it)));
+
+ // have inactive listener(s) in pending list
+ categorizedListeners.computeIfPresent(LISTENER_IS_INACTIVE, (category, list) -> {
+ mPendingNotifyListeners.addAll(list);
+ return list;
+ });
+
+ // get active listener(s)
+ categorizedListeners.computeIfPresent(LISTENER_IS_ACTIVE, (category, list) -> {
+ mActiveSubscriptionsListeners.addAll(list);
+ // notify each one of them
+ list.stream().forEach(it -> it.onChanged());
+ return list;
+ });
}
}
diff --git a/src/com/android/settings/network/telephony/MobileNetworkActivity.java b/src/com/android/settings/network/telephony/MobileNetworkActivity.java
index 5016460..b122cdc 100644
--- a/src/com/android/settings/network/telephony/MobileNetworkActivity.java
+++ b/src/com/android/settings/network/telephony/MobileNetworkActivity.java
@@ -132,15 +132,13 @@
: ((startIntent != null)
? startIntent.getIntExtra(Settings.EXTRA_SUB_ID, SUB_ID_NULL)
: SUB_ID_NULL);
+ // perform registration after mCurSubscriptionId been configured.
+ registerActiveSubscriptionsListener();
final SubscriptionInfo subscription = getSubscription();
maybeShowContactDiscoveryDialog(subscription);
- // Since onChanged() will take place immediately when addActiveSubscriptionsListener(),
- // perform registration after mCurSubscriptionId been configured.
- registerActiveSubscriptionsListener();
-
- updateSubscriptions(subscription, savedInstanceState);
+ updateSubscriptions(subscription, null);
}
@VisibleForTesting
@@ -296,7 +294,7 @@
final Fragment fragment = new MobileNetworkSettings();
fragment.setArguments(bundle);
fragmentTransaction.replace(R.id.content_frame, fragment, fragmentTag);
- fragmentTransaction.commit();
+ fragmentTransaction.commitAllowingStateLoss();
}
private void removeContactDiscoveryDialog(int subId) {
diff --git a/tests/unit/src/com/android/settings/network/ProxySubscriptionManagerTest.java b/tests/unit/src/com/android/settings/network/ProxySubscriptionManagerTest.java
new file mode 100644
index 0000000..afe9d19
--- /dev/null
+++ b/tests/unit/src/com/android/settings/network/ProxySubscriptionManagerTest.java
@@ -0,0 +1,228 @@
+/*
+ * Copyright (C) 2021 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.network;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+
+import androidx.lifecycle.Lifecycle;
+import androidx.test.annotation.UiThreadTest;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+
+@RunWith(AndroidJUnit4.class)
+public class ProxySubscriptionManagerTest {
+
+ private Context mContext;
+ @Mock
+ private ActiveSubscriptionsListener mActiveSubscriptionsListener;
+ @Mock
+ private GlobalSettingsChangeListener mAirplaneModeOnSettingsChangeListener;
+
+ @Mock
+ private Lifecycle mLifecycle_ON_PAUSE;
+ @Mock
+ private Lifecycle mLifecycle_ON_RESUME;
+ @Mock
+ private Lifecycle mLifecycle_ON_DESTROY;
+
+ private Client mClient1;
+ private Client mClient2;
+
+ @Before
+ @UiThreadTest
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+
+ mContext = spy(ApplicationProvider.getApplicationContext());
+
+ doReturn(Lifecycle.State.CREATED).when(mLifecycle_ON_PAUSE).getCurrentState();
+ doReturn(Lifecycle.State.STARTED).when(mLifecycle_ON_RESUME).getCurrentState();
+ doReturn(Lifecycle.State.DESTROYED).when(mLifecycle_ON_DESTROY).getCurrentState();
+
+ mClient1 = new Client();
+ mClient1.setLifecycle(mLifecycle_ON_RESUME);
+ mClient2 = new Client();
+ mClient2.setLifecycle(mLifecycle_ON_RESUME);
+ }
+
+ private ProxySubscriptionManager getInstance(Context context) {
+ ProxySubscriptionManager proxy =
+ Mockito.mock(ProxySubscriptionManager.class, Mockito.CALLS_REAL_METHODS);
+ proxy.init(context, mActiveSubscriptionsListener, mAirplaneModeOnSettingsChangeListener);
+ proxy.notifySubscriptionInfoMightChanged();
+ return proxy;
+ }
+
+ public class Client implements ProxySubscriptionManager.OnActiveSubscriptionChangedListener {
+ private Lifecycle lifeCycle;
+ private int numberOfCallback;
+
+ public void onChanged() {
+ numberOfCallback++;
+ }
+
+ public Lifecycle getLifecycle() {
+ return lifeCycle;
+ }
+
+ public int getCallbackCount() {
+ return numberOfCallback;
+ }
+
+ public void setLifecycle(Lifecycle lifecycle) {
+ lifeCycle = lifecycle;
+ }
+ }
+
+ @Test
+ @UiThreadTest
+ public void addActiveSubscriptionsListener_addOneClient_getNoCallback() {
+ ProxySubscriptionManager proxy = getInstance(mContext);
+
+ proxy.addActiveSubscriptionsListener(mClient1);
+ assertThat(mClient1.getCallbackCount()).isEqualTo(0);
+ }
+
+ @Test
+ @UiThreadTest
+ public void addActiveSubscriptionsListener_addOneClient_changeOnSimGetCallback() {
+ ProxySubscriptionManager proxy = getInstance(mContext);
+
+ proxy.addActiveSubscriptionsListener(mClient1);
+ assertThat(mClient1.getCallbackCount()).isEqualTo(0);
+
+ proxy.notifySubscriptionInfoMightChanged();
+ assertThat(mClient1.getCallbackCount()).isEqualTo(1);
+ }
+
+ @Test
+ @UiThreadTest
+ public void addActiveSubscriptionsListener_addOneClient_noCallbackUntilUiResume() {
+ ProxySubscriptionManager proxy = getInstance(mContext);
+
+ mClient1.setLifecycle(mLifecycle_ON_PAUSE);
+
+ proxy.addActiveSubscriptionsListener(mClient1);
+ assertThat(mClient1.getCallbackCount()).isEqualTo(0);
+
+ proxy.notifySubscriptionInfoMightChanged();
+ assertThat(mClient1.getCallbackCount()).isEqualTo(0);
+
+ mClient1.setLifecycle(mLifecycle_ON_RESUME);
+ proxy.onStart();
+ Assert.assertTrue(mClient1.getCallbackCount() > 0);
+
+ mClient1.setLifecycle(mLifecycle_ON_PAUSE);
+ proxy.onStop();
+ int latestCallbackCount = mClient1.getCallbackCount();
+
+ proxy.notifySubscriptionInfoMightChanged();
+ assertThat(mClient1.getCallbackCount()).isEqualTo(latestCallbackCount);
+ }
+
+ @Test
+ @UiThreadTest
+ public void addActiveSubscriptionsListener_addTwoClient_eachClientGetNoCallback() {
+ ProxySubscriptionManager proxy = getInstance(mContext);
+
+ proxy.addActiveSubscriptionsListener(mClient1);
+ assertThat(mClient1.getCallbackCount()).isEqualTo(0);
+
+ proxy.addActiveSubscriptionsListener(mClient2);
+ assertThat(mClient1.getCallbackCount()).isEqualTo(0);
+ assertThat(mClient2.getCallbackCount()).isEqualTo(0);
+ }
+
+ @Test
+ @UiThreadTest
+ public void addActiveSubscriptionsListener_addTwoClient_callbackOnlyWhenResume() {
+ ProxySubscriptionManager proxy = getInstance(mContext);
+
+ proxy.addActiveSubscriptionsListener(mClient1);
+ assertThat(mClient1.getCallbackCount()).isEqualTo(0);
+
+ proxy.addActiveSubscriptionsListener(mClient2);
+ assertThat(mClient1.getCallbackCount()).isEqualTo(0);
+ assertThat(mClient2.getCallbackCount()).isEqualTo(0);
+
+ mClient1.setLifecycle(mLifecycle_ON_PAUSE);
+ proxy.onStop();
+ assertThat(mClient1.getCallbackCount()).isEqualTo(0);
+ assertThat(mClient2.getCallbackCount()).isEqualTo(0);
+
+ proxy.notifySubscriptionInfoMightChanged();
+ assertThat(mClient1.getCallbackCount()).isEqualTo(0);
+ assertThat(mClient2.getCallbackCount()).isEqualTo(1);
+
+ mClient1.setLifecycle(mLifecycle_ON_RESUME);
+ proxy.onStart();
+ Assert.assertTrue(mClient1.getCallbackCount() > 0);
+ assertThat(mClient2.getCallbackCount()).isEqualTo(1);
+ }
+
+ @Test
+ @UiThreadTest
+ public void removeActiveSubscriptionsListener_removedClient_noCallback() {
+ ProxySubscriptionManager proxy = getInstance(mContext);
+
+ proxy.addActiveSubscriptionsListener(mClient1);
+ assertThat(mClient1.getCallbackCount()).isEqualTo(0);
+
+ proxy.notifySubscriptionInfoMightChanged();
+ assertThat(mClient1.getCallbackCount()).isEqualTo(1);
+
+ proxy.removeActiveSubscriptionsListener(mClient1);
+ assertThat(mClient1.getCallbackCount()).isEqualTo(1);
+
+ proxy.notifySubscriptionInfoMightChanged();
+ assertThat(mClient1.getCallbackCount()).isEqualTo(1);
+ }
+
+ @Test
+ @UiThreadTest
+ public void notifySubscriptionInfoMightChanged_destroyedClient_autoRemove() {
+ ProxySubscriptionManager proxy = getInstance(mContext);
+
+ proxy.addActiveSubscriptionsListener(mClient1);
+ assertThat(mClient1.getCallbackCount()).isEqualTo(0);
+
+ proxy.notifySubscriptionInfoMightChanged();
+ assertThat(mClient1.getCallbackCount()).isEqualTo(1);
+
+ mClient1.setLifecycle(mLifecycle_ON_DESTROY);
+ proxy.notifySubscriptionInfoMightChanged();
+ assertThat(mClient1.getCallbackCount()).isEqualTo(1);
+
+ mClient1.setLifecycle(mLifecycle_ON_RESUME);
+ proxy.notifySubscriptionInfoMightChanged();
+ assertThat(mClient1.getCallbackCount()).isEqualTo(1);
+ }
+}