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);
+    }
+}