diff --git a/src/com/android/settings/network/ActiveSubsciptionsListener.java b/src/com/android/settings/network/ActiveSubsciptionsListener.java new file mode 100644 index 00000000000..3e4272d62ce --- /dev/null +++ b/src/com/android/settings/network/ActiveSubsciptionsListener.java @@ -0,0 +1,320 @@ +/* + * 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 android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Handler; +import android.os.Looper; +import android.telephony.CarrierConfigManager; +import android.telephony.SubscriptionInfo; +import android.telephony.SubscriptionManager; +import android.telephony.TelephonyManager; +import android.text.TextUtils; +import android.util.Log; + +import androidx.annotation.VisibleForTesting; + +import com.android.internal.telephony.TelephonyIntents; + +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * A listener for active subscription change + */ +public abstract class ActiveSubsciptionsListener + extends SubscriptionManager.OnSubscriptionsChangedListener + implements AutoCloseable { + + private static final String TAG = "ActiveSubsciptions"; + private static final boolean DEBUG = false; + + /** + * Constructor + * + * @param looper {@code Looper} of this listener + * @param context {@code Context} of this listener + */ + public ActiveSubsciptionsListener(Looper looper, Context context) { + mLooper = looper; + mContext = context; + + mCacheState = new AtomicInteger(STATE_NOT_LISTENING); + mMaxActiveSubscriptionInfos = new AtomicInteger(MAX_SUBSCRIPTION_UNKNOWN); + + mSubscriptionChangeIntentFilter = new IntentFilter(); + mSubscriptionChangeIntentFilter.addAction( + CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED); + mSubscriptionChangeIntentFilter.addAction( + TelephonyIntents.ACTION_RADIO_TECHNOLOGY_CHANGED); + mSubscriptionChangeIntentFilter.addAction( + TelephonyManager.ACTION_MULTI_SIM_CONFIG_CHANGED); + } + + @VisibleForTesting + BroadcastReceiver getSubscriptionChangeReceiver() { + return new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (isInitialStickyBroadcast()) { + return; + } + final String action = intent.getAction(); + if (TextUtils.isEmpty(action)) { + return; + } + if (CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED.equals(action)) { + final int subId = intent.getIntExtra( + CarrierConfigManager.EXTRA_SUBSCRIPTION_INDEX, + SubscriptionManager.INVALID_SUBSCRIPTION_ID); + if (!clearCachedSubId(subId)) { + return; + } + } + onSubscriptionsChanged(); + } + }; + } + + private Looper mLooper; + private Context mContext; + + private static final int STATE_NOT_LISTENING = 0; + private static final int STATE_STOPPING = 1; + private static final int STATE_PREPARING = 2; + private static final int STATE_LISTENING = 3; + private static final int STATE_DATA_CACHED = 4; + + private AtomicInteger mCacheState; + private SubscriptionManager mSubscriptionManager; + + private IntentFilter mSubscriptionChangeIntentFilter; + private BroadcastReceiver mSubscriptionChangeReceiver; + + private static final int MAX_SUBSCRIPTION_UNKNOWN = -1; + + private AtomicInteger mMaxActiveSubscriptionInfos; + private List mCachedActiveSubscriptionInfo; + + /** + * Active subscriptions got changed + */ + public abstract void onChanged(); + + @Override + public void onSubscriptionsChanged() { + // clear value in cache + clearCache(); + listenerNotify(); + } + + /** + * Start listening subscriptions change + */ + public void start() { + monitorSubscriptionsChange(true); + } + + /** + * Stop listening subscriptions change + */ + public void stop() { + monitorSubscriptionsChange(false); + } + + /** + * Implementation of {@code AutoCloseable} + */ + public void close() { + stop(); + } + + /** + * Get SubscriptionManager + * + * @return a SubscriptionManager + */ + public SubscriptionManager getSubscriptionManager() { + if (mSubscriptionManager == null) { + mSubscriptionManager = mContext.getSystemService(SubscriptionManager.class); + } + return mSubscriptionManager; + } + + /** + * Get current max. number active subscription info(s) been setup within device + * + * @return max. number of active subscription info(s) + */ + public int getActiveSubscriptionInfoCountMax() { + int cacheState = mCacheState.get(); + if (cacheState < STATE_LISTENING) { + return getSubscriptionManager().getActiveSubscriptionInfoCountMax(); + } + + mMaxActiveSubscriptionInfos.compareAndSet(MAX_SUBSCRIPTION_UNKNOWN, + getSubscriptionManager().getActiveSubscriptionInfoCountMax()); + return mMaxActiveSubscriptionInfos.get(); + } + + /** + * Get a list of active subscription info + * + * @return A list of active subscription info + */ + public List getActiveSubscriptionsInfo() { + if (mCacheState.get() >= STATE_DATA_CACHED) { + return mCachedActiveSubscriptionInfo; + } + mCachedActiveSubscriptionInfo = getSubscriptionManager().getActiveSubscriptionInfoList(); + mCacheState.compareAndSet(STATE_LISTENING, STATE_DATA_CACHED); + + if (DEBUG) { + if ((mCachedActiveSubscriptionInfo == null) + || (mCachedActiveSubscriptionInfo.size() <= 0)) { + Log.d(TAG, "active subscriptions: " + mCachedActiveSubscriptionInfo); + } else { + final StringBuilder logString = new StringBuilder("active subscriptions:"); + for (SubscriptionInfo subInfo : mCachedActiveSubscriptionInfo) { + logString.append(" " + subInfo.getSubscriptionId()); + } + Log.d(TAG, logString.toString()); + } + } + + return mCachedActiveSubscriptionInfo; + } + + /** + * 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) { + final List subInfoList = getActiveSubscriptionsInfo(); + if (subInfoList == null) { + return null; + } + for (SubscriptionInfo subInfo : subInfoList) { + if (subInfo.getSubscriptionId() == subId) { + return subInfo; + } + } + return null; + } + + /** + * Get a list of all subscription info which accessible by Settings app + * + * @return A list of accessible subscription info + */ + public List getAccessibleSubscriptionsInfo() { + return getSubscriptionManager().getAvailableSubscriptionInfoList(); + } + + /** + * Get an accessible subscription info with given subscription ID + * + * @param subId target subscription ID + * @return A subscription info which is accessible list + */ + public SubscriptionInfo getAccessibleSubscriptionInfo(int subId) { + // Always check if subId is part of activeSubscriptions + // since there's cache design within SubscriptionManager. + // That give us a chance to avoid from querying ContentProvider. + final SubscriptionInfo activeSubInfo = getActiveSubscriptionInfo(subId); + if (activeSubInfo != null) { + return activeSubInfo; + } + + final List subInfoList = getAccessibleSubscriptionsInfo(); + if (subInfoList == null) { + return null; + } + for (SubscriptionInfo subInfo : subInfoList) { + if (subInfo.getSubscriptionId() == subId) { + return subInfo; + } + } + return null; + } + + /** + * Clear data cached within listener + */ + public void clearCache() { + mMaxActiveSubscriptionInfos.set(MAX_SUBSCRIPTION_UNKNOWN); + mCacheState.compareAndSet(STATE_DATA_CACHED, STATE_LISTENING); + mCachedActiveSubscriptionInfo = null; + } + + private void monitorSubscriptionsChange(boolean on) { + if (on) { + if (!mCacheState.compareAndSet(STATE_NOT_LISTENING, STATE_PREPARING)) { + return; + } + + if (mSubscriptionChangeReceiver == null) { + mSubscriptionChangeReceiver = getSubscriptionChangeReceiver(); + } + mContext.registerReceiver(mSubscriptionChangeReceiver, + mSubscriptionChangeIntentFilter, null, new Handler(mLooper)); + getSubscriptionManager().addOnSubscriptionsChangedListener(this); + mCacheState.compareAndSet(STATE_PREPARING, STATE_LISTENING); + return; + } + + final int currentState = mCacheState.getAndSet(STATE_STOPPING); + if (currentState <= STATE_STOPPING) { + mCacheState.compareAndSet(STATE_STOPPING, currentState); + return; + } + if (mSubscriptionChangeReceiver != null) { + mContext.unregisterReceiver(mSubscriptionChangeReceiver); + } + getSubscriptionManager().removeOnSubscriptionsChangedListener(this); + clearCache(); + mCacheState.compareAndSet(STATE_STOPPING, STATE_NOT_LISTENING); + } + + private void listenerNotify() { + if (mCacheState.get() < STATE_LISTENING) { + return; + } + onChanged(); + } + + private boolean clearCachedSubId(int subId) { + if (mCacheState.get() < STATE_DATA_CACHED) { + return false; + } + if (mCachedActiveSubscriptionInfo == null) { + return false; + } + for (SubscriptionInfo subInfo : mCachedActiveSubscriptionInfo) { + if (subInfo.getSubscriptionId() == subId) { + clearCache(); + return true; + } + } + return false; + } +} diff --git a/src/com/android/settings/network/GlobalSettingsChangeListener.java b/src/com/android/settings/network/GlobalSettingsChangeListener.java new file mode 100644 index 00000000000..4c58c609ee5 --- /dev/null +++ b/src/com/android/settings/network/GlobalSettingsChangeListener.java @@ -0,0 +1,136 @@ +/* + * 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.os.Looper; +import android.provider.Settings; + +import androidx.lifecycle.Lifecycle; +import androidx.lifecycle.LifecycleObserver; +import androidx.lifecycle.OnLifecycleEvent; + +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * A listener for Settings.Global configuration change, with support of Lifecycle + */ +public abstract class GlobalSettingsChangeListener extends ContentObserver + implements LifecycleObserver, AutoCloseable { + + /** + * Constructor + * + * @param context {@code Context} of this listener + * @param field field of Global Settings + */ + public GlobalSettingsChangeListener(Context context, String field) { + this(Looper.getMainLooper(), context, field); + } + + /** + * Constructor + * + * @param looper {@code Looper} for processing callback + * @param context {@code Context} of this listener + * @param field field of Global Settings + */ + public GlobalSettingsChangeListener(Looper looper, Context context, String field) { + super(new Handler(looper)); + mContext = context; + mField = field; + mUri = Settings.Global.getUriFor(field); + mListening = new AtomicBoolean(false); + monitorUri(true); + } + + private Context mContext; + private String mField; + private Uri mUri; + private AtomicBoolean mListening; + 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 (!mListening.get()) { + return; + } + onChanged(mField); + } + + @OnLifecycleEvent(ON_START) + void onStart() { + monitorUri(true); + } + + @OnLifecycleEvent(ON_STOP) + void onStop() { + monitorUri(false); + } + + @OnLifecycleEvent(ON_DESTROY) + void onDestroy() { + close(); + } + + /** + * Implementation of AutoCloseable + */ + public void close() { + monitorUri(false); + notifyChangeBasedOn(null); + } + + private void monitorUri(boolean on) { + if (!mListening.compareAndSet(!on, on)) { + return; + } + + if (on) { + mContext.getContentResolver().registerContentObserver(mUri, false, this); + return; + } + + 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..b10c727d2c9 --- /dev/null +++ b/src/com/android/settings/network/ProxySubscriptionManager.java @@ -0,0 +1,237 @@ +/* + * 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.os.Looper; +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 implements LifecycleObserver { + + /** + * Interface for monitor active subscriptions list changing + */ + public interface OnActiveSubscriptionChangedListener { + /** + * When active subscriptions list get changed + */ + void onChanged(); + /** + * get Lifecycle of listener + * + * @return Returns Lifecycle. + */ + default Lifecycle getLifecycle() { + return null; + } + } + + /** + * 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.getApplicationContext()); + return sSingleton; + } + + private static ProxySubscriptionManager sSingleton; + + private ProxySubscriptionManager(Context context) { + final Looper looper = Looper.getMainLooper(); + + mActiveSubscriptionsListeners = + new ArrayList(); + + mSubsciptionsMonitor = new ActiveSubsciptionsListener(looper, context) { + public void onChanged() { + notifyAllListeners(); + } + }; + mAirplaneModeMonitor = new GlobalSettingsChangeListener(looper, + context, Settings.Global.AIRPLANE_MODE_ON) { + public void onChanged(String field) { + mSubsciptionsMonitor.clearCache(); + notifyAllListeners(); + } + }; + + mSubsciptionsMonitor.start(); + } + + private Lifecycle mLifecycle; + private ActiveSubsciptionsListener mSubsciptionsMonitor; + private GlobalSettingsChangeListener mAirplaneModeMonitor; + + private List mActiveSubscriptionsListeners; + + private void notifyAllListeners() { + for (OnActiveSubscriptionChangedListener listener : mActiveSubscriptionsListeners) { + final Lifecycle lifecycle = listener.getLifecycle(); + if ((lifecycle == null) + || (lifecycle.getCurrentState().isAtLeast(Lifecycle.State.STARTED))) { + listener.onChanged(); + } + } + } + + /** + * Lifecycle for data within proxy + * + * @param lifecycle life cycle to reference + */ + public void setLifecycle(Lifecycle lifecycle) { + if (mLifecycle == lifecycle) { + return; + } + if (mLifecycle != null) { + mLifecycle.removeObserver(this); + } + if (lifecycle != null) { + lifecycle.addObserver(this); + } + mLifecycle = lifecycle; + mAirplaneModeMonitor.notifyChangeBasedOn(lifecycle); + } + + @OnLifecycleEvent(ON_START) + void onStart() { + mSubsciptionsMonitor.start(); + } + + @OnLifecycleEvent(ON_STOP) + void onStop() { + mSubsciptionsMonitor.stop(); + } + + @OnLifecycleEvent(ON_DESTROY) + void onDestroy() { + mSubsciptionsMonitor.close(); + mAirplaneModeMonitor.close(); + + if (mLifecycle != null) { + mLifecycle.removeObserver(this); + mLifecycle = null; + + sSingleton = null; + } + } + + /** + * Get SubscriptionManager + * + * @return a SubscriptionManager + */ + public SubscriptionManager get() { + return mSubsciptionsMonitor.getSubscriptionManager(); + } + + /** + * Get current max. number active subscription info(s) been setup within device + * + * @return max. number of active subscription info(s) + */ + public int getActiveSubscriptionInfoCountMax() { + return mSubsciptionsMonitor.getActiveSubscriptionInfoCountMax(); + } + + /** + * 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); + } + + /** + * Get a list of accessible subscription info + * + * @return A list of accessible subscription info + */ + public List getAccessibleSubscriptionsInfo() { + return mSubsciptionsMonitor.getAccessibleSubscriptionsInfo(); + } + + /** + * Get an accessible subscription info with given subscription ID + * + * @param subId target subscription ID + * @return A subscription info which is accessible list + */ + public SubscriptionInfo getAccessibleSubscriptionInfo(int subId) { + return mSubsciptionsMonitor.getAccessibleSubscriptionInfo(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) { + if (mActiveSubscriptionsListeners.contains(listener)) { + return; + } + 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/tests/robotests/src/com/android/settings/network/ActiveSubsciptionsListenerTest.java b/tests/robotests/src/com/android/settings/network/ActiveSubsciptionsListenerTest.java new file mode 100644 index 00000000000..4c7b55b64f0 --- /dev/null +++ b/tests/robotests/src/com/android/settings/network/ActiveSubsciptionsListenerTest.java @@ -0,0 +1,200 @@ +/* + * 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.doReturn; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.robolectric.Shadows.shadowOf; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.os.Looper; +import android.telephony.CarrierConfigManager; +import android.telephony.SubscriptionInfo; +import android.telephony.SubscriptionManager; +import android.telephony.TelephonyManager; + +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import com.android.internal.telephony.TelephonyIntents; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.MockitoAnnotations; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.shadow.api.Shadow; +import org.robolectric.shadows.ShadowBroadcastReceiver; +import org.robolectric.shadows.ShadowContextImpl; +import org.robolectric.shadows.ShadowSubscriptionManager; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; + +@RunWith(AndroidJUnit4.class) +public class ActiveSubsciptionsListenerTest { + private static final int SUB_ID1 = 3; + private static final int SUB_ID2 = 7; + + private static final Intent INTENT_RADIO_TECHNOLOGY_CHANGED = + new Intent(TelephonyIntents.ACTION_RADIO_TECHNOLOGY_CHANGED); + + private static final Intent INTENT_MULTI_SIM_CONFIG_CHANGED = + new Intent(TelephonyManager.ACTION_MULTI_SIM_CONFIG_CHANGED); + + private static final Intent INTENT_CARRIER_CONFIG_CHANGED = + new Intent(CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED); + + private Context mContext; + private ShadowContextImpl mShadowContextImpl; + private SubscriptionManager mSubscriptionManager; + private ShadowSubscriptionManager mShadowSubscriptionManager; + private List mActiveSubscriptions; + + private ActiveSubsciptionsListenerImpl mListener; + private BroadcastReceiver mReceiver; + private ShadowBroadcastReceiver mShadowReceiver; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + mContext = RuntimeEnvironment.application.getBaseContext(); + mShadowContextImpl = Shadow.extract(mContext); + + mSubscriptionManager = spy(mContext.getSystemService(SubscriptionManager.class)); + mShadowSubscriptionManager = shadowOf(mSubscriptionManager); + + mActiveSubscriptions = new ArrayList(); + mActiveSubscriptions.add(ShadowSubscriptionManager.SubscriptionInfoBuilder + .newBuilder().setId(SUB_ID1).buildSubscriptionInfo()); + mActiveSubscriptions.add(ShadowSubscriptionManager.SubscriptionInfoBuilder + .newBuilder().setId(SUB_ID2).buildSubscriptionInfo()); + mShadowSubscriptionManager.setActiveSubscriptionInfoList(mActiveSubscriptions); + + mListener = spy(new ActiveSubsciptionsListenerImpl(Looper.getMainLooper(), mContext)); + doReturn(mSubscriptionManager).when(mListener).getSubscriptionManager(); + mReceiver = mListener.getSubscriptionChangeReceiver(); + mShadowReceiver = shadowOf(mReceiver); + doReturn(mReceiver).when(mListener).getSubscriptionChangeReceiver(); + } + + @After + public void cleanUp() { + mListener.close(); + } + + private class ActiveSubsciptionsListenerImpl extends ActiveSubsciptionsListener { + private ActiveSubsciptionsListenerImpl(Looper looper, Context context) { + super(looper, context); + } + public void onChanged() {} + } + + private void sendIntentToReceiver(Intent intent) { + mShadowReceiver.onReceive(mContext, intent, new AtomicBoolean(false)); + } + + @Test + public void constructor_noListeningWasSetup() { + verify(mListener, never()).onChanged(); + } + + @Test + public void start_configChangedIntent_onChangedShouldBeCalled() { + sendIntentToReceiver(INTENT_RADIO_TECHNOLOGY_CHANGED); + sendIntentToReceiver(INTENT_MULTI_SIM_CONFIG_CHANGED); + verify(mListener, never()).onChanged(); + + mListener.start(); + + sendIntentToReceiver(INTENT_RADIO_TECHNOLOGY_CHANGED); + verify(mListener, times(1)).onChanged(); + + sendIntentToReceiver(INTENT_MULTI_SIM_CONFIG_CHANGED); + verify(mListener, times(2)).onChanged(); + + mListener.stop(); + + sendIntentToReceiver(INTENT_RADIO_TECHNOLOGY_CHANGED); + sendIntentToReceiver(INTENT_MULTI_SIM_CONFIG_CHANGED); + verify(mListener, times(2)).onChanged(); + } + + @Test + public void start_carrierConfigChangedIntent_onChangedWhenSubIdBeenCached() { + sendIntentToReceiver(INTENT_CARRIER_CONFIG_CHANGED); + verify(mListener, never()).onChanged(); + + mListener.start(); + + mListener.getActiveSubscriptionsInfo(); + + sendIntentToReceiver(INTENT_CARRIER_CONFIG_CHANGED); + verify(mListener, never()).onChanged(); + + INTENT_CARRIER_CONFIG_CHANGED.putExtra(CarrierConfigManager.EXTRA_SUBSCRIPTION_INDEX, + SUB_ID2); + sendIntentToReceiver(INTENT_CARRIER_CONFIG_CHANGED); + verify(mListener, times(1)).onChanged(); + + mListener.stop(); + + sendIntentToReceiver(INTENT_CARRIER_CONFIG_CHANGED); + verify(mListener, times(1)).onChanged(); + } + + + @Test + public void start_alwaysFetchAndCacheResult() { + mListener.start(); + + List subInfoList = null; + int numberOfAccess = 0; + for (int numberOfSubInfo = mActiveSubscriptions.size(); numberOfSubInfo >= 0; + numberOfSubInfo--) { + if (mActiveSubscriptions.size() > numberOfSubInfo) { + mActiveSubscriptions.remove(numberOfSubInfo); + } + mShadowSubscriptionManager.setActiveSubscriptionInfoList(mActiveSubscriptions); + + // fetch twice and test if they generated access to SubscriptionManager only once + subInfoList = mListener.getActiveSubscriptionsInfo(); + subInfoList = mListener.getActiveSubscriptionsInfo(); + + numberOfAccess++; + verify(mSubscriptionManager, times(numberOfAccess)).getActiveSubscriptionInfoList(); + + mListener.clearCache(); + } + + mShadowSubscriptionManager.setActiveSubscriptionInfoList(null); + + // fetch twice and test if they generated access to SubscriptionManager only once + subInfoList = mListener.getActiveSubscriptionsInfo(); + subInfoList = mListener.getActiveSubscriptionsInfo(); + + numberOfAccess++; + verify(mSubscriptionManager, times(numberOfAccess)).getActiveSubscriptionInfoList(); + } +} 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..e4190714a50 --- /dev/null +++ b/tests/robotests/src/com/android/settings/network/GlobalSettingsChangeListenerTest.java @@ -0,0 +1,86 @@ +/* + * 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.os.Looper; +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(Looper.getMainLooper(), + 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); + } +}