Merge "[Settings] Code refactor for SIM change detection"
diff --git a/src/com/android/settings/ResetNetworkConfirm.java b/src/com/android/settings/ResetNetworkConfirm.java
index 0cd94a5..c707b96 100644
--- a/src/com/android/settings/ResetNetworkConfirm.java
+++ b/src/com/android/settings/ResetNetworkConfirm.java
@@ -61,6 +61,7 @@
     @VisibleForTesting ResetNetworkRequest mResetNetworkRequest;
     private ProgressDialog mProgressDialog;
     private AlertDialog mAlertDialog;
+    @VisibleForTesting ResetSubscriptionContract mResetSubscriptionContract;
     private OnSubscriptionsChangedListener mSubscriptionsChangedListener;
 
     /**
@@ -130,16 +131,11 @@
             }
 
             // abandon execution if subscription no longer active
-            int subId = mResetNetworkRequest.getResetApnSubId();
-            if (subId != SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
-                SubscriptionManager mgr = getSubscriptionManager();
-                // always remove listener
-                stopMonitorSubscriptionChange(mgr);
-                if (!isSubscriptionRemainActive(mgr, subId)) {
-                    Log.w(TAG, "subId " + subId + " disappear when confirm");
-                    mActivity.finish();
-                    return;
-                }
+            Integer subId = mResetSubscriptionContract.getAnyMissingSubscriptionId();
+            if (subId != null) {
+                Log.w(TAG, "subId " + subId + " no longer active");
+                getActivity().onBackPressed();
+                return;
             }
 
             // Should dismiss the progress dialog firstly if it is showing
@@ -186,7 +182,7 @@
             Bundle savedInstanceState) {
         View view = (new ResetNetworkRestrictionViewBuilder(mActivity)).build();
         if (view != null) {
-            stopMonitorSubscriptionChange(getSubscriptionManager());
+            mResetSubscriptionContract.close();
             Log.w(TAG, "Access deny.");
             return view;
         }
@@ -208,13 +204,15 @@
 
         mActivity = getActivity();
 
-        if (mResetNetworkRequest.getResetApnSubId()
-                == ResetNetworkRequest.INVALID_SUBSCRIPTION_ID) {
-            return;
-        }
-        // close confirmation dialog when reset specific subscription
-        // but removed priori to the confirmation button been pressed
-        startMonitorSubscriptionChange(getSubscriptionManager());
+        mResetSubscriptionContract = new ResetSubscriptionContract(getContext(),
+                mResetNetworkRequest) {
+            @Override
+            public void onSubscriptionInactive(int subscriptionId) {
+                // close UI if subscription no longer active
+                Log.w(TAG, "subId " + subscriptionId + " no longer active.");
+                getActivity().onBackPressed();
+            }
+        };
     }
 
     @Override
@@ -223,63 +221,22 @@
         mResetNetworkRequest.writeIntoBundle(outState);
     }
 
-    private SubscriptionManager getSubscriptionManager() {
-        SubscriptionManager mgr = mActivity.getSystemService(SubscriptionManager.class);
-        if (mgr == null) {
-            Log.w(TAG, "No SubscriptionManager");
-        }
-        return mgr;
-    }
-
-    private void startMonitorSubscriptionChange(SubscriptionManager mgr) {
-        if (mgr == null) {
-            return;
-        }
-        // update monitor listener
-        mSubscriptionsChangedListener = new OnSubscriptionsChangedListener(
-                Looper.getMainLooper()) {
-            @Override
-            public void onSubscriptionsChanged() {
-                int subId = mResetNetworkRequest.getResetApnSubId();
-                SubscriptionManager mgr = getSubscriptionManager();
-                if (isSubscriptionRemainActive(mgr, subId)) {
-                    return;
-                }
-                // close UI if subscription no longer active
-                Log.w(TAG, "subId " + subId + " no longer active.");
-                stopMonitorSubscriptionChange(mgr);
-                mActivity.finish();
-            }
-        };
-        mgr.addOnSubscriptionsChangedListener(
-                mActivity.getMainExecutor(), mSubscriptionsChangedListener);
-    }
-
-    private boolean isSubscriptionRemainActive(SubscriptionManager mgr, int subscriptionId) {
-        return (mgr == null) ? false : (mgr.getActiveSubscriptionInfo(subscriptionId) != null);
-    }
-
-    private void stopMonitorSubscriptionChange(SubscriptionManager mgr) {
-        if ((mgr == null) || (mSubscriptionsChangedListener == null)) {
-            return;
-        }
-        mgr.removeOnSubscriptionsChangedListener(mSubscriptionsChangedListener);
-        mSubscriptionsChangedListener = null;
-    }
-
     @Override
     public void onDestroy() {
         if (mResetNetworkTask != null) {
             mResetNetworkTask.cancel(true /* mayInterruptIfRunning */);
             mResetNetworkTask = null;
         }
+        if (mResetSubscriptionContract != null) {
+            mResetSubscriptionContract.close();
+            mResetSubscriptionContract = null;
+        }
         if (mProgressDialog != null) {
             mProgressDialog.dismiss();
         }
         if (mAlertDialog != null) {
             mAlertDialog.dismiss();
         }
-        stopMonitorSubscriptionChange(getSubscriptionManager());
         super.onDestroy();
     }
 
diff --git a/src/com/android/settings/ResetSubscriptionContract.java b/src/com/android/settings/ResetSubscriptionContract.java
new file mode 100644
index 0000000..580e907
--- /dev/null
+++ b/src/com/android/settings/ResetSubscriptionContract.java
@@ -0,0 +1,157 @@
+/*
+ * Copyright (C) 2022 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;
+
+import android.content.Context;
+import android.telephony.SubscriptionManager;
+import android.telephony.SubscriptionManager.OnSubscriptionsChangedListener;
+import android.util.Log;
+
+import androidx.annotation.VisibleForTesting;
+
+import java.util.concurrent.Executors;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.stream.IntStream;
+
+/**
+ * A Class monitoring the availability of subscription IDs provided within reset request.
+ *
+ * This is to detect the situation when user changing SIM card during the presenting of
+ * confirmation UI.
+ */
+public class ResetSubscriptionContract implements AutoCloseable {
+    private static final String TAG = "ResetSubscriptionContract";
+
+    private final Context mContext;
+    private ExecutorService mExecutorService;
+    private final int [] mResetSubscriptionIds;
+    @VisibleForTesting
+    protected OnSubscriptionsChangedListener mSubscriptionsChangedListener;
+    private AtomicBoolean mSubscriptionsUpdateNotify = new AtomicBoolean();
+
+    /**
+     * Constructor
+     * @param context Context
+     * @param resetRequest the request object for perform network reset operation.
+     */
+    public ResetSubscriptionContract(Context context, ResetNetworkRequest resetRequest) {
+        mContext = context;
+        // Only keeps specific subscription ID required to perform reset operation
+        IntStream subIdStream = IntStream.of(
+                resetRequest.getResetTelephonyAndNetworkPolicyManager()
+                , resetRequest.getResetApnSubId());
+        mResetSubscriptionIds = subIdStream.sorted().distinct()
+                .filter(id -> SubscriptionManager.isUsableSubscriptionId(id))
+                .toArray();
+
+        if (mResetSubscriptionIds.length <= 0) {
+            return;
+        }
+
+        // Monitoring callback through background thread
+        mExecutorService = Executors.newSingleThreadExecutor();
+        startMonitorSubscriptionChange();
+    }
+
+    /**
+     * A method for detecting if there's any subscription under monitor no longer active.
+     * @return subscription ID which is no longer active.
+     */
+    public Integer getAnyMissingSubscriptionId() {
+        if (mResetSubscriptionIds.length <= 0) {
+            return null;
+        }
+        SubscriptionManager mgr = getSubscriptionManager();
+        if (mgr == null) {
+            Log.w(TAG, "Fail to access subscription manager");
+            return mResetSubscriptionIds[0];
+        }
+        for (int idx = 0; idx < mResetSubscriptionIds.length; idx++) {
+            int subId = mResetSubscriptionIds[idx];
+            if (mgr.getActiveSubscriptionInfo(subId) == null) {
+                Log.w(TAG, "SubId " + subId + " no longer active.");
+                return subId;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Async callback when detecting if there's any subscription under monitor no longer active.
+     * @param subscriptionId subscription ID which is no longer active.
+     */
+    public void onSubscriptionInactive(int subscriptionId) {}
+
+    @VisibleForTesting
+    protected SubscriptionManager getSubscriptionManager() {
+        return mContext.getSystemService(SubscriptionManager.class);
+    }
+
+    @VisibleForTesting
+    protected OnSubscriptionsChangedListener getChangeListener() {
+        return new OnSubscriptionsChangedListener() {
+            @Override
+            public void onSubscriptionsChanged() {
+                /**
+                 * Reducing the processing time on main UI thread through a flag.
+                 * Once flag get into false, which means latest callback has been
+                 * processed.
+                 */
+                mSubscriptionsUpdateNotify.set(true);
+
+                // Back to main UI thread
+                mContext.getMainExecutor().execute(() -> {
+                    // Remove notifications and perform checking.
+                    if (mSubscriptionsUpdateNotify.getAndSet(false)) {
+                        Integer subId = getAnyMissingSubscriptionId();
+                        if (subId != null) {
+                            onSubscriptionInactive(subId);
+                        }
+                    }
+                });
+            }
+        };
+    }
+
+    private void startMonitorSubscriptionChange() {
+        SubscriptionManager mgr = getSubscriptionManager();
+        if (mgr == null) {
+            return;
+        }
+        // update monitor listener
+        mSubscriptionsChangedListener = getChangeListener();
+
+        mgr.addOnSubscriptionsChangedListener(
+                mExecutorService, mSubscriptionsChangedListener);
+    }
+
+    // Implementation of AutoCloseable
+    public void close() {
+        if (mExecutorService == null) {
+            return;
+        }
+        // Stop monitoring subscription change
+        SubscriptionManager mgr = getSubscriptionManager();
+        if (mgr != null) {
+            mgr.removeOnSubscriptionsChangedListener(mSubscriptionsChangedListener);
+        }
+        // Release Executor
+        mExecutorService.shutdownNow();
+        mExecutorService = null;
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/ResetNetworkConfirmTest.java b/tests/robotests/src/com/android/settings/ResetNetworkConfirmTest.java
index 5dad40d..0bab303 100644
--- a/tests/robotests/src/com/android/settings/ResetNetworkConfirmTest.java
+++ b/tests/robotests/src/com/android/settings/ResetNetworkConfirmTest.java
@@ -74,6 +74,14 @@
     public void testResetNetworkData_notResetEsim() {
         mResetNetworkConfirm.mResetNetworkRequest =
                 new ResetNetworkRequest(ResetNetworkRequest.RESET_NONE);
+        mResetNetworkConfirm.mResetSubscriptionContract =
+                new ResetSubscriptionContract(mActivity,
+                mResetNetworkConfirm.mResetNetworkRequest) {
+            @Override
+            public void onSubscriptionInactive(int subscriptionId) {
+                mActivity.onBackPressed();
+            }
+        };
 
         mResetNetworkConfirm.mFinalClickListener.onClick(null /* View */);
         Robolectric.getBackgroundThreadScheduler().advanceToLastPostedRunnable();
diff --git a/tests/unit/src/com/android/settings/ResetSubscriptionContractTest.java b/tests/unit/src/com/android/settings/ResetSubscriptionContractTest.java
new file mode 100644
index 0000000..4443304
--- /dev/null
+++ b/tests/unit/src/com/android/settings/ResetSubscriptionContractTest.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2022 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;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.telephony.SubscriptionInfo;
+import android.telephony.SubscriptionManager;
+import android.telephony.SubscriptionManager.OnSubscriptionsChangedListener;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@RunWith(AndroidJUnit4.class)
+public class ResetSubscriptionContractTest {
+
+    private static final int SUB_ID_1 = 3;
+    private static final int SUB_ID_2 = 8;
+
+    @Mock
+    private SubscriptionManager mSubscriptionManager;
+    @Mock
+    private OnSubscriptionsChangedListener mOnSubscriptionsChangedListener;
+    @Mock
+    private SubscriptionInfo mSubscriptionInfo1;
+    @Mock
+    private SubscriptionInfo mSubscriptionInfo2;
+
+    private Context mContext;
+    private ResetNetworkRequest mRequestArgs;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+
+        mContext = spy(ApplicationProvider.getApplicationContext());
+        mRequestArgs = new ResetNetworkRequest(new Bundle());
+    }
+
+    private ResetSubscriptionContract createTestObject() {
+        return new ResetSubscriptionContract(mContext, mRequestArgs) {
+            @Override
+            protected SubscriptionManager getSubscriptionManager() {
+                return mSubscriptionManager;
+            }
+            @Override
+            protected OnSubscriptionsChangedListener getChangeListener() {
+                return mOnSubscriptionsChangedListener;
+            }
+        };
+    }
+
+    @Test
+    public void getAnyMissingSubscriptionId_returnNull_whenNoSubscriptionChange() {
+        mRequestArgs.setResetTelephonyAndNetworkPolicyManager(SUB_ID_1);
+        doReturn(mSubscriptionInfo1).when(mSubscriptionManager)
+                .getActiveSubscriptionInfo(SUB_ID_1);
+        mRequestArgs.setResetApn(SUB_ID_2);
+        doReturn(mSubscriptionInfo2).when(mSubscriptionManager)
+                .getActiveSubscriptionInfo(SUB_ID_2);
+
+        ResetSubscriptionContract target = createTestObject();
+
+        verify(mSubscriptionManager).addOnSubscriptionsChangedListener(any(), any());
+
+        assertNull(target.getAnyMissingSubscriptionId());
+    }
+
+    @Test
+    public void getAnyMissingSubscriptionId_returnSubId_whenSubscriptionNotActive() {
+        mRequestArgs.setResetTelephonyAndNetworkPolicyManager(SUB_ID_1);
+        doReturn(mSubscriptionInfo1).when(mSubscriptionManager)
+                .getActiveSubscriptionInfo(SUB_ID_1);
+        mRequestArgs.setResetApn(SUB_ID_2);
+        doReturn(null).when(mSubscriptionManager)
+                .getActiveSubscriptionInfo(SUB_ID_2);
+
+        ResetSubscriptionContract target = createTestObject();
+
+        verify(mSubscriptionManager).addOnSubscriptionsChangedListener(any(), any());
+
+        assertEquals(target.getAnyMissingSubscriptionId(), new Integer(SUB_ID_2));
+    }
+}