diff --git a/res/xml/network_and_internet_v2.xml b/res/xml/network_and_internet_v2.xml index 974739d3c0e..8e0b4263edb 100644 --- a/res/xml/network_and_internet_v2.xml +++ b/res/xml/network_and_internet_v2.xml @@ -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"> sResultsForTesting; + + @VisibleForTesting + static void setAvailableSubscriptionsForTesting(List results) { + sResultsForTesting = results; + } + public static List getAvailableSubscriptions(SubscriptionManager manager) { + if (sResultsForTesting != null) { + return sResultsForTesting; + } List 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 index 00000000000..9e55341a7d3 --- /dev/null +++ b/src/com/android/settings/network/SubscriptionsPreferenceController.java @@ -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 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 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 index 00000000000..fbd78672b3d --- /dev/null +++ b/tests/robotests/src/com/android/settings/network/MultiNetworkHeaderControllerTest.java @@ -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 captor = ArgumentCaptor.forClass(Boolean.class); + + verify(mPreferenceCategory, atLeastOnce()).setVisible(captor.capture()); + List 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 captor = ArgumentCaptor.forClass(Boolean.class); + + verify(mPreferenceCategory, atLeastOnce()).setVisible(captor.capture()); + List 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 index 00000000000..016a885d517 --- /dev/null +++ b/tests/robotests/src/com/android/settings/network/SubscriptionsPreferenceControllerTest.java @@ -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 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 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"); + } +}