diff --git a/res/drawable/ic_calls_sms.xml b/res/drawable/ic_calls_sms.xml new file mode 100644 index 00000000000..e1217a0f172 --- /dev/null +++ b/res/drawable/ic_calls_sms.xml @@ -0,0 +1,30 @@ + + + + + \ No newline at end of file diff --git a/res/drawable/ic_sms.xml b/res/drawable/ic_sms.xml new file mode 100644 index 00000000000..cb388e36108 --- /dev/null +++ b/res/drawable/ic_sms.xml @@ -0,0 +1,25 @@ + + + + \ No newline at end of file diff --git a/res/values/strings.xml b/res/values/strings.xml index 6bfbe6c32b5..9b285230067 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -12447,9 +12447,9 @@ Wi\u2011Fi Calling - Make and receive calls over non-carrier networks like Wi\u2011Fi + Make and receive calls over non\u2011carrier networks like Wi\u2011Fi - Wi-Fi calling allows you to make and receive calls over non-carrier networks such as some Wi\u2011Fi networks.\n\nCross-SIM calling allows you to leverage the mobile data of a SIM to make and receive calls from another SIM. + Wi\u2011Fi calling allows you to make and receive calls over non\u2011carrier networks such as some Wi\u2011Fi networks. Calls diff --git a/res/xml/network_provider_calls_sms.xml b/res/xml/network_provider_calls_sms.xml index b4086fcc4c5..b68b48fc0a8 100644 --- a/res/xml/network_provider_calls_sms.xml +++ b/res/xml/network_provider_calls_sms.xml @@ -25,6 +25,7 @@ android:title="@string/calls_preference_title" settings:controller="com.android.settings.network.telephony.CallsDefaultSubscriptionController" android:order="10" + android:icon="@drawable/ic_phone" /> diff --git a/src/com/android/settings/network/NetworkDashboardFragment.java b/src/com/android/settings/network/NetworkDashboardFragment.java index df5eae5132c..76a84bb43b8 100644 --- a/src/com/android/settings/network/NetworkDashboardFragment.java +++ b/src/com/android/settings/network/NetworkDashboardFragment.java @@ -21,13 +21,13 @@ import android.app.Dialog; import android.app.settings.SettingsEnums; import android.content.Context; import android.os.Bundle; -import android.util.FeatureFlagUtils; import android.util.Log; import androidx.appcompat.app.AlertDialog; import androidx.fragment.app.Fragment; import com.android.settings.R; +import com.android.settings.Utils; import com.android.settings.dashboard.DashboardFragment; import com.android.settings.network.MobilePlanPreferenceController.MobilePlanPreferenceHost; import com.android.settings.search.BaseSearchIndexProvider; @@ -58,7 +58,7 @@ public class NetworkDashboardFragment extends DashboardFragment implements @Override protected int getPreferenceScreenResId() { - if (isProviderModelEnabled(getContext())) { + if (Utils.isProviderModelEnabled(getContext())) { return R.xml.network_provider_internet; } else { return R.xml.network_and_internet; @@ -69,7 +69,7 @@ public class NetworkDashboardFragment extends DashboardFragment implements public void onAttach(Context context) { super.onAttach(context); - if (!isProviderModelEnabled(context)) { + if (!Utils.isProviderModelEnabled(context)) { use(MultiNetworkHeaderController.class).init(getSettingsLifecycle()); } use(AirplaneModePreferenceController.class).setFragment(this); @@ -104,13 +104,15 @@ public class NetworkDashboardFragment extends DashboardFragment implements final MobilePlanPreferenceController mobilePlanPreferenceController = new MobilePlanPreferenceController(context, mobilePlanHost); final WifiPrimarySwitchPreferenceController wifiPreferenceController = - isProviderModelEnabled(context) + Utils.isProviderModelEnabled(context) ? null : new WifiPrimarySwitchPreferenceController( context, metricsFeatureProvider); final InternetPreferenceController internetPreferenceController = - isProviderModelEnabled(context) ? new InternetPreferenceController(context) : null; + Utils.isProviderModelEnabled(context) + ? new InternetPreferenceController(context) + : null; final VpnPreferenceController vpnPreferenceController = new VpnPreferenceController(context); @@ -143,6 +145,9 @@ public class NetworkDashboardFragment extends DashboardFragment implements controllers.add(internetPreferenceController); } controllers.add(privateDnsPreferenceController); + if (Utils.isProviderModelEnabled(context)) { + controllers.add(new NetworkProviderCallsSmsController(context, lifecycle)); + } return controllers; } @@ -187,8 +192,4 @@ public class NetworkDashboardFragment extends DashboardFragment implements null /* mobilePlanHost */); } }; - - private static boolean isProviderModelEnabled(Context context) { - return FeatureFlagUtils.isEnabled(context, FeatureFlagUtils.SETTINGS_PROVIDER_MODEL); - } } diff --git a/src/com/android/settings/network/NetworkProviderCallsSmsController.java b/src/com/android/settings/network/NetworkProviderCallsSmsController.java new file mode 100644 index 00000000000..f7d9221a451 --- /dev/null +++ b/src/com/android/settings/network/NetworkProviderCallsSmsController.java @@ -0,0 +1,204 @@ +/* + * Copyright (C) 2020 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; + +import android.content.Context; +import android.os.UserManager; +import android.telephony.SubscriptionInfo; +import android.telephony.SubscriptionManager; +import android.util.Log; + +import androidx.annotation.VisibleForTesting; +import androidx.lifecycle.LifecycleObserver; +import androidx.lifecycle.OnLifecycleEvent; +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; + +import com.android.settings.R; +import com.android.settingslib.RestrictedPreference; +import com.android.settingslib.core.AbstractPreferenceController; +import com.android.settingslib.core.lifecycle.Lifecycle; + +import java.util.List; + +public class NetworkProviderCallsSmsController extends AbstractPreferenceController implements + SubscriptionsChangeListener.SubscriptionsChangeListenerClient, LifecycleObserver { + + private static final String TAG = "NetworkProviderCallsSmsController"; + private static final String KEY = "calls_and_sms"; + private static final String PREFERRED_CALL_SMS = "preferred"; + private static final String PREFERRED_CALL = "calls preferred"; + private static final String PREFERRED_SMS = "SMS preferred"; + private static final String UNAVAILABLE = "unavailable"; + + private UserManager mUserManager; + private SubscriptionManager mSubscriptionManager; + private SubscriptionsChangeListener mSubscriptionsChangeListener; + + private RestrictedPreference mPreference; + + /** + * The summary text and click behavior of the "Calls & SMS" item on the + * Network & internet page. + */ + public NetworkProviderCallsSmsController(Context context, Lifecycle lifecycle) { + super(context); + + mUserManager = context.getSystemService(UserManager.class); + mSubscriptionManager = context.getSystemService(SubscriptionManager.class); + if (lifecycle != null) { + mSubscriptionsChangeListener = new SubscriptionsChangeListener(context, this); + lifecycle.addObserver(this); + } + } + + @OnLifecycleEvent(Event.ON_RESUME) + public void onResume() { + mSubscriptionsChangeListener.start(); + update(); + } + + @OnLifecycleEvent(Event.ON_PAUSE) + public void onPause() { + mSubscriptionsChangeListener.stop(); + } + + @Override + public void displayPreference(PreferenceScreen screen) { + super.displayPreference(screen); + mPreference = screen.findPreference(getPreferenceKey()); + } + + @Override + public CharSequence getSummary() { + final List subs = SubscriptionUtil.getActiveSubscriptions( + mSubscriptionManager); + + if (subs.isEmpty()) { + return null; + } else { + final StringBuilder summary = new StringBuilder(); + for (SubscriptionInfo subInfo : subs) { + int subsSize = subs.size(); + + // Set displayName as summary if there is only one valid SIM. + if (subsSize == 1 + && SubscriptionManager.isValidSubscriptionId(subInfo.getSubscriptionId())) { + return subInfo.getDisplayName(); + } + + CharSequence status = getPreferredStatus(subInfo); + if (status.toString().isEmpty()) { + // If there are 2 or more SIMs and one of these has no preferred status, + // set only its displayName as summary. + summary.append(subInfo.getDisplayName()); + } else { + summary.append(subInfo.getDisplayName()) + .append(" (") + .append(status) + .append(")"); + } + // Do not add ", " for the last subscription. + if (subInfo != subs.get(subs.size() - 1)) { + summary.append(", "); + } + } + return summary; + } + } + + @VisibleForTesting + protected CharSequence getPreferredStatus(SubscriptionInfo subInfo) { + final int subId = subInfo.getSubscriptionId(); + String status = ""; + boolean isDataPreferred = subId == getDefaultVoiceSubscriptionId(); + boolean isSmsPreferred = subId == getDefaultSmsSubscriptionId(); + + if (!SubscriptionManager.isValidSubscriptionId(subId)) { + status = UNAVAILABLE; + } else { + if (isDataPreferred && isSmsPreferred) { + status = PREFERRED_CALL_SMS; + } else if (isDataPreferred) { + status = PREFERRED_CALL; + } else if (isSmsPreferred) { + status = PREFERRED_SMS; + } + } + return status; + } + + @VisibleForTesting + protected int getDefaultVoiceSubscriptionId(){ + return SubscriptionManager.getDefaultVoiceSubscriptionId(); + } + + @VisibleForTesting + protected int getDefaultSmsSubscriptionId(){ + return SubscriptionManager.getDefaultSmsSubscriptionId(); + } + + private void update() { + if (mPreference == null || mPreference.isDisabledByAdmin()) { + return; + } + refreshSummary(mPreference); + mPreference.setOnPreferenceClickListener(null); + mPreference.setFragment(null); + + final List subs = SubscriptionUtil.getActiveSubscriptions( + mSubscriptionManager); + if (subs.isEmpty()) { + mPreference.setEnabled(false); + } else { + mPreference.setFragment(NetworkProviderCallsSmsFragment.class.getCanonicalName()); + } + } + + @Override + public boolean isAvailable() { + return mUserManager.isAdminUser(); + } + + @Override + public String getPreferenceKey() { + return KEY; + } + + @Override + public void onAirplaneModeChanged(boolean airplaneModeEnabled) { + update(); + } + + @Override + public void updateState(Preference preference) { + super.updateState(preference); + if (preference == null) { + return; + } + refreshSummary(mPreference); + update(); + } + + @Override + public void onSubscriptionsChanged() { + refreshSummary(mPreference); + update(); + } +} diff --git a/src/com/android/settings/network/telephony/DefaultSubscriptionController.java b/src/com/android/settings/network/telephony/DefaultSubscriptionController.java index 5fcedbaec41..560922c8310 100644 --- a/src/com/android/settings/network/telephony/DefaultSubscriptionController.java +++ b/src/com/android/settings/network/telephony/DefaultSubscriptionController.java @@ -36,6 +36,7 @@ import androidx.preference.PreferenceScreen; import com.android.internal.annotations.VisibleForTesting; import com.android.settings.R; +import com.android.settings.Utils; import com.android.settings.network.SubscriptionUtil; import com.android.settings.network.SubscriptionsChangeListener; @@ -90,7 +91,7 @@ public abstract class DefaultSubscriptionController extends TelephonyBasePrefere @Override public int getAvailabilityStatus(int subId) { final List subs = SubscriptionUtil.getActiveSubscriptions(mManager); - if (subs.size() > 1) { + if (subs.size() > 1 || Utils.isProviderModelEnabled(mContext)) { return AVAILABLE; } else { return CONDITIONALLY_UNAVAILABLE; @@ -157,6 +158,12 @@ public abstract class DefaultSubscriptionController extends TelephonyBasePrefere final ArrayList displayNames = new ArrayList<>(); final ArrayList subscriptionIds = new ArrayList<>(); + if (Utils.isProviderModelEnabled(mContext) && subs.size() == 1) { + mPreference.setEnabled(false); + mPreference.setSummary(subs.get(0).getDisplayName()); + return; + } + final int serviceDefaultSubId = getDefaultSubscriptionId(); boolean subIsAvailable = false; diff --git a/tests/unit/src/com/android/settings/network/NetworkProviderCallsSmsControllerTest.java b/tests/unit/src/com/android/settings/network/NetworkProviderCallsSmsControllerTest.java new file mode 100644 index 00000000000..061e1c01c66 --- /dev/null +++ b/tests/unit/src/com/android/settings/network/NetworkProviderCallsSmsControllerTest.java @@ -0,0 +1,297 @@ +/* + * Copyright (C) 2020 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; +import static com.google.common.truth.Truth.assertThat; + +import static org.junit.Assert.assertTrue; + +import android.content.Context; +import android.os.Looper; +import android.telephony.SubscriptionInfo; +import android.telephony.SubscriptionManager; +import android.text.TextUtils; + +import com.android.settingslib.core.lifecycle.Lifecycle; +import com.android.settingslib.RestrictedPreference; + +import androidx.lifecycle.LifecycleOwner; +import androidx.lifecycle.LifecycleRegistry; +import androidx.preference.PreferenceCategory; +import androidx.preference.PreferenceManager; +import androidx.preference.PreferenceScreen; +import androidx.test.annotation.UiThreadTest; +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.InstrumentationRegistry; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.Arrays; + +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +@RunWith(AndroidJUnit4.class) +public class NetworkProviderCallsSmsControllerTest { + + private static final int SUB_ID_1 = 1; + private static final int SUB_ID_2 = 2; + private static final String KEY_PREFERENCE_CALLS_SMS = "calls_and_sms"; + private static final String DISPLAY_NAME_1 = "Sub 1"; + private static final String DISPLAY_NAME_2 = "Sub 2"; + private static final String PREFERRED_CALL_SMS = "preferred"; + private static final String PREFERRED_CALL = "calls preferred"; + private static final String PREFERRED_SMS = "SMS preferred"; + private static final String UNAVAILABLE = "unavailable"; + + @Mock + private SubscriptionManager mSubscriptionManager; + @Mock + private SubscriptionInfo mSubscriptionInfo1; + @Mock + private SubscriptionInfo mSubscriptionInfo2; + @Mock + private Lifecycle mLifecycle; + @Mock + private LifecycleOwner mLifecycleOwner; + private LifecycleRegistry mLifecycleRegistry; + + private MockNetworkProviderCallsSmsController mController; + private PreferenceManager mPreferenceManager; + private PreferenceScreen mPreferenceScreen; + private RestrictedPreference mPreference; + + private Context mContext; + + /** + * Mock the NetworkProviderCallsSmsController that allows allows one to set a default voice + * and SMS subscription ID. + */ + private class MockNetworkProviderCallsSmsController extends + com.android.settings.network.NetworkProviderCallsSmsController { + public MockNetworkProviderCallsSmsController(Context context, Lifecycle lifecycle) { + super(context, lifecycle); + } + + private int mDefaultVoiceSubscriptionId; + private int mDefaultSmsSubscriptionId; + + @Override + protected int getDefaultVoiceSubscriptionId() { + return mDefaultVoiceSubscriptionId; + } + + @Override + protected int getDefaultSmsSubscriptionId() { + return mDefaultSmsSubscriptionId; + } + + public void setDefaultVoiceSubscriptionId(int subscriptionId) { + mDefaultVoiceSubscriptionId = subscriptionId; + } + + public void setDefaultSmsSubscriptionId(int subscriptionId) { + mDefaultSmsSubscriptionId = subscriptionId; + } + } + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mContext = spy(ApplicationProvider.getApplicationContext()); + when(mContext.getSystemService(SubscriptionManager.class)).thenReturn(mSubscriptionManager); + + if (Looper.myLooper() == null) { + Looper.prepare(); + } + + mPreferenceManager = new PreferenceManager(mContext); + mPreferenceScreen = mPreferenceManager.createPreferenceScreen(mContext); + mPreference = new RestrictedPreference(mContext); + mPreference.setKey(KEY_PREFERENCE_CALLS_SMS); + mController = new MockNetworkProviderCallsSmsController(mContext, mLifecycle); + + mLifecycleRegistry = new LifecycleRegistry(mLifecycleOwner); + when(mLifecycleOwner.getLifecycle()).thenReturn(mLifecycleRegistry); + } + + private void displayPreferenceWithLifecycle() { + mLifecycleRegistry.addObserver(mController); + mPreferenceScreen.addPreference(mPreference); + mController.displayPreference(mPreferenceScreen); + mLifecycleRegistry.handleLifecycleEvent(Event.ON_RESUME); + } + + private void setupSubscriptionInfoList(int subId, String displayName, + SubscriptionInfo subscriptionInfo) { + when(subscriptionInfo.getSubscriptionId()).thenReturn(subId); + doReturn(subscriptionInfo).when(mSubscriptionManager).getActiveSubscriptionInfo(subId); + when(subscriptionInfo.getDisplayName()).thenReturn(displayName); + } + + @Test + @UiThreadTest + public void getSummary_invalidSubId_returnUnavailable() { + setupSubscriptionInfoList(SubscriptionManager.INVALID_SUBSCRIPTION_ID, DISPLAY_NAME_1, + mSubscriptionInfo1); + when(mSubscriptionManager.getActiveSubscriptionInfoList()).thenReturn( + Arrays.asList(mSubscriptionInfo1)); + displayPreferenceWithLifecycle(); + + final StringBuilder summary = new StringBuilder(); + summary.append(DISPLAY_NAME_1) + .append(" (") + .append(UNAVAILABLE) + .append(")"); + + assertTrue(TextUtils.equals(mController.getSummary(), summary)); + } + + @Test + @UiThreadTest + public void getSummary_oneIsInvalidSubIdTwoIsValidSubId_returnOneIsUnavailable() { + setupSubscriptionInfoList(SubscriptionManager.INVALID_SUBSCRIPTION_ID, DISPLAY_NAME_1, + mSubscriptionInfo1); + setupSubscriptionInfoList(SUB_ID_2, DISPLAY_NAME_2, mSubscriptionInfo2); + when(mSubscriptionManager.getActiveSubscriptionInfoList()).thenReturn( + Arrays.asList(mSubscriptionInfo1, mSubscriptionInfo2)); + displayPreferenceWithLifecycle(); + + final StringBuilder summary = new StringBuilder(); + summary.append(DISPLAY_NAME_1) + .append(" (") + .append(UNAVAILABLE) + .append(")") + .append(", ") + .append(DISPLAY_NAME_2); + + assertTrue(TextUtils.equals(mController.getSummary(), summary)); + } + + + + @Test + @UiThreadTest + public void getSummary_oneSubscription_returnDisplayName() { + setupSubscriptionInfoList(SUB_ID_1, DISPLAY_NAME_1, mSubscriptionInfo1); + when(mSubscriptionManager.getActiveSubscriptionInfoList()).thenReturn( + Arrays.asList(mSubscriptionInfo1)); + displayPreferenceWithLifecycle(); + + assertThat(mPreference.getSummary()).isEqualTo(DISPLAY_NAME_1); + } + + @Test + @UiThreadTest + public void getSummary_allSubscriptionsHaveNoPreferredStatus_returnDisplayName() { + setupSubscriptionInfoList(SUB_ID_1, DISPLAY_NAME_1, mSubscriptionInfo1); + setupSubscriptionInfoList(SUB_ID_2, DISPLAY_NAME_2, mSubscriptionInfo2); + when(mSubscriptionManager.getActiveSubscriptionInfoList()).thenReturn( + Arrays.asList(mSubscriptionInfo1, mSubscriptionInfo2)); + displayPreferenceWithLifecycle(); + + final StringBuilder summary = new StringBuilder(); + summary.append(DISPLAY_NAME_1).append(", ").append(DISPLAY_NAME_2); + + assertTrue(TextUtils.equals(mController.getSummary(), summary)); + } + + @Test + @UiThreadTest + public void getSummary_oneSubscriptionsIsCallPreferredTwoIsSmsPreferred_returnStatus() { + + mController.setDefaultVoiceSubscriptionId(SUB_ID_1); + mController.setDefaultSmsSubscriptionId(SUB_ID_2); + + setupSubscriptionInfoList(SUB_ID_1, DISPLAY_NAME_1, mSubscriptionInfo1); + setupSubscriptionInfoList(SUB_ID_2, DISPLAY_NAME_2, mSubscriptionInfo2); + when(mSubscriptionManager.getActiveSubscriptionInfoList()).thenReturn( + Arrays.asList(mSubscriptionInfo1, mSubscriptionInfo2)); + displayPreferenceWithLifecycle(); + + final StringBuilder summary = new StringBuilder(); + summary.append(DISPLAY_NAME_1) + .append(" (") + .append(PREFERRED_CALL) + .append(")") + .append(", ") + .append(DISPLAY_NAME_2) + .append(" (") + .append(PREFERRED_SMS) + .append(")"); + + assertTrue(TextUtils.equals(mController.getSummary(), summary)); + } + + @Test + @UiThreadTest + public void getSummary_oneSubscriptionsIsSmsPreferredTwoIsCallPreferred_returnStatus() { + + mController.setDefaultVoiceSubscriptionId(SUB_ID_2); + mController.setDefaultSmsSubscriptionId(SUB_ID_1); + + setupSubscriptionInfoList(SUB_ID_1, DISPLAY_NAME_1, mSubscriptionInfo1); + setupSubscriptionInfoList(SUB_ID_2, DISPLAY_NAME_2, mSubscriptionInfo2); + when(mSubscriptionManager.getActiveSubscriptionInfoList()).thenReturn( + Arrays.asList(mSubscriptionInfo1, mSubscriptionInfo2)); + displayPreferenceWithLifecycle(); + + final StringBuilder summary = new StringBuilder(); + summary.append(DISPLAY_NAME_1) + .append(" (") + .append(PREFERRED_SMS) + .append(")") + .append(", ") + .append(DISPLAY_NAME_2) + .append(" (") + .append(PREFERRED_CALL) + .append(")"); + + assertTrue(TextUtils.equals(mController.getSummary(), summary)); + } + + @Test + @UiThreadTest + public void getSummary_oneSubscriptionsIsSmsPreferredAndIsCallPreferred_returnStatus() { + + mController.setDefaultVoiceSubscriptionId(SUB_ID_1); + mController.setDefaultSmsSubscriptionId(SUB_ID_1); + + setupSubscriptionInfoList(SUB_ID_1, DISPLAY_NAME_1, mSubscriptionInfo1); + setupSubscriptionInfoList(SUB_ID_2, DISPLAY_NAME_2, mSubscriptionInfo2); + when(mSubscriptionManager.getActiveSubscriptionInfoList()).thenReturn( + Arrays.asList(mSubscriptionInfo1, mSubscriptionInfo2)); + displayPreferenceWithLifecycle(); + + final StringBuilder summary = new StringBuilder(); + summary.append(DISPLAY_NAME_1) + .append(" (") + .append(PREFERRED_CALL_SMS) + .append(")") + .append(", ") + .append(DISPLAY_NAME_2); + + assertTrue(TextUtils.equals(mController.getSummary(), summary)); + } +}