OSDN Git Service

Add subscriptions list to the multi-network header
authorAntony Sargent <asargent@google.com>
Wed, 12 Dec 2018 17:09:45 +0000 (09:09 -0800)
committerAntony Sargent <asargent@google.com>
Mon, 17 Dec 2018 21:26:13 +0000 (13:26 -0800)
The new UX for showing multiple active mobile plan subscriptions (SIMs,
eSIMs, etc.)  needs a header at the top of the Network & internet page
that shows wifi status and a list of active mobile subscriptions in the
form of one Preference per subscription.

This CL adds display of the mobile subscriptions to this header. It does
not yet show the correct summary text or do anything when you tap on
them - that will be coming in subsequent CLs. Also, since adding a
variable number of items to the top of the page messes up our current
strategy of having a fixed number of hidden items at the bottom based on
overall item count, this CL just makes all items visible. Subsequent CLs
can restore this behavior with dynamic adjustment.

Bug: 116349402
Test: make RunSettingsRoboTests
Change-Id: Ibfadf8cb61f6f5aff6ce38b7974267b1e4ddc719

res/xml/network_and_internet_v2.xml
src/com/android/settings/network/MultiNetworkHeaderController.java
src/com/android/settings/network/NetworkDashboardFragment.java
src/com/android/settings/network/SubscriptionUtil.java
src/com/android/settings/network/SubscriptionsPreferenceController.java [new file with mode: 0644]
tests/robotests/src/com/android/settings/network/MultiNetworkHeaderControllerTest.java [new file with mode: 0644]
tests/robotests/src/com/android/settings/network/SubscriptionsPreferenceControllerTest.java [new file with mode: 0644]

index 974739d..8e0b426 100644 (file)
@@ -18,8 +18,7 @@
     xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:settings="http://schemas.android.com/apk/res-auto"
     android:key="network_and_internet_screen"
-    android:title="@string/network_dashboard_title"
-    settings:initialExpandedChildrenCount="5">
+    android:title="@string/network_dashboard_title">
 
     <PreferenceCategory
         android:key="multi_network_header"
index 1c0fc74..881aaa2 100644 (file)
@@ -19,18 +19,55 @@ package com.android.settings.network;
 import android.content.Context;
 
 import com.android.settings.core.BasePreferenceController;
+import com.android.settingslib.core.lifecycle.Lifecycle;
+
+import androidx.annotation.VisibleForTesting;
+import androidx.preference.PreferenceCategory;
+import androidx.preference.PreferenceScreen;
 
 // This controls a header at the top of the Network & internet page that only appears when there
 // are two or more active mobile subscriptions. It shows an overview of available network
 // connections with an entry for wifi (if connected) and an entry for each subscription.
-public class MultiNetworkHeaderController extends BasePreferenceController {
+public class MultiNetworkHeaderController extends BasePreferenceController implements
+        SubscriptionsPreferenceController.UpdateListener {
+    public static final String TAG = "MultiNetworkHdrCtrl";
+
+    private SubscriptionsPreferenceController mSubscriptionsController;
+    private PreferenceCategory mPreferenceCategory;
 
     public MultiNetworkHeaderController(Context context, String key) {
-      super(context, key);
+        super(context, key);
+    }
+
+    public void init(Lifecycle lifecycle) {
+        mSubscriptionsController = createSubscriptionsController(lifecycle);
+        // TODO(asargent) - add in a controller for showing wifi status here
+    }
+
+    @VisibleForTesting
+    SubscriptionsPreferenceController createSubscriptionsController(Lifecycle lifecycle) {
+        return new SubscriptionsPreferenceController(mContext, lifecycle, this, mPreferenceKey, 10);
+    }
+
+    @Override
+    public void displayPreference(PreferenceScreen screen) {
+        super.displayPreference(screen);
+        mPreferenceCategory = (PreferenceCategory) screen.findPreference(mPreferenceKey);
+        mPreferenceCategory.setVisible(isAvailable());
+        mSubscriptionsController.displayPreference(screen);
     }
 
     @Override
     public int getAvailabilityStatus() {
-        return UNSUPPORTED_ON_DEVICE;
+        if (mSubscriptionsController == null || !mSubscriptionsController.isAvailable()) {
+            return CONDITIONALLY_UNAVAILABLE;
+        } else {
+            return AVAILABLE;
+        }
+    }
+
+    @Override
+    public void onChildrenUpdated() {
+        mPreferenceCategory.setVisible(isAvailable());
     }
 }
index 70481f1..7d94bba 100644 (file)
@@ -72,6 +72,9 @@ public class NetworkDashboardFragment extends DashboardFragment implements
     public void onAttach(Context context) {
         super.onAttach(context);
 
+        if (FeatureFlagUtils.isEnabled(context, FeatureFlags.NETWORK_INTERNET_V2)) {
+            use(MultiNetworkHeaderController.class).init(getSettingsLifecycle());
+        }
         use(AirplaneModePreferenceController.class).setFragment(this);
     }
 
index cfe27db..6b14759 100644 (file)
@@ -24,8 +24,20 @@ import java.util.ArrayList;
 import java.util.Iterator;
 import java.util.List;
 
+import androidx.annotation.VisibleForTesting;
+
 public class SubscriptionUtil {
+    private static List<SubscriptionInfo> sResultsForTesting;
+
+    @VisibleForTesting
+    static void setAvailableSubscriptionsForTesting(List<SubscriptionInfo> results) {
+        sResultsForTesting = results;
+    }
+
     public static List<SubscriptionInfo> getAvailableSubscriptions(SubscriptionManager manager) {
+        if (sResultsForTesting != null) {
+            return sResultsForTesting;
+        }
         List<SubscriptionInfo> subscriptions = manager.getAvailableSubscriptionInfoList();
         if (subscriptions == null) {
             subscriptions = new ArrayList<>();
diff --git a/src/com/android/settings/network/SubscriptionsPreferenceController.java b/src/com/android/settings/network/SubscriptionsPreferenceController.java
new file mode 100644 (file)
index 0000000..9e55341
--- /dev/null
@@ -0,0 +1,183 @@
+/*
+ * Copyright (C) 2018 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_PAUSE;
+import static androidx.lifecycle.Lifecycle.Event.ON_RESUME;
+
+import android.content.Context;
+import android.telephony.SubscriptionInfo;
+import android.telephony.SubscriptionManager;
+
+import com.android.settings.R;
+import com.android.settingslib.core.AbstractPreferenceController;
+
+import java.util.Map;
+
+import androidx.collection.ArrayMap;
+import androidx.lifecycle.Lifecycle;
+import androidx.lifecycle.LifecycleObserver;
+import androidx.lifecycle.OnLifecycleEvent;
+import androidx.preference.Preference;
+import androidx.preference.PreferenceGroup;
+import androidx.preference.PreferenceScreen;
+
+/**
+ * This manages a set of Preferences it places into a PreferenceGroup owned by some parent
+ * controller class - one for each available subscription. This controller is only considered
+ * available if there are 2 or more subscriptions.
+ */
+public class SubscriptionsPreferenceController extends AbstractPreferenceController implements
+        LifecycleObserver, SubscriptionsChangeListener.SubscriptionsChangeListenerClient {
+    private static final String TAG = "SubscriptionsPrefCntrlr";
+
+    private UpdateListener mUpdateListener;
+    private String mPreferenceGroupKey;
+    private PreferenceGroup mPreferenceGroup;
+    private SubscriptionManager mManager;
+    private SubscriptionsChangeListener mSubscriptionsListener;
+
+    // Map of subscription id to Preference
+    private Map<Integer, Preference> mSubscriptionPreferences;
+    private int mStartOrder;
+
+    /**
+     * This interface lets a parent of this class know that some change happened - this could
+     * either be because overall availability changed, or because we've added/removed/updated some
+     * preferences.
+     */
+    public interface UpdateListener {
+        void onChildrenUpdated();
+    }
+
+    /**
+     * @param context            the context for the UI where we're placing these preferences
+     * @param lifecycle          for listening to lifecycle events for the UI
+     * @param updateListener     called to let our parent controller know that our availability has
+     *                           changed, or that one or more of the preferences we've placed in the
+     *                           PreferenceGroup has changed
+     * @param preferenceGroupKey the key used to lookup the PreferenceGroup where Preferences will
+     *                           be placed
+     * @param startOrder         the order that should be given to the first Preference placed into
+     *                           the PreferenceGroup; the second will use startOrder+1, third will
+     *                           use startOrder+2, etc. - this is useful for when the parent wants
+     *                           to have other preferences in the same PreferenceGroup and wants
+     *                           a specific ordering relative to this controller's prefs.
+     */
+    public SubscriptionsPreferenceController(Context context, Lifecycle lifecycle,
+            UpdateListener updateListener, String preferenceGroupKey, int startOrder) {
+        super(context);
+        mUpdateListener = updateListener;
+        mPreferenceGroupKey = preferenceGroupKey;
+        mStartOrder = startOrder;
+        mManager = context.getSystemService(SubscriptionManager.class);
+        mSubscriptionPreferences = new ArrayMap<>();
+        mSubscriptionsListener = new SubscriptionsChangeListener(context, this);
+        lifecycle.addObserver(this);
+    }
+
+    @OnLifecycleEvent(ON_RESUME)
+    public void onResume() {
+        mSubscriptionsListener.start();
+        update();
+    }
+
+    @OnLifecycleEvent(ON_PAUSE)
+    public void onPause() {
+        mSubscriptionsListener.stop();
+    }
+
+    @Override
+    public void displayPreference(PreferenceScreen screen) {
+        mPreferenceGroup = (PreferenceGroup) screen.findPreference(mPreferenceGroupKey);
+        update();
+    }
+
+    private void update() {
+        if (mPreferenceGroup == null) {
+            return;
+        }
+
+        if (mSubscriptionsListener.isAirplaneModeOn()) {
+            for (Preference pref : mSubscriptionPreferences.values()) {
+                mPreferenceGroup.removePreference(pref);
+            }
+            mSubscriptionPreferences.clear();
+            mUpdateListener.onChildrenUpdated();
+            return;
+        }
+
+        final Map<Integer, Preference> existingPrefs = mSubscriptionPreferences;
+        mSubscriptionPreferences = new ArrayMap<>();
+
+        int order = mStartOrder;
+        for (SubscriptionInfo info :  SubscriptionUtil.getAvailableSubscriptions(mManager) ) {
+            final int subId = info.getSubscriptionId();
+            Preference pref = existingPrefs.remove(subId);
+            if (pref == null) {
+                pref = new Preference(mPreferenceGroup.getContext());
+                mPreferenceGroup.addPreference(pref);
+            }
+            pref.setTitle(info.getDisplayName());
+            pref.setIcon(R.drawable.ic_network_cell);
+            pref.setOrder(order++);
+
+            // TODO(asargent) - set summary here to indicate default for calls/sms and data
+
+            pref.setOnPreferenceClickListener(clickedPref -> {
+                // TODO(asargent) - make this start MobileNetworkActivity once we've
+                // added support for it to take a subscription id
+                return true;
+            });
+
+            mSubscriptionPreferences.put(subId, pref);
+        }
+
+        // Remove any old preferences that no longer map to a subscription.
+        for (Preference pref : existingPrefs.values()) {
+            mPreferenceGroup.removePreference(pref);
+        }
+        mUpdateListener.onChildrenUpdated();
+    }
+
+    /**
+     *
+     * @return true if there are at least 2 available subscriptions.
+     */
+    @Override
+    public boolean isAvailable() {
+        if (mSubscriptionsListener.isAirplaneModeOn()) {
+            return false;
+        }
+        return SubscriptionUtil.getAvailableSubscriptions(mManager).size() >= 2;
+    }
+
+    @Override
+    public String getPreferenceKey() {
+        return null;
+    }
+
+    @Override
+    public void onAirplaneModeChanged(boolean airplaneModeEnabled) {
+        update();
+    }
+
+    @Override
+    public void onSubscriptionsChanged() {
+        update();
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/network/MultiNetworkHeaderControllerTest.java b/tests/robotests/src/com/android/settings/network/MultiNetworkHeaderControllerTest.java
new file mode 100644 (file)
index 0000000..fbd7867
--- /dev/null
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2018 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.ArgumentMatchers.eq;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.telephony.SubscriptionManager;
+
+import com.android.settingslib.core.lifecycle.Lifecycle;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+
+import java.util.List;
+
+import androidx.lifecycle.LifecycleOwner;
+import androidx.preference.PreferenceCategory;
+import androidx.preference.PreferenceScreen;
+
+@RunWith(RobolectricTestRunner.class)
+public class MultiNetworkHeaderControllerTest {
+    private static final String KEY_HEADER = "multi_network_header";
+
+    @Mock
+    private PreferenceScreen mPreferenceScreen;
+    @Mock
+    private PreferenceCategory mPreferenceCategory;
+    @Mock
+    private SubscriptionsPreferenceController mSubscriptionsController;
+    @Mock
+    private SubscriptionManager mSubscriptionManager;
+
+    private Context mContext;
+    private LifecycleOwner mLifecycleOwner;
+    private Lifecycle mLifecycle;
+    private MultiNetworkHeaderController mHeaderController;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        mContext = spy(RuntimeEnvironment.application);
+        mLifecycleOwner = () -> mLifecycle;
+        mLifecycle = new Lifecycle(mLifecycleOwner);
+        when(mContext.getSystemService(SubscriptionManager.class)).thenReturn(mSubscriptionManager);
+        when(mPreferenceScreen.findPreference(eq(KEY_HEADER))).thenReturn(mPreferenceCategory);
+
+        mHeaderController = spy(new MultiNetworkHeaderController(mContext, KEY_HEADER));
+        doReturn(mSubscriptionsController).when(mHeaderController).createSubscriptionsController(
+                mLifecycle);
+    }
+
+    @Test
+    public void isAvailable_beforeInitIsCalled_notAvailable() {
+        assertThat(mHeaderController.isAvailable()).isFalse();
+    }
+
+    // When calling displayPreference, the header itself should only be visible if the
+    // subscriptions controller says it is available. This is a helper for test cases of this logic.
+    private void displayPreferenceTest(boolean subscriptionsAvailable,
+            boolean setVisibleExpectedValue) {
+        when(mSubscriptionsController.isAvailable()).thenReturn(subscriptionsAvailable);
+
+        mHeaderController.init(mLifecycle);
+        mHeaderController.displayPreference(mPreferenceScreen);
+        verify(mPreferenceCategory, never()).setVisible(eq(!setVisibleExpectedValue));
+        verify(mPreferenceCategory, atLeastOnce()).setVisible(eq(setVisibleExpectedValue));
+    }
+
+    @Test
+    public void displayPreference_subscriptionsNotAvailable_categoryIsNotVisible() {
+        displayPreferenceTest(false, false);
+    }
+
+    @Test
+    public void displayPreference_subscriptionsAvailable_categoryIsVisible() {
+        displayPreferenceTest(true, true);
+    }
+
+    @Test
+    public void onChildUpdated_subscriptionsBecameAvailable_categoryIsVisible() {
+        when(mSubscriptionsController.isAvailable()).thenReturn(false);
+        mHeaderController.init(mLifecycle);
+        mHeaderController.displayPreference(mPreferenceScreen);
+
+        when(mSubscriptionsController.isAvailable()).thenReturn(true);
+        mHeaderController.onChildrenUpdated();
+        ArgumentCaptor<Boolean> captor = ArgumentCaptor.forClass(Boolean.class);
+
+        verify(mPreferenceCategory, atLeastOnce()).setVisible(captor.capture());
+        List<Boolean> values = captor.getAllValues();
+        assertThat(values.get(values.size()-1)).isEqualTo(Boolean.TRUE);
+    }
+
+    @Test
+    public void onChildUpdated_subscriptionsBecameUnavailable_categoryIsNotVisible() {
+        when(mSubscriptionsController.isAvailable()).thenReturn(true);
+        mHeaderController.init(mLifecycle);
+        mHeaderController.displayPreference(mPreferenceScreen);
+
+        when(mSubscriptionsController.isAvailable()).thenReturn(false);
+        mHeaderController.onChildrenUpdated();
+        ArgumentCaptor<Boolean> captor = ArgumentCaptor.forClass(Boolean.class);
+
+        verify(mPreferenceCategory, atLeastOnce()).setVisible(captor.capture());
+        List<Boolean> values = captor.getAllValues();
+        assertThat(values.get(values.size()-1)).isEqualTo(Boolean.FALSE);
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/network/SubscriptionsPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/network/SubscriptionsPreferenceControllerTest.java
new file mode 100644 (file)
index 0000000..016a885
--- /dev/null
@@ -0,0 +1,218 @@
+/*
+ * Copyright (C) 2018 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.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.provider.Settings;
+import android.telephony.SubscriptionInfo;
+import android.telephony.SubscriptionManager;
+
+import com.android.settingslib.core.lifecycle.Lifecycle;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+
+import androidx.lifecycle.LifecycleOwner;
+import androidx.preference.Preference;
+import androidx.preference.PreferenceCategory;
+import androidx.preference.PreferenceScreen;
+
+@RunWith(RobolectricTestRunner.class)
+public class SubscriptionsPreferenceControllerTest {
+    private static final String KEY = "preference_group";
+
+    @Mock
+    private PreferenceScreen mScreen;
+    @Mock
+    private PreferenceCategory mPreferenceCategory;
+    @Mock
+    private SubscriptionManager mSubscriptionManager;
+
+    private Context mContext;
+    private LifecycleOwner mLifecycleOwner;
+    private Lifecycle mLifecycle;
+    private SubscriptionsPreferenceController mController;
+    private int mOnChildUpdatedCount;
+    private SubscriptionsPreferenceController.UpdateListener mUpdateListener;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        mContext = spy(RuntimeEnvironment.application);
+        mLifecycleOwner = () -> mLifecycle;
+        mLifecycle = new Lifecycle(mLifecycleOwner);
+        when(mContext.getSystemService(SubscriptionManager.class)).thenReturn(mSubscriptionManager);
+        when(mScreen.findPreference(eq(KEY))).thenReturn(mPreferenceCategory);
+        when(mPreferenceCategory.getContext()).thenReturn(mContext);
+        mOnChildUpdatedCount = 0;
+        mUpdateListener = () -> mOnChildUpdatedCount++;
+
+        mController = new SubscriptionsPreferenceController(mContext, mLifecycle, mUpdateListener,
+                KEY, 5);
+    }
+
+    @Test
+    public void isAvailable_oneSubscription_availableFalse() {
+        SubscriptionUtil.setAvailableSubscriptionsForTesting(
+                Arrays.asList(mock(SubscriptionInfo.class)));
+        assertThat(mController.isAvailable()).isFalse();
+    }
+
+    @Test
+    public void isAvailable_twoSubscriptions_availableTrue() {
+        SubscriptionUtil.setAvailableSubscriptionsForTesting(
+                Arrays.asList(mock(SubscriptionInfo.class), mock(SubscriptionInfo.class)));
+        assertThat(mController.isAvailable()).isTrue();
+    }
+
+    @Test
+    public void isAvailable_fiveSubscriptions_availableTrue() {
+        final ArrayList<SubscriptionInfo> subs = new ArrayList<>();
+        for (int i = 0; i < 5; i++) {
+            subs.add(mock(SubscriptionInfo.class));
+        }
+        SubscriptionUtil.setAvailableSubscriptionsForTesting(subs);
+        assertThat(mController.isAvailable()).isTrue();
+    }
+
+    @Test
+    public void isAvailable_airplaneModeOn_availableFalse() {
+        SubscriptionUtil.setAvailableSubscriptionsForTesting(
+                Arrays.asList(mock(SubscriptionInfo.class), mock(SubscriptionInfo.class)));
+        assertThat(mController.isAvailable()).isTrue();
+        Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 1);
+        assertThat(mController.isAvailable()).isFalse();
+    }
+
+    @Test
+    public void onAirplaneModeChanged_airplaneModeTurnedOn_eventFired() {
+        SubscriptionUtil.setAvailableSubscriptionsForTesting(
+                Arrays.asList(mock(SubscriptionInfo.class), mock(SubscriptionInfo.class)));
+        mController.onResume();
+        mController.displayPreference(mScreen);
+        assertThat(mController.isAvailable()).isTrue();
+
+        final int updateCountBeforeModeChange = mOnChildUpdatedCount;
+        Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 1);
+        mController.onAirplaneModeChanged(true);
+        assertThat(mController.isAvailable()).isFalse();
+        assertThat(mOnChildUpdatedCount).isEqualTo(updateCountBeforeModeChange + 1);
+    }
+
+    @Test
+    public void onAirplaneModeChanged_airplaneModeTurnedOff_eventFired() {
+        Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 1);
+        SubscriptionUtil.setAvailableSubscriptionsForTesting(
+                Arrays.asList(mock(SubscriptionInfo.class), mock(SubscriptionInfo.class)));
+        mController.onResume();
+        mController.displayPreference(mScreen);
+        assertThat(mController.isAvailable()).isFalse();
+
+        final int updateCountBeforeModeChange = mOnChildUpdatedCount;
+        Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 0);
+        mController.onAirplaneModeChanged(false);
+        assertThat(mController.isAvailable()).isTrue();
+        assertThat(mOnChildUpdatedCount).isEqualTo(updateCountBeforeModeChange + 1);
+    }
+
+    @Test
+    public void onSubscriptionsChanged_countBecameTwo_eventFired() {
+        final SubscriptionInfo sub1 = mock(SubscriptionInfo.class);
+        final SubscriptionInfo sub2 = mock(SubscriptionInfo.class);
+        SubscriptionUtil.setAvailableSubscriptionsForTesting(Arrays.asList(sub1));
+        mController.onResume();
+        mController.displayPreference(mScreen);
+        assertThat(mController.isAvailable()).isFalse();
+
+        final int updateCountBeforeSubscriptionChange = mOnChildUpdatedCount;
+        SubscriptionUtil.setAvailableSubscriptionsForTesting(Arrays.asList(sub1, sub2));
+        mController.onSubscriptionsChanged();
+        assertThat(mController.isAvailable()).isTrue();
+        assertThat(mOnChildUpdatedCount).isEqualTo(updateCountBeforeSubscriptionChange + 1);
+    }
+
+    @Test
+    public void onSubscriptionsChanged_countBecameOne_eventFired() {
+        final SubscriptionInfo sub1 = mock(SubscriptionInfo.class);
+        final SubscriptionInfo sub2 = mock(SubscriptionInfo.class);
+        SubscriptionUtil.setAvailableSubscriptionsForTesting(Arrays.asList(sub1, sub2));
+        mController.onResume();
+        mController.displayPreference(mScreen);
+        assertThat(mController.isAvailable()).isTrue();
+
+        final int updateCountBeforeSubscriptionChange = mOnChildUpdatedCount;
+        SubscriptionUtil.setAvailableSubscriptionsForTesting(Arrays.asList(sub1));
+        mController.onSubscriptionsChanged();
+        assertThat(mController.isAvailable()).isFalse();
+        assertThat(mOnChildUpdatedCount).isEqualTo(updateCountBeforeSubscriptionChange + 1);
+    }
+
+
+    @Test
+    public void onSubscriptionsChanged_subscriptionReplaced_preferencesChanged() {
+        final SubscriptionInfo sub1 = mock(SubscriptionInfo.class);
+        final SubscriptionInfo sub2 = mock(SubscriptionInfo.class);
+        final SubscriptionInfo sub3 = mock(SubscriptionInfo.class);
+        when(sub1.getDisplayName()).thenReturn("sub1");
+        when(sub2.getDisplayName()).thenReturn("sub2");
+        when(sub3.getDisplayName()).thenReturn("sub3");
+        when(sub1.getSubscriptionId()).thenReturn(1);
+        when(sub2.getSubscriptionId()).thenReturn(2);
+        when(sub3.getSubscriptionId()).thenReturn(3);
+
+        // Start out with only sub1 and sub2.
+        SubscriptionUtil.setAvailableSubscriptionsForTesting(Arrays.asList(sub1, sub2));
+        mController.onResume();
+        mController.displayPreference(mScreen);
+        final ArgumentCaptor<Preference> captor = ArgumentCaptor.forClass(Preference.class);
+        verify(mPreferenceCategory, times(2)).addPreference(captor.capture());
+        assertThat(captor.getAllValues().size()).isEqualTo(2);
+        assertThat(captor.getAllValues().get(0).getTitle()).isEqualTo("sub1");
+        assertThat(captor.getAllValues().get(1).getTitle()).isEqualTo("sub2");
+
+        // Now replace sub2 with sub3, and make sure the old preference was removed and the new
+        // preference was added.
+        final int updateCountBeforeSubscriptionChange = mOnChildUpdatedCount;
+        SubscriptionUtil.setAvailableSubscriptionsForTesting(Arrays.asList(sub1, sub3));
+        mController.onSubscriptionsChanged();
+        assertThat(mController.isAvailable()).isTrue();
+        assertThat(mOnChildUpdatedCount).isEqualTo(updateCountBeforeSubscriptionChange + 1);
+
+        verify(mPreferenceCategory).removePreference(captor.capture());
+        assertThat(captor.getValue().getTitle()).isEqualTo("sub2");
+        verify(mPreferenceCategory, times(3)).addPreference(captor.capture());
+        assertThat(captor.getValue().getTitle()).isEqualTo("sub3");
+    }
+}