diff --git a/src/com/android/settings/network/ProxySubscriptionManager.java b/src/com/android/settings/network/ProxySubscriptionManager.java index eb1a7d4f608..614491a5016 100644 --- a/src/com/android/settings/network/ProxySubscriptionManager.java +++ b/src/com/android/settings/network/ProxySubscriptionManager.java @@ -25,19 +25,30 @@ import android.os.Looper; 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 @@ public class ProxySubscriptionManager implements LifecycleObserver { 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(); + mPendingNotifyListeners = + new ArrayList(); - 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 @@ public class ProxySubscriptionManager implements LifecycleObserver { private GlobalSettingsChangeListener mAirplaneModeMonitor; private List mActiveSubscriptionsListeners; + private List 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 listeners = + new ArrayList(mPendingNotifyListeners); + listeners.addAll(mActiveSubscriptionsListeners); + + mActiveSubscriptionsListeners.clear(); + mPendingNotifyListeners.clear(); + processStatusChangeOnListeners(listeners); } /** @@ -131,6 +160,11 @@ public class ProxySubscriptionManager implements LifecycleObserver { @OnLifecycleEvent(ON_START) void onStart() { mSubscriptionMonitor.start(); + + // callback notify those listener(s) which back to active state + List listeners = mPendingNotifyListeners; + mPendingNotifyListeners = new ArrayList(); + processStatusChangeOnListeners(listeners); } @OnLifecycleEvent(ON_STOP) @@ -215,12 +249,17 @@ public class ProxySubscriptionManager implements LifecycleObserver { } /** - * 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 @@ public class ProxySubscriptionManager implements LifecycleObserver { * * @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 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 listeners) { + // categorize listener(s), and end of life listener(s) been ignored + Map> 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 50164609dd0..b122cdc04b1 100644 --- a/src/com/android/settings/network/telephony/MobileNetworkActivity.java +++ b/src/com/android/settings/network/telephony/MobileNetworkActivity.java @@ -132,15 +132,13 @@ public class MobileNetworkActivity extends SettingsBaseActivity : ((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 @@ public class MobileNetworkActivity extends SettingsBaseActivity 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 00000000000..afe9d19c485 --- /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); + } +}