diff --git a/src/com/android/settings/network/GlobalSettingsChangeListener.java b/src/com/android/settings/network/GlobalSettingsChangeListener.java new file mode 100644 index 00000000000..03103d7392e --- /dev/null +++ b/src/com/android/settings/network/GlobalSettingsChangeListener.java @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2019 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 androidx.lifecycle.Lifecycle.Event.ON_DESTROY; +import static androidx.lifecycle.Lifecycle.Event.ON_START; +import static androidx.lifecycle.Lifecycle.Event.ON_STOP; + +import android.content.Context; +import android.database.ContentObserver; +import android.net.Uri; +import android.os.Handler; +import android.provider.Settings; + +import androidx.lifecycle.Lifecycle; +import androidx.lifecycle.LifecycleObserver; +import androidx.lifecycle.OnLifecycleEvent; + +/** + * A listener for Settings.Global configuration change, with support of Lifecycle + */ +abstract class GlobalSettingsChangeListener extends ContentObserver implements LifecycleObserver { + + /** + * Constructor + * + * @param context of this listener + * @param field field of Global Settings + */ + GlobalSettingsChangeListener(Context context, String field) { + super(new Handler()); + mContext = context; + mField = field; + monitorUri(true); + } + + private Context mContext; + private String mField; + private Uri mUri; + private Lifecycle mLifecycle; + + /** + * Observed Settings got changed + */ + public abstract void onChanged(String field); + + /** + * Notify change of Globals.Setting based on given Lifecycle + * + * @param lifecycle life cycle to reference + */ + public void notifyChangeBasedOn(Lifecycle lifecycle) { + if (mLifecycle != null) { + mLifecycle.removeObserver(this); + } + if (lifecycle != null) { + lifecycle.addObserver(this); + } + mLifecycle = lifecycle; + } + + public void onChange(boolean selfChange) { + if (!isMonitoring()) { + return; + } + onChanged(mField); + } + + @OnLifecycleEvent(ON_START) + void onStart() { + monitorUri(true); + } + + @OnLifecycleEvent(ON_STOP) + void onStop() { + monitorUri(false); + } + + @OnLifecycleEvent(ON_DESTROY) + void onDestroy() { + monitorUri(false); + notifyChangeBasedOn(null); + } + + private boolean isMonitoring() { + return (mUri != null); + } + + private void monitorUri(boolean on) { + if (isMonitoring() == on) { + return; + } + if (mUri == null) { + mUri = Settings.Global.getUriFor(mField); + 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 new file mode 100644 index 00000000000..ec4fcbea7ce --- /dev/null +++ b/src/com/android/settings/network/ProxySubscriptionManager.java @@ -0,0 +1,197 @@ +/* + * Copyright (C) 2019 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 androidx.lifecycle.Lifecycle.Event.ON_DESTROY; +import static androidx.lifecycle.Lifecycle.Event.ON_START; +import static androidx.lifecycle.Lifecycle.Event.ON_STOP; + +import android.content.Context; +import android.provider.Settings; +import android.telephony.SubscriptionInfo; +import android.telephony.SubscriptionManager; + +import androidx.lifecycle.Lifecycle; +import androidx.lifecycle.LifecycleObserver; +import androidx.lifecycle.OnLifecycleEvent; + +import java.util.ArrayList; +import java.util.List; + +/** + * A proxy to the subscription manager + */ +public class ProxySubscriptionManager extends SubscriptionManager.OnSubscriptionsChangedListener + implements LifecycleObserver { + + /** + * Interface for monitor active subscriptions list changing + */ + public interface OnActiveSubscriptionChangedListener { + /** + * When active subscriptions list get changed + */ + void onChanged(); + } + + /** + * Get proxy instance to subscription manager + * + * @return proxy to subscription manager + */ + public static ProxySubscriptionManager getInstance(Context context) { + if (sSingleton != null) { + return sSingleton; + } + sSingleton = new ProxySubscriptionManager(context); + return sSingleton; + } + + private static ProxySubscriptionManager sSingleton; + + private ProxySubscriptionManager(Context context) { + mContext = context; + + mActiveSubscriptionsListeners = + new ArrayList(); + + mSubsciptionsMonitor = new ActiveSubsciptionsListener(context) { + public void onChanged() { + notifyAllListeners(); + } + }; + mAirplaneModeMonitor = new GlobalSettingsChangeListener(context, + Settings.Global.AIRPLANE_MODE_ON) { + public void onChanged(String field) { + mSubsciptionsMonitor.clearCache(); + notifyAllListeners(); + } + }; + + mKeepCacheWhenOnStart = true; + mSubsciptionsMonitor.start(); + } + + private Lifecycle mLifecycle; + private Context mContext; + private ActiveSubsciptionsListener mSubsciptionsMonitor; + private GlobalSettingsChangeListener mAirplaneModeMonitor; + private boolean mKeepCacheWhenOnStart; + + private List mActiveSubscriptionsListeners; + + private void notifyAllListeners() { + for (OnActiveSubscriptionChangedListener listener : mActiveSubscriptionsListeners) { + listener.onChanged(); + } + } + + /** + * Lifecycle for data within proxy + * + * @param lifecycle life cycle to reference + */ + public void setLifecycle(Lifecycle lifecycle) { + if (mLifecycle != null) { + mLifecycle.removeObserver(this); + } + if (lifecycle != null) { + lifecycle.addObserver(this); + } + mLifecycle = lifecycle; + mAirplaneModeMonitor.notifyChangeBasedOn(lifecycle); + } + + @OnLifecycleEvent(ON_START) + void onStart() { + if (!mKeepCacheWhenOnStart) { + mSubsciptionsMonitor.clearCache(); + } + mSubsciptionsMonitor.start(); + } + + @OnLifecycleEvent(ON_STOP) + void onStop() { + mKeepCacheWhenOnStart = false; + mSubsciptionsMonitor.stop(); + } + + @OnLifecycleEvent(ON_DESTROY) + void onDestroy() { + mSubsciptionsMonitor.stop(); + + if (mLifecycle != null) { + mLifecycle.removeObserver(this); + mLifecycle = null; + + sSingleton = null; + } + } + + /** + * Get SubscriptionManager + * + * @return a SubscriptionManager + */ + public SubscriptionManager get() { + return mSubsciptionsMonitor.getSubscriptionManager(); + } + + /** + * Get a list of active subscription info + * + * @return A list of active subscription info + */ + public List getActiveSubscriptionsInfo() { + return mSubsciptionsMonitor.getActiveSubscriptionsInfo(); + } + + /** + * Get an active subscription info with given subscription ID + * + * @param subId target subscription ID + * @return A subscription info which is active list + */ + public SubscriptionInfo getActiveSubscriptionInfo(int subId) { + return mSubsciptionsMonitor.getActiveSubscriptionInfo(subId); + } + + /** + * Clear data cached within proxy + */ + public void clearCache() { + mSubsciptionsMonitor.clearCache(); + } + + /** + * Add listener to active subscriptions monitor list + * + * @param listener listener to active subscriptions change + */ + public void addActiveSubscriptionsListener(OnActiveSubscriptionChangedListener listener) { + mActiveSubscriptionsListeners.add(listener); + } + + /** + * Remove listener from active subscriptions monitor list + * + * @param listener listener to active subscriptions change + */ + public void removeActiveSubscriptionsListener(OnActiveSubscriptionChangedListener listener) { + mActiveSubscriptionsListeners.remove(listener); + } +} diff --git a/src/com/android/settings/network/telephony/MobileNetworkActivity.java b/src/com/android/settings/network/telephony/MobileNetworkActivity.java index 26107222ae5..b4653a760e8 100644 --- a/src/com/android/settings/network/telephony/MobileNetworkActivity.java +++ b/src/com/android/settings/network/telephony/MobileNetworkActivity.java @@ -36,7 +36,7 @@ import com.android.settings.R; import com.android.settings.core.FeatureFlags; import com.android.settings.core.SettingsBaseActivity; import com.android.settings.development.featureflags.FeatureFlagPersistent; -import com.android.settings.network.ActiveSubsciptionsListener; +import com.android.settings.network.ProxySubscriptionManager; import com.google.android.material.bottomnavigation.BottomNavigationView; @@ -45,7 +45,8 @@ import java.util.List; /** * Activity for displaying MobileNetworkSettings */ -public class MobileNetworkActivity extends SettingsBaseActivity { +public class MobileNetworkActivity extends SettingsBaseActivity + implements ProxySubscriptionManager.OnActiveSubscriptionChangedListener { private static final String TAG = "MobileNetworkActivity"; @VisibleForTesting @@ -53,7 +54,7 @@ public class MobileNetworkActivity extends SettingsBaseActivity { @VisibleForTesting static final int SUB_ID_NULL = Integer.MIN_VALUE; - private ActiveSubsciptionsListener mSubscriptionAccess; + private ProxySubscriptionManager mProxySubscriptionMgr; private int mCurSubscriptionId; @Override @@ -86,11 +87,9 @@ public class MobileNetworkActivity extends SettingsBaseActivity { } setActionBar(findViewById(R.id.mobile_action_bar)); - mSubscriptionAccess = new ActiveSubsciptionsListener(this) { - public void onChanged() { - updateSubscriptions(getSubscription()); - } - }; + mProxySubscriptionMgr = ProxySubscriptionManager.getInstance(this); + mProxySubscriptionMgr.setLifecycle(getLifecycle()); + mProxySubscriptionMgr.addActiveSubscriptionsListener(this); mCurSubscriptionId = savedInstanceState != null ? savedInstanceState.getInt(Settings.EXTRA_SUB_ID, SUB_ID_NULL) @@ -105,17 +104,23 @@ public class MobileNetworkActivity extends SettingsBaseActivity { updateTitleAndNavigation(subscription); } - @Override - protected void onStart() { - super.onStart(); - mSubscriptionAccess.start(); + /** + * Implementation of ProxySubscriptionManager.OnActiveSubscriptionChangedListener + */ + public void onChanged() { updateSubscriptions(getSubscription()); } @Override - protected void onStop() { - super.onStop(); - mSubscriptionAccess.stop(); + protected void onStart() { + super.onStart(); + updateSubscriptions(getSubscription()); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + mProxySubscriptionMgr.removeActiveSubscriptionsListener(this); } @Override @@ -164,12 +169,12 @@ public class MobileNetworkActivity extends SettingsBaseActivity { SubscriptionInfo getSubscription() { if (mCurSubscriptionId != SUB_ID_NULL) { final SubscriptionInfo subInfo = - mSubscriptionAccess.getActiveSubscriptionInfo(mCurSubscriptionId); + mProxySubscriptionMgr.getActiveSubscriptionInfo(mCurSubscriptionId); if (subInfo != null) { return subInfo; } } - final List subInfos = mSubscriptionAccess.getActiveSubscriptionsInfo(); + final List subInfos = mProxySubscriptionMgr.getActiveSubscriptionsInfo(); if (CollectionUtils.isEmpty(subInfos)) { return null; } @@ -179,7 +184,7 @@ public class MobileNetworkActivity extends SettingsBaseActivity { private void updateBottomNavigationView() { final BottomNavigationView navigation = findViewById(R.id.bottom_nav); - final List subInfos = mSubscriptionAccess.getActiveSubscriptionsInfo(); + final List subInfos = mProxySubscriptionMgr.getActiveSubscriptionsInfo(); if (CollectionUtils.size(subInfos) <= 1) { navigation.setVisibility(View.GONE); } else { @@ -196,7 +201,7 @@ public class MobileNetworkActivity extends SettingsBaseActivity { if (!isSubscriptionChanged(subId)) { return true; } - final SubscriptionInfo subscriptionInfo = mSubscriptionAccess + final SubscriptionInfo subscriptionInfo = mProxySubscriptionMgr .getActiveSubscriptionInfo(subId); if (subscriptionInfo == null) { return true; diff --git a/tests/robotests/src/com/android/settings/network/GlobalSettingsChangeListenerTest.java b/tests/robotests/src/com/android/settings/network/GlobalSettingsChangeListenerTest.java new file mode 100644 index 00000000000..8f41b4a7e75 --- /dev/null +++ b/tests/robotests/src/com/android/settings/network/GlobalSettingsChangeListenerTest.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2019 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 org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import android.content.Context; +import android.provider.Settings; + +import androidx.lifecycle.Lifecycle; + +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; + +@RunWith(RobolectricTestRunner.class) +public class GlobalSettingsChangeListenerTest { + + @Mock + private Lifecycle mLifecycle; + + private Context mContext; + private GlobalSettingsChangeListener mListener; + + private static final String SETTINGS_FIELD = Settings.Global.AIRPLANE_MODE_ON; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mContext = spy(RuntimeEnvironment.application); + mListener = spy(new GlobalSettingsChangeListener(mContext, SETTINGS_FIELD) { + public void onChanged(String field) {} + }); + + doNothing().when(mLifecycle).addObserver(mListener); + doNothing().when(mLifecycle).removeObserver(mListener); + } + + @Test + public void whenChanged_onChangedBeenCalled() { + mListener.onChange(false); + verify(mListener, times(1)).onChanged(SETTINGS_FIELD); + } + + @Test + public void whenNotifyChangeBasedOnLifecycle_onStopEvent_onChangedNotCalled() { + mListener.notifyChangeBasedOn(mLifecycle); + mListener.onStart(); + + mListener.onChange(false); + verify(mListener, times(1)).onChanged(SETTINGS_FIELD); + + mListener.onStop(); + + mListener.onChange(false); + verify(mListener, times(1)).onChanged(SETTINGS_FIELD); + + mListener.onStart(); + + mListener.onChange(false); + verify(mListener, times(2)).onChanged(SETTINGS_FIELD); + } +}