diff --git a/res/values/strings.xml b/res/values/strings.xml index c861ed9c3c8..46069950728 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -2398,6 +2398,13 @@ %1$s is the only SIM in your device. Do you want to use this SIM for mobile data, calls, and SMS messages? + + Switch SIMs automatically? + + Allow your phone to automatically switch to %1$s for mobile data when it has better availability. + + \n\nCalls, messages, and network traffic may be visible to your organization. + Incorrect SIM PIN code you must now contact your carrier to unlock your device. diff --git a/src/com/android/settings/sim/EnableAutoDataSwitchDialogFragment.java b/src/com/android/settings/sim/EnableAutoDataSwitchDialogFragment.java new file mode 100644 index 00000000000..b1b5f8e69ed --- /dev/null +++ b/src/com/android/settings/sim/EnableAutoDataSwitchDialogFragment.java @@ -0,0 +1,209 @@ +/* + * Copyright (C) 2022 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.sim; + +import android.app.Dialog; +import android.app.settings.SettingsEnums; +import android.content.DialogInterface; +import android.os.Bundle; +import android.os.UserHandle; +import android.os.UserManager; +import android.telephony.SubscriptionInfo; +import android.telephony.SubscriptionManager; +import android.telephony.TelephonyManager; +import android.text.TextUtils; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; +import androidx.appcompat.app.AlertDialog; + +import com.android.settings.R; +import com.android.settings.network.SubscriptionUtil; + +import java.util.List; + +/** + * Show a dialog prompting the user to enable auto data switch following the dialog where user chose + * default data SIM. + */ +public class EnableAutoDataSwitchDialogFragment extends SimDialogFragment implements + DialogInterface.OnClickListener { + private static final String TAG = "EnableAutoDataSwitchDialogFragment"; + /** Sub Id of the non-default data SIM */ + private int mBackupDataSubId = SubscriptionManager.INVALID_SUBSCRIPTION_ID; + + /** @return a new instance of this fragment */ + public static EnableAutoDataSwitchDialogFragment newInstance() { + final EnableAutoDataSwitchDialogFragment fragment = + new EnableAutoDataSwitchDialogFragment(); + final Bundle args = initArguments(SimDialogActivity.ENABLE_AUTO_DATA_SWITCH, + R.string.enable_auto_data_switch_dialog_title); + fragment.setArguments(args); + return fragment; + } + + @NonNull + @Override + public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { + final AlertDialog dialog = new AlertDialog.Builder(getContext()) + .setPositiveButton(R.string.yes, this) + .setNegativeButton(R.string.sim_action_no_thanks, null) + .create(); + updateDialog(dialog); + return dialog; + } + + @Override + public int getMetricsCategory() { + return SettingsEnums.DIALOG_AUTO_DATA_SWITCH; + } + + /** update dialog */ + public void updateDialog(AlertDialog dialog) { + Log.d(TAG, "Dialog updated, dismiss status: " + mWasDismissed); + + if (mWasDismissed) { + return; + } + + if (dialog == null) { + Log.d(TAG, "Dialog is null."); + dismiss(); + return; + } + + // Set message + View content = LayoutInflater.from(getContext()).inflate( + R.layout.sim_confirm_dialog_multiple_enabled_profiles_supported, null); + TextView dialogMessage = content != null ? content.findViewById(R.id.msg) : null; + final String message = getMessage(); + if (TextUtils.isEmpty(message) || dialogMessage == null) { + onDismiss(dialog); + return; + } + dialogMessage.setText(message); + dialogMessage.setVisibility(View.VISIBLE); + dialog.setView(content); + + // Set title + View titleView = LayoutInflater.from(getContext()).inflate( + R.layout.sim_confirm_dialog_title_multiple_enabled_profiles_supported, null); + TextView titleTextView = titleView.findViewById(R.id.title); + titleTextView.setText(getContext().getString(getTitleResId())); + dialog.setCustomTitle(titleTextView); + } + + /** + * @return The message of the dialog. {@code null} if the dialog shouldn't be displayed. + */ + @VisibleForTesting + protected String getMessage() { + int ddsSubId = getDefaultDataSubId(); + if (ddsSubId == SubscriptionManager.INVALID_SUBSCRIPTION_ID) return null; + Log.d(TAG, "DDS SubId: " + ddsSubId); + + SubscriptionManager subscriptionManager = getSubscriptionManager(); + List activeSubscriptions = subscriptionManager + .getActiveSubscriptionInfoList(); + if (activeSubscriptions == null) return null; + + // Find if a backup data sub exists. + SubscriptionInfo backupSubInfo = activeSubscriptions.stream() + .filter(subInfo -> subInfo.getSubscriptionId() != ddsSubId) + .findFirst() + .orElse(null); + if (backupSubInfo == null) return null; + mBackupDataSubId = backupSubInfo.getSubscriptionId(); + + // Check if auto data switch is already enabled + final TelephonyManager telephonyManager = getTelephonyManagerForSub(mBackupDataSubId); + if (telephonyManager == null) { + Log.d(TAG, "telephonyManager for " + mBackupDataSubId + " is null"); + return null; + } + if (telephonyManager.isMobileDataPolicyEnabled( + TelephonyManager.MOBILE_DATA_POLICY_AUTO_DATA_SWITCH)) { + Log.d(TAG, "AUTO_DATA_SWITCH already enabled"); + return null; + } + + Log.d(TAG, "Backup data sub Id: " + mBackupDataSubId); + // The description of the feature + String message = + getContext().getString( + R.string.enable_auto_data_switch_dialog_message, + SubscriptionUtil.getUniqueSubscriptionDisplayName( + backupSubInfo, getContext())); + UserManager userManager = getUserManager(); + if (userManager == null) return message; + + // If one of the sub is dedicated to work profile(enterprise-managed), which means we might + // switching between personal & work profile, append a warning to the message. + UserHandle ddsUserHandle = subscriptionManager.getSubscriptionUserHandle(ddsSubId); + UserHandle nDdsUserHandle = subscriptionManager.getSubscriptionUserHandle(mBackupDataSubId); + boolean isDdsManaged = ddsUserHandle != null && userManager.isManagedProfile( + ddsUserHandle.getIdentifier()); + boolean isNDdsManaged = nDdsUserHandle != null && userManager.isManagedProfile( + nDdsUserHandle.getIdentifier()); + Log.d(TAG, "isDdsManaged= " + isDdsManaged + " isNDdsManaged=" + isNDdsManaged); + if (isDdsManaged ^ isNDdsManaged) { + message += getContext().getString( + R.string.auto_data_switch_dialog_managed_profile_warning); + } + + return message; + } + + @Override + public void updateDialog() { + updateDialog((AlertDialog) getDialog()); + } + + @Override + public void onClick(DialogInterface dialog, int buttonClicked) { + if (buttonClicked != DialogInterface.BUTTON_POSITIVE) { + return; + } + final SimDialogActivity activity = (SimDialogActivity) getActivity(); + if (mBackupDataSubId != SubscriptionManager.INVALID_SUBSCRIPTION_ID) { + activity.onSubscriptionSelected(getDialogType(), mBackupDataSubId); + } + } + + private TelephonyManager getTelephonyManagerForSub(int subId) { + return getContext().getSystemService(TelephonyManager.class) + .createForSubscriptionId(subId); + } + + private SubscriptionManager getSubscriptionManager() { + return getContext().getSystemService(SubscriptionManager.class); + } + + @VisibleForTesting + protected int getDefaultDataSubId() { + return SubscriptionManager.getDefaultDataSubscriptionId(); + } + + private UserManager getUserManager() { + return getContext().getSystemService(UserManager.class); + } +} diff --git a/src/com/android/settings/sim/SelectSpecificDataSimDialogFragment.java b/src/com/android/settings/sim/SelectSpecificDataSimDialogFragment.java index b2ca6210a86..6ac0067d382 100644 --- a/src/com/android/settings/sim/SelectSpecificDataSimDialogFragment.java +++ b/src/com/android/settings/sim/SelectSpecificDataSimDialogFragment.java @@ -72,14 +72,31 @@ public class SelectSpecificDataSimDialogFragment extends SimDialogFragment imple } @Override - public void onClick(DialogInterface dialog, int buttonClicked) { - if (buttonClicked != DialogInterface.BUTTON_POSITIVE) { - return; + public void onDismiss(@NonNull DialogInterface dialog) { + Log.d(TAG, "Dialog onDismiss, dismiss status: " + mWasDismissed); + if (!mWasDismissed) { + // This dialog might be called onDismiss twice due to first time called by onDismiss() + // as a consequence of user action. We need this fragment alive so the activity + // doesn't end, which allows the following dialog to attach. Upon the second dialog + // dismiss, this fragment is removed from SimDialogActivity.onFragmentDismissed to + // end the activity. + mWasDismissed = true; + final SimDialogActivity activity = (SimDialogActivity) getActivity(); + activity.showEnableAutoDataSwitchDialog(); + // Not using super.onDismiss because it will result in an immediate end of the activity, + // before the second auto data switch dialog can attach. + if (getDialog() != null) getDialog().dismiss(); } + } + + @Override + public void onClick(DialogInterface dialog, int buttonClicked) { final SimDialogActivity activity = (SimDialogActivity) getActivity(); - final SubscriptionInfo info = getTargetSubscriptionInfo(); - if (info != null) { - activity.onSubscriptionSelected(getDialogType(), info.getSubscriptionId()); + if (buttonClicked == DialogInterface.BUTTON_POSITIVE) { + final SubscriptionInfo info = getTargetSubscriptionInfo(); + if (info != null) { + activity.onSubscriptionSelected(getDialogType(), info.getSubscriptionId()); + } } } diff --git a/src/com/android/settings/sim/SimDialogActivity.java b/src/com/android/settings/sim/SimDialogActivity.java index 464ba9b4a51..a1258be64b0 100644 --- a/src/com/android/settings/sim/SimDialogActivity.java +++ b/src/com/android/settings/sim/SimDialogActivity.java @@ -16,8 +16,6 @@ package com.android.settings.sim; -import static android.content.Context.MODE_PRIVATE; - import android.app.Activity; import android.content.Intent; import android.content.SharedPreferences; @@ -61,6 +59,8 @@ public class SimDialogActivity extends FragmentActivity { public static final int SMS_PICK_FOR_MESSAGE = 4; // Dismiss the current dialog and finish the activity. public static final int PICK_DISMISS = 5; + // Show auto data switch dialog(when user enables multi-SIM) + public static final int ENABLE_AUTO_DATA_SWITCH = 6; @Override protected void onCreate(Bundle savedInstanceState) { @@ -122,7 +122,7 @@ public class SimDialogActivity extends FragmentActivity { private SimDialogFragment createFragment(int dialogType) { switch (dialogType) { case DATA_PICK: - return getDataPickDialogFramgent(); + return getDataPickDialogFragment(); case CALLS_PICK: return CallsSimListDialogFragment.newInstance(dialogType, R.string.select_sim_for_calls, @@ -141,12 +141,14 @@ public class SimDialogActivity extends FragmentActivity { return SimListDialogFragment.newInstance(dialogType, R.string.select_sim_for_sms, false /* includeAskEveryTime */, false /* isCancelItemShowed */); + case ENABLE_AUTO_DATA_SWITCH: + return EnableAutoDataSwitchDialogFragment.newInstance(); default: throw new IllegalArgumentException("Invalid dialog type " + dialogType + " sent."); } } - private SimDialogFragment getDataPickDialogFramgent() { + private SimDialogFragment getDataPickDialogFragment() { if (SubscriptionManager.getDefaultDataSubscriptionId() == SubscriptionManager.INVALID_SUBSCRIPTION_ID) { return SimListDialogFragment.newInstance(DATA_PICK, R.string.select_sim_for_data, @@ -181,15 +183,40 @@ public class SimDialogActivity extends FragmentActivity { intent.putExtra(RESULT_SUB_ID, subId); setResult(Activity.RESULT_OK, intent); break; + case ENABLE_AUTO_DATA_SWITCH: + onEnableAutoDataSwitch(subId); + break; default: throw new IllegalArgumentException( "Invalid dialog type " + dialogType + " sent."); } } + /** + * Show dialog prompting the user to enable auto data switch + */ + public void showEnableAutoDataSwitchDialog() { + final FragmentManager fragmentManager = getSupportFragmentManager(); + SimDialogFragment fragment = createFragment(ENABLE_AUTO_DATA_SWITCH); + fragment.show(fragmentManager, Integer.toString(ENABLE_AUTO_DATA_SWITCH)); + } + + /** + * @param subId The sub Id to enable auto data switch + */ + public void onEnableAutoDataSwitch(int subId) { + Log.d(TAG, "onEnableAutoDataSwitch subId:" + subId); + final TelephonyManager telephonyManager = getSystemService( + TelephonyManager.class).createForSubscriptionId(subId); + telephonyManager.setMobileDataPolicyEnabled( + TelephonyManager.MOBILE_DATA_POLICY_AUTO_DATA_SWITCH, true); + } + public void onFragmentDismissed(SimDialogFragment simDialogFragment) { final List fragments = getSupportFragmentManager().getFragments(); - if (fragments.size() == 1 && fragments.get(0) == simDialogFragment) { + if (fragments.size() == 1 && fragments.get(0) == simDialogFragment + || simDialogFragment.getDialogType() == ENABLE_AUTO_DATA_SWITCH) { + Log.d(TAG, "onFragmentDismissed dialogType:" + simDialogFragment.getDialogType()); finishAndRemoveTask(); } } @@ -200,7 +227,8 @@ public class SimDialogActivity extends FragmentActivity { TelephonyManager.class).createForSubscriptionId(subId); subscriptionManager.setDefaultDataSubId(subId); if (subId != SubscriptionManager.INVALID_SUBSCRIPTION_ID) { - telephonyManager.setDataEnabled(true); + telephonyManager.setDataEnabledForReason(TelephonyManager.DATA_ENABLED_REASON_USER, + true); Toast.makeText(this, R.string.data_switch_started, Toast.LENGTH_LONG).show(); } } diff --git a/src/com/android/settings/sim/SimListDialogFragment.java b/src/com/android/settings/sim/SimListDialogFragment.java index 245d31e8980..5b84d7b67c0 100644 --- a/src/com/android/settings/sim/SimListDialogFragment.java +++ b/src/com/android/settings/sim/SimListDialogFragment.java @@ -109,16 +109,18 @@ public class SimListDialogFragment extends SimDialogFragment { * @param selectionIndex the index of item in the list. */ public void onClick(int selectionIndex) { + final SimDialogActivity activity = (SimDialogActivity) getActivity(); if (selectionIndex >= 0 && selectionIndex < mSubscriptions.size()) { int subId = SubscriptionManager.INVALID_SUBSCRIPTION_ID; final SubscriptionInfo subscription = mSubscriptions.get(selectionIndex); if (subscription != null) { subId = subscription.getSubscriptionId(); } - final SimDialogActivity activity = (SimDialogActivity) getActivity(); activity.onSubscriptionSelected(getDialogType(), subId); } - dismiss(); + Log.d(TAG, "Start showing auto data switch dialog"); + activity.showEnableAutoDataSwitchDialog(); + if (getDialog() != null) getDialog().dismiss(); } protected List getCurrentSubscriptions() { diff --git a/tests/robotests/src/com/android/settings/sim/EnableAutoDataSwitchDialogFragmentTest.java b/tests/robotests/src/com/android/settings/sim/EnableAutoDataSwitchDialogFragmentTest.java new file mode 100644 index 00000000000..ad60d06dc04 --- /dev/null +++ b/tests/robotests/src/com/android/settings/sim/EnableAutoDataSwitchDialogFragmentTest.java @@ -0,0 +1,147 @@ +/* + * Copyright (C) 2022 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.sim; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.spy; + +import android.content.Context; +import android.os.UserHandle; +import android.os.UserManager; +import android.telephony.SubscriptionManager; +import android.telephony.TelephonyManager; + +import com.android.settings.R; +import com.android.settings.testutils.shadow.ShadowAlertDialogCompat; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import java.util.Arrays; +import java.util.Collections; + +@RunWith(RobolectricTestRunner.class) +@Config(shadows = ShadowAlertDialogCompat.class) +public class EnableAutoDataSwitchDialogFragmentTest + extends SimDialogFragmentTestBase { + private static final String SUMMARY = "fake summary"; + private static final String WARNING = "fake warning"; + + // Mock + @Mock + private Context mContext; + @Mock + private SubscriptionManager mSubscriptionManager; + @Mock + private TelephonyManager mTelephonyManager; + @Mock + private UserManager mUserManager; + + @Before + public void setUp() { + super.setUp(); + mFragment = spy(EnableAutoDataSwitchDialogFragment.newInstance()); + doReturn(mContext).when(mFragment).getContext(); + + doReturn(mSubscriptionManager).when(mContext).getSystemService(SubscriptionManager.class); + doReturn(mTelephonyManager).when(mContext).getSystemService(TelephonyManager.class); + doReturn(mTelephonyManager).when(mTelephonyManager).createForSubscriptionId(anyInt()); + doReturn(mUserManager).when(mContext).getSystemService(UserManager.class); + + doReturn(SIM1_ID).when(mFragment).getDefaultDataSubId(); + doReturn(Arrays.asList(mSim1, mSim2)).when(mSubscriptionManager) + .getActiveSubscriptionInfoList(); + doReturn(true).when(mUserManager) + .isManagedProfile(UserHandle.MIN_SECONDARY_USER_ID); + + doReturn(SUMMARY).when(mContext).getString( + eq(R.string.enable_auto_data_switch_dialog_message), any()); + doReturn(WARNING).when(mContext).getString( + R.string.auto_data_switch_dialog_managed_profile_warning); + } + + @After + public void tearDown() { + mFragment = null; + } + + @Test + public void updateDialog_getMessage_noDdsExists() { + doReturn(SubscriptionManager.INVALID_SUBSCRIPTION_ID).when(mFragment).getDefaultDataSubId(); + String msg = mFragment.getMessage(); + assertThat(msg).isEqualTo(null); + } + + @Test + public void updateDialog_getMessage_noBackupSubExists() { + doReturn(Collections.singletonList(mSim1)).when(mSubscriptionManager) + .getActiveSubscriptionInfoList(); + String msg = mFragment.getMessage(); + assertThat(msg).isEqualTo(null); + } + + @Test + public void updateDialog_getMessage_autoSwitchAlreadyEnabled() { + doReturn(true).when(mTelephonyManager).isMobileDataPolicyEnabled( + TelephonyManager.MOBILE_DATA_POLICY_AUTO_DATA_SWITCH); + String msg = mFragment.getMessage(); + assertThat(msg).isEqualTo(null); + } + + @Test + public void updateDialog_getMessage_noManagedProfile() { + UserHandle userHandle = UserHandle.of(UserHandle.USER_NULL); + UserHandle userHandle2 = UserHandle.of(UserHandle.USER_SYSTEM); + doReturn(userHandle).when(mSubscriptionManager).getSubscriptionUserHandle(SIM1_ID); + doReturn(userHandle2).when(mSubscriptionManager).getSubscriptionUserHandle(SIM2_ID); + String msg = mFragment.getMessage(); + assertThat(msg).contains(SUMMARY); + assertThat(msg).doesNotContain(WARNING); + } + + @Test + public void updateDialog_getMessage_hasManagedProfile() { + UserHandle userHandle = UserHandle.of(UserHandle.USER_NULL); + UserHandle userHandle2 = UserHandle.of(UserHandle.MIN_SECONDARY_USER_ID); + doReturn(userHandle).when(mSubscriptionManager).getSubscriptionUserHandle(SIM1_ID); + doReturn(userHandle2).when(mSubscriptionManager).getSubscriptionUserHandle(SIM2_ID); + String msg = mFragment.getMessage(); + assertThat(msg).contains(SUMMARY); + assertThat(msg).contains(WARNING); + } + + @Test + public void updateDialog_getMessage_BothManagedProfile() { + UserHandle userHandle = UserHandle.of(UserHandle.MIN_SECONDARY_USER_ID); + UserHandle userHandle2 = UserHandle.of(UserHandle.MIN_SECONDARY_USER_ID); + doReturn(userHandle).when(mSubscriptionManager).getSubscriptionUserHandle(SIM1_ID); + doReturn(userHandle2).when(mSubscriptionManager).getSubscriptionUserHandle(SIM2_ID); + String msg = mFragment.getMessage(); + assertThat(msg).contains(SUMMARY); + assertThat(msg).doesNotContain(WARNING); + } +}