Slot Change Receiver Migration

Implemented slot change cases when pSIM is inserted and removed.
Bug: 153811431
Bug: 170508680
Test: Manually tested

Change-Id: Ib0a96da1d7d702f7c64e75b929c73b8548f8e459
This commit is contained in:
Jiashen Wang
2021-01-18 19:14:00 -08:00
parent 0457af5347
commit b54d25ee13
17 changed files with 936 additions and 22 deletions

View File

@@ -3669,6 +3669,26 @@
</intent-filter>
</receiver>
<activity
android:name=".sim.ChooseSimActivity"
android:theme="@style/GlifV3Theme.DayNight.NoActionBar"
android:launchMode="singleInstance"
android:exported="false"/>
<activity
android:name=".sim.SwitchToEsimConfirmDialogActivity"
android:exported="false"
android:permission="android.permission.WRITE_EMBEDDED_SUBSCRIPTIONS"
android:launchMode="singleInstance"
android:theme="@style/Transparent" />
<activity
android:name=".sim.DsdsDialogActivity"
android:exported="false"
android:permission="android.permission.WRITE_EMBEDDED_SUBSCRIPTIONS"
android:launchMode="singleInstance"
android:theme="@style/Transparent" />
<service android:name=".sim.SimNotificationService"
android:permission="android.permission.BIND_JOB_SERVICE" />

View File

@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Copyright (C) 2021 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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="32"
android:viewportHeight="32">
<path
android:pathData="M 0 0 H 32 V 32 H 0 V 0 Z" />
<path
android:fillColor="@color/homepage_generic_icon_background"
android:pathData="M24,5.33h1.33c0.74,0,1.33,0.6,1.33,1.33v18.67c0,0.74-0.6,1.33-1.33,1.33H24c-0.74,0-1.33-0.6-1.33-1.33
V6.67C22.67,5.93,23.26,5.33,24,5.33z" />
<path
android:fillColor="@color/homepage_generic_icon_background"
android:pathData="M8,18.67h1.33c0.74,0,1.33,0.6,1.33,1.33v5.33c0,0.74-0.6,1.33-1.33,1.33H8c-0.74,0-1.33-0.6-1.33-1.33V20
C6.67,19.26,7.26,18.67,8,18.67z" />
<path
android:fillColor="@color/homepage_generic_icon_background"
android:pathData="M16,12h1.33c0.74,0,1.33,0.6,1.33,1.33v12c0,0.74-0.6,1.33-1.33,1.33H16c-0.74,0-1.33-0.6-1.33-1.33v-12
C14.67,12.6,15.26,12,16,12z" />
</vector>

View File

@@ -0,0 +1,48 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2021 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.
-->
<com.google.android.setupdesign.GlifLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/glif_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:icon="@drawable/ic_network_signal_blue">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
style="@style/SudContentFrame"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="@dimen/subtitle_bottom_padding">
<TextView
android:id="@+id/subtitle"
style="@style/SudDescription.Glif"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
<com.google.android.setupdesign.GlifRecyclerLayout
android:id="@+id/recycler_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:entries="@xml/items_multiple_carrier" />
</LinearLayout>
</com.google.android.setupdesign.GlifLayout>

View File

@@ -21,6 +21,7 @@
<style name="GlifTheme.DayNight" parent="GlifTheme" />
<style name="GlifV2Theme.DayNight" parent="GlifV2Theme" />
<style name="GlifV3Theme.DayNight" parent="GlifV3Theme" />
<style name="GlifV3Theme.DayNight.NoActionBar" parent="GlifV3Theme.NoActionBar" />
<style name="GlifV2Theme.DayNight.Transparent" parent="GlifV2Theme.Transparent" />
<style name="GlifV3Theme.DayNight.Transparent" parent="GlifV3Theme.Transparent" />
<style name="SetupWizardTheme.DayNight.Transparent" parent="SetupWizardTheme.Transparent" />

View File

@@ -440,4 +440,7 @@
<!-- Text padding for EmptyTextSettings -->
<dimen name="empty_text_padding">24dp</dimen>
<!-- Choose SIM Activity dimens -->
<dimen name="subtitle_bottom_padding">24dp</dimen>
</resources>

View File

@@ -12129,6 +12129,34 @@
<string name="post_dsds_reboot_notification_title_with_carrier"><xliff:g id="carrier_name" example="Google Fi">%1$s</xliff:g> is active</string>
<!-- The body text of post DSDS reboot notification. [CHAR LIMIT=NONE] -->
<string name="post_dsds_reboot_notification_text">Tap to update SIM settings</string>
<!-- Title on a push notification indicating that the user's device switched to a new mobile network. [CHAR LIMIT=NONE] -->
<string name="switch_to_removable_notification">Switched to <xliff:g id="carrier_name" example="Google Fi">%1$s</xliff:g></string>
<!-- Title on a push notification indicating that the user's device switched to a new mobile network. [CHAR LIMIT=NONE] -->
<string name="switch_to_removable_notification_no_carrier_name">Switched to another carrier</string>
<!-- Message in a push notification indicating that the user's phone has connected to a different mobile network. [CHAR LIMIT=NONE] -->
<string name="network_changed_notification_text">Your mobile network has changed</string>
<!-- Strings for choose SIM activity -->
<!-- The title text of choose SIM activity. [CHAR LIMIT=NONE] -->
<string name="choose_sim_title">Choose a number to use</string>
<!-- The body text of choose SIM activity. [CHAR LIMIT=NONE] -->
<string name="choose_sim_text"><xliff:g id="number" example="2">%1$d</xliff:g> numbers are available on this device, but only one can be used at a time</string>
<!-- String indicating that we are activating the profile [CHAR LIMIT=NONE] -->
<string name="choose_sim_activating">Activating<xliff:g id="ellipsis" example="...">&#8230;</xliff:g></string>
<!-- String indicating that we failed to activate the selected profile [CHAR LIMIT=NONE] -->
<string name="choose_sim_could_not_activate">Couldn\u2019t be activated right now</string>
<!-- String indicating that the number for the specified profile is unknown [CHAR LIMIT=NONE] -->
<string name="choose_sim_item_summary_unknown">Unknown number</string>
<!-- Strings for switch SIM confirmation dialog. -->
<!-- The title text of switch SIM confirmation dialog. [CHAR LIMIT=NONE] -->
<string name="switch_sim_dialog_title">Use <xliff:g id="carrier_name" example="Google Fi">%1$s</xliff:g>?</string>
<!-- The body text of switch SIM confirmation dialog. [CHAR LIMIT=NONE] -->
<string name="switch_sim_dialog_text"><xliff:g id="carrier_name" example="Google Fi">%1$s</xliff:g> will be used for mobile data, calls, and SMS.</string>
<!-- The title text of skip sim switch dialog. [CHAR LIMIT=NONE] -->
<string name="switch_sim_dialog_no_switch_title">No active SIMs available</string>
<!-- The body text of skip sim switch dialog. [CHAR LIMIT=NONE] -->
<string name="switch_sim_dialog_no_switch_text">To use mobile data, call features, and SMS at a later time, go to your network settings</string>
<!-- Button label of the removable sim card. [CHAR LIMIT=NONE] -->
<string name="sim_card_label">SIM card</string>

View File

@@ -133,6 +133,14 @@
<item name="*android:lockPatternStyle">@style/LockPatternStyle.Setup</item>
</style>
<style name="GlifV3Theme.Light.NoActionBar" parent="GlifV3Theme.Light">
<item name="android:windowActionBar">false</item>
</style>
<style name="GlifV3Theme.NoActionBar" parent="GlifV3Theme">
<item name="android:windowActionBar">false</item>
</style>
<style name="GlifV2Theme.Transparent">
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:windowNoTitle">true</item>
@@ -216,6 +224,7 @@
<style name="GlifTheme.DayNight" parent="GlifTheme.Light" />
<style name="GlifV2Theme.DayNight" parent="GlifV2Theme.Light" />
<style name="GlifV3Theme.DayNight" parent="GlifV3Theme.Light" />
<style name="GlifV3Theme.DayNight.NoActionBar" parent="GlifV3Theme.Light.NoActionBar" />
<style name="GlifV2Theme.DayNight.Transparent" parent="GlifV2Theme.Light.Transparent" />
<style name="GlifV3Theme.DayNight.Transparent" parent="GlifV3Theme.Light.Transparent" />
<style name="SetupWizardTheme.DayNight.Transparent" parent="SetupWizardTheme.Light.Transparent" />

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2021 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.
-->
<ItemGroup xmlns:android="http://schemas.android.com/apk/res/android" />

View File

@@ -24,15 +24,18 @@ import static com.android.internal.util.CollectionUtils.emptyIfNull;
import android.annotation.Nullable;
import android.content.Context;
import android.os.ParcelUuid;
import android.telephony.PhoneNumberUtils;
import android.telephony.SubscriptionInfo;
import android.telephony.SubscriptionManager;
import android.telephony.TelephonyManager;
import android.telephony.UiccCardInfo;
import android.telephony.UiccSlotInfo;
import android.text.TextUtils;
import android.util.Log;
import androidx.annotation.VisibleForTesting;
import com.android.internal.telephony.MccTable;
import com.android.settings.network.telephony.DeleteEuiccSubscriptionDialogActivity;
import com.android.settings.network.telephony.ToggleSubscriptionDialogActivity;
import com.android.settingslib.DeviceInfoUtils;
@@ -514,4 +517,64 @@ public class SubscriptionUtil {
.filter(sub -> sub.isEmbedded() && groupUuid.equals(sub.getGroupUuid()))
.collect(Collectors.toList());
}
/** Returns the formatted phone number of a subscription. */
@Nullable
public static String getFormattedPhoneNumber(
Context context, SubscriptionInfo subscriptionInfo) {
if (subscriptionInfo == null) {
Log.e(TAG, "Invalid subscription.");
return null;
}
TelephonyManager telephonyManager = context.getSystemService(TelephonyManager.class);
String rawPhoneNumber =
telephonyManager.getLine1Number(subscriptionInfo.getSubscriptionId());
String countryIso = MccTable.countryCodeForMcc(subscriptionInfo.getMccString());
if (TextUtils.isEmpty(rawPhoneNumber)) {
return null;
}
return PhoneNumberUtils.formatNumber(rawPhoneNumber, countryIso);
}
/**
* Returns the subscription on a removable sim card. The device does not need to be on removable
* slot.
*/
@Nullable
public static SubscriptionInfo getFirstRemovableSubscription(Context context) {
TelephonyManager telephonyManager = context.getSystemService(TelephonyManager.class);
SubscriptionManager subscriptionManager =
context.getSystemService(SubscriptionManager.class);
List<UiccCardInfo> cardInfos = telephonyManager.getUiccCardsInfo();
if (cardInfos == null) {
Log.w(TAG, "UICC cards info list is empty.");
return null;
}
List<SubscriptionInfo> allSubscriptions = subscriptionManager.getAllSubscriptionInfoList();
if (allSubscriptions == null) {
Log.w(TAG, "All subscription info list is empty.");
return null;
}
for (UiccCardInfo cardInfo : cardInfos) {
if (cardInfo == null) {
Log.w(TAG, "Got null card.");
continue;
}
if (!cardInfo.isRemovable()
|| cardInfo.getCardId() == TelephonyManager.UNSUPPORTED_CARD_ID) {
Log.i(TAG, "Skip embedded card or invalid cardId on slot: "
+ cardInfo.getSlotIndex());
continue;
}
Log.i(TAG, "Target removable cardId :" + cardInfo.getCardId());
for (SubscriptionInfo subInfo : allSubscriptions) {
// Match the removable card id with subscription card id.
if (cardInfo.getCardId() == subInfo.getCardId()) {
return subInfo;
}
}
}
return null;
}
}

View File

@@ -177,10 +177,7 @@ public class ToggleSubscriptionDialogActivity extends SubscriptionActionDialogAc
showRebootConfirmDialog();
return;
}
Log.i(
TAG,
"Enabling DSDS without rebooting. "
+ getString(R.string.sim_action_enabling_sim_without_carrier_name));
Log.i(TAG, "Enabling DSDS without rebooting.");
showProgressDialog(
getString(R.string.sim_action_enabling_sim_without_carrier_name));
mEnableMultiSimSidecar.run(NUM_OF_SIMS_FOR_DSDS);
@@ -272,7 +269,7 @@ public class ToggleSubscriptionDialogActivity extends SubscriptionActionDialogAc
case SidecarFragment.State.ERROR:
mEnableMultiSimSidecar.reset();
Log.i(TAG, "Failed to switch to DSDS without rebooting.");
ProgressDialogFragment.dismiss(getFragmentManager());
dismissProgressDialog();
showErrorDialog(
getString(R.string.dsds_activation_failure_title),
getString(R.string.dsds_activation_failure_body_msg2));
@@ -290,7 +287,7 @@ public class ToggleSubscriptionDialogActivity extends SubscriptionActionDialogAc
Log.i(TAG, "DSDS enabled, start to enable pSIM profile.");
handleTogglePsimAction();
ProgressDialogFragment.dismiss(getFragmentManager());
dismissProgressDialog();
finish();
}

View File

@@ -0,0 +1,321 @@
/*
* Copyright (C) 2021 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.Activity;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.telephony.SubscriptionInfo;
import android.telephony.SubscriptionManager;
import android.text.TextUtils;
import android.util.Log;
import android.view.View;
import android.widget.TextView;
import com.android.settings.R;
import com.android.settings.SidecarFragment;
import com.android.settings.network.SubscriptionUtil;
import com.android.settings.network.SwitchToEuiccSubscriptionSidecar;
import com.android.settings.network.SwitchToRemovableSlotSidecar;
import com.android.settings.network.UiccSlotUtil;
import com.google.android.setupdesign.GlifLayout;
import com.google.android.setupdesign.GlifRecyclerLayout;
import com.google.android.setupdesign.items.Dividable;
import com.google.android.setupdesign.items.IItem;
import com.google.android.setupdesign.items.Item;
import com.google.android.setupdesign.items.ItemGroup;
import com.google.android.setupdesign.items.RecyclerItemAdapter;
import com.google.android.setupdesign.view.HeaderRecyclerView;
import java.util.ArrayList;
import java.util.List;
/** Activity to show a list of profiles for user to choose. */
public class ChooseSimActivity extends Activity
implements RecyclerItemAdapter.OnItemSelectedListener, SidecarFragment.Listener {
// Whether there is a pSIM profile in the selection list.
public static final String KEY_HAS_PSIM = "has_psim";
// After the user selects eSIM profile, whether continue to show Mobile Network Settings screen
// to select other preferences.
// Note: KEY_NO_PSIM_CONTINUE_TO_SETTINGS and mNoPsimContinueToSettings are not used for now
// for UI changes. We may use them in the future.
public static final String KEY_NO_PSIM_CONTINUE_TO_SETTINGS = "no_psim_continue_to_settings";
private static final String TAG = "ChooseSimActivity";
private static final int INDEX_PSIM = -1;
private static final String STATE_SELECTED_INDEX = "selected_index";
private static final String STATE_IS_SWITCHING = "is_switching";
private boolean mHasPsim;
private boolean mNoPsimContinueToSettings;
private ArrayList<SubscriptionInfo> mEmbeddedSubscriptions = new ArrayList<>();
private SubscriptionInfo mRemovableSubscription = null;
private ItemGroup mItemGroup;
private SwitchToEuiccSubscriptionSidecar mSwitchToEuiccSubscriptionSidecar;
private SwitchToRemovableSlotSidecar mSwitchToRemovableSlotSidecar;
// Variables have states.
private int mSelectedItemIndex;
private boolean mIsSwitching;
/** Returns an intent of {@code ChooseSimActivity} */
public static Intent getIntent(Context context) {
return new Intent(context, ChooseSimActivity.class);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.choose_sim_activity);
Intent intent = getIntent();
mHasPsim = intent.getBooleanExtra(KEY_HAS_PSIM, false);
mNoPsimContinueToSettings = intent.getBooleanExtra(KEY_NO_PSIM_CONTINUE_TO_SETTINGS, false);
updateSubscriptions();
if (mEmbeddedSubscriptions.size() == 0) {
Log.e(TAG, "Unable to find available eSIM subscriptions.");
finish();
return;
}
if (savedInstanceState != null) {
mSelectedItemIndex = savedInstanceState.getInt(STATE_SELECTED_INDEX);
mIsSwitching = savedInstanceState.getBoolean(STATE_IS_SWITCHING);
}
GlifLayout layout = findViewById(R.id.glif_layout);
TextView textView = findViewById(R.id.subtitle);
int subscriptionCount = mEmbeddedSubscriptions.size();
if (mHasPsim) { // Choose a number to use
subscriptionCount++;
}
layout.setHeaderText(getString(R.string.choose_sim_title));
textView.setText(getString(R.string.choose_sim_text, subscriptionCount));
displaySubscriptions();
mSwitchToRemovableSlotSidecar = SwitchToRemovableSlotSidecar.get(getFragmentManager());
mSwitchToEuiccSubscriptionSidecar =
SwitchToEuiccSubscriptionSidecar.get(getFragmentManager());
}
@Override
public void onResume() {
super.onResume();
mSwitchToRemovableSlotSidecar.addListener(this);
mSwitchToEuiccSubscriptionSidecar.addListener(this);
}
@Override
public void onPause() {
mSwitchToEuiccSubscriptionSidecar.removeListener(this);
mSwitchToRemovableSlotSidecar.removeListener(this);
super.onPause();
}
@Override
protected void onSaveInstanceState(Bundle outState) {
outState.putInt(STATE_SELECTED_INDEX, mSelectedItemIndex);
outState.putBoolean(STATE_IS_SWITCHING, mIsSwitching);
super.onSaveInstanceState(outState);
}
@Override
public void onItemSelected(IItem item) {
if (mIsSwitching) {
// If we already selected an item, do not try to switch to another one.
return;
}
mIsSwitching = true;
Item subItem = (Item) item;
subItem.setSummary(getString(R.string.choose_sim_activating));
mSelectedItemIndex = subItem.getId();
if (mSelectedItemIndex == INDEX_PSIM) {
Log.i(TAG, "Ready to switch to pSIM slot.");
mSwitchToRemovableSlotSidecar.run(UiccSlotUtil.INVALID_PHYSICAL_SLOT_ID);
} else {
Log.i(TAG, "Ready to switch to eSIM subscription with index: " + mSelectedItemIndex);
mSwitchToEuiccSubscriptionSidecar.run(
mEmbeddedSubscriptions.get(mSelectedItemIndex).getSubscriptionId());
}
}
@Override
public void onStateChange(SidecarFragment fragment) {
if (fragment == mSwitchToRemovableSlotSidecar) {
switch (mSwitchToRemovableSlotSidecar.getState()) {
case SidecarFragment.State.SUCCESS:
mSwitchToRemovableSlotSidecar.reset();
Log.i(TAG, "Switch slot successfully.");
SubscriptionManager subMgr = getSystemService(SubscriptionManager.class);
if (subMgr.canDisablePhysicalSubscription()) {
SubscriptionInfo removableSub =
SubscriptionUtil.getFirstRemovableSubscription(this);
if (removableSub != null) {
subMgr.setUiccApplicationsEnabled(
removableSub.getSubscriptionId(), true);
}
}
finish();
break;
case SidecarFragment.State.ERROR:
mSwitchToRemovableSlotSidecar.reset();
Log.e(TAG, "Failed to switch slot in ChooseSubscriptionsActivity.");
handleEnableRemovableSimError();
// We don't call finish() and just stay on this page.
break;
}
} else if (fragment == mSwitchToEuiccSubscriptionSidecar) {
switch (mSwitchToEuiccSubscriptionSidecar.getState()) {
case SidecarFragment.State.SUCCESS:
mSwitchToEuiccSubscriptionSidecar.reset();
if (mNoPsimContinueToSettings) {
// Currently, there shouldn't be a case that mNoPsimContinueToSettings is
// true. If this can be true in the future, we should finish() this page
// and direct to Settings page here.
Log.e(
TAG,
"mNoPsimContinueToSettings is true which is not supported for"
+ " now.");
} else {
Log.i(TAG, "User finished selecting eSIM profile.");
finish();
}
break;
case SidecarFragment.State.ERROR:
mSwitchToEuiccSubscriptionSidecar.reset();
Log.e(TAG, "Failed to switch subscription in ChooseSubscriptionsActivity.");
Item item = (Item) mItemGroup.getItemAt(mSelectedItemIndex);
item.setEnabled(false);
item.setSummary(getString(R.string.choose_sim_could_not_activate));
mIsSwitching = false;
// We don't call finish() and just stay on this page.
break;
}
}
}
private void displaySubscriptions() {
View rootView = findViewById(android.R.id.content);
GlifRecyclerLayout layout = rootView.findViewById(R.id.recycler_list);
RecyclerItemAdapter adapter = (RecyclerItemAdapter) layout.getAdapter();
adapter.setOnItemSelectedListener(this);
mItemGroup = (ItemGroup) adapter.getRootItemHierarchy();
// Display pSIM profile.
if (mHasPsim) {
Item item = new DisableableItem();
// Title
CharSequence title = null;
if (mRemovableSubscription != null) {
title =
SubscriptionUtil.getUniqueSubscriptionDisplayName(
mRemovableSubscription.getSubscriptionId(), this);
}
item.setTitle(TextUtils.isEmpty(title) ? getString(R.string.sim_card_label) : title);
if (mIsSwitching && mSelectedItemIndex == INDEX_PSIM) {
item.setSummary(getString(R.string.choose_sim_activating));
} else {
// Phone number
String phoneNumber =
SubscriptionUtil.getFormattedPhoneNumber(this, mRemovableSubscription);
item.setSummary(TextUtils.isEmpty(phoneNumber) ? "" : phoneNumber);
}
// pSIM profile has index -1.
item.setId(INDEX_PSIM);
mItemGroup.addChild(item);
}
// Display all eSIM profiles.
int index = 0;
for (SubscriptionInfo sub : mEmbeddedSubscriptions) {
Item item = new DisableableItem();
CharSequence title =
SubscriptionUtil.getUniqueSubscriptionDisplayName(
sub.getSubscriptionId(), this);
item.setTitle(TextUtils.isEmpty(title) ? sub.getDisplayName() : title);
if (mIsSwitching && mSelectedItemIndex == index) {
item.setSummary(getString(R.string.choose_sim_activating));
} else {
String phoneNumber = SubscriptionUtil.getFormattedPhoneNumber(this, sub);
item.setSummary(TextUtils.isEmpty(phoneNumber) ? "" : phoneNumber);
}
item.setId(index++);
mItemGroup.addChild(item);
}
// This removes the unused header artifact from GlifRecyclerLayout.
HeaderRecyclerView rv = (HeaderRecyclerView) layout.getRecyclerView();
rv.getHeader().setVisibility(View.GONE);
}
private void updateSubscriptions() {
List<SubscriptionInfo> subscriptions =
SubscriptionUtil.getSelectableSubscriptionInfoList(this);
if (subscriptions != null) {
for (SubscriptionInfo sub : subscriptions) {
if (sub == null) {
continue;
}
if (sub.isEmbedded()) {
mEmbeddedSubscriptions.add(sub);
} else {
mRemovableSubscription = sub;
}
}
}
}
private void handleEnableRemovableSimError() {
// mSelectedItemIndex will be -1 if pSIM is selected. Since pSIM is always be
// listed at index 0, we change the itemIndex to 0 if pSIM is selected.
int itemIndex = mSelectedItemIndex == INDEX_PSIM ? 0 : mSelectedItemIndex;
Item item = (Item) mItemGroup.getItemAt(itemIndex);
item.setEnabled(false);
item.setSummary(getString(R.string.choose_sim_could_not_activate));
mIsSwitching = false;
}
class DisableableItem extends Item implements Dividable {
@Override
public boolean isDividerAllowedAbove() {
return true;
}
@Override
public boolean isDividerAllowedBelow() {
return true;
}
@Override
public void onBindView(View view) {
super.onBindView(view);
TextView title = view.findViewById(R.id.sud_items_title);
TextView summary = view.findViewById(R.id.sud_items_summary);
title.setEnabled(isEnabled());
summary.setEnabled(isEnabled());
}
}
}

View File

@@ -0,0 +1,147 @@
/*
* Copyright (C) 2021 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.content.Intent;
import android.os.Bundle;
import android.telephony.TelephonyManager;
import android.util.Log;
import com.android.settings.R;
import com.android.settings.SidecarFragment;
import com.android.settings.network.EnableMultiSimSidecar;
import com.android.settings.network.telephony.ConfirmDialogFragment;
import com.android.settings.network.telephony.SubscriptionActionDialogActivity;
/** Activity to show the enabling DSDS dialog. */
public class DsdsDialogActivity extends SubscriptionActionDialogActivity
implements SidecarFragment.Listener, ConfirmDialogFragment.OnConfirmListener {
private static final String TAG = "DsdsDialogActivity";
// Dialog tags
private static final int DIALOG_TAG_ENABLE_DSDS_CONFIRMATION = 1;
private static final int DIALOG_TAG_ENABLE_DSDS_REBOOT_CONFIRMATION = 2;
// Number of SIMs for DSDS
private static final int NUM_OF_SIMS_FOR_DSDS = 2;
private EnableMultiSimSidecar mEnableMultiSimSidecar;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mEnableMultiSimSidecar = EnableMultiSimSidecar.get(getFragmentManager());
if (savedInstanceState == null) {
showEnableDsdsConfirmDialog();
}
}
@Override
protected void onResume() {
super.onResume();
mEnableMultiSimSidecar.addListener(this);
}
@Override
protected void onPause() {
mEnableMultiSimSidecar.removeListener(this);
super.onPause();
}
@Override
public void onStateChange(SidecarFragment fragment) {
if (fragment == mEnableMultiSimSidecar) {
switch (fragment.getState()) {
case SidecarFragment.State.SUCCESS:
mEnableMultiSimSidecar.reset();
Log.i(TAG, "Enabled DSDS successfully");
dismissProgressDialog();
finish();
break;
case SidecarFragment.State.ERROR:
mEnableMultiSimSidecar.reset();
Log.e(TAG, "Failed to enable DSDS");
dismissProgressDialog();
showErrorDialog(
getString(R.string.dsds_activation_failure_title),
getString(R.string.dsds_activation_failure_body_msg2));
break;
}
}
}
@Override
public void onConfirm(int tag, boolean confirmed) {
if (!confirmed) {
Log.i(TAG, "User cancel the dialog to enable DSDS.");
startChooseSimActivity();
return;
}
TelephonyManager telephonyManager = getSystemService(TelephonyManager.class);
switch (tag) {
case DIALOG_TAG_ENABLE_DSDS_CONFIRMATION:
if (telephonyManager.doesSwitchMultiSimConfigTriggerReboot()) {
Log.i(TAG, "Device does not support reboot free DSDS.");
showRebootConfirmDialog();
return;
}
Log.i(TAG, "Enabling DSDS without rebooting.");
showProgressDialog(
getString(R.string.sim_action_enabling_sim_without_carrier_name));
mEnableMultiSimSidecar.run(NUM_OF_SIMS_FOR_DSDS);
break;
case DIALOG_TAG_ENABLE_DSDS_REBOOT_CONFIRMATION:
Log.i(TAG, "User confirmed reboot to enable DSDS.");
SimActivationNotifier.setShowSimSettingsNotification(this, true);
telephonyManager.switchMultiSimConfig(NUM_OF_SIMS_FOR_DSDS);
break;
default:
Log.e(TAG, "Unrecognized confirmation dialog tag: " + tag);
break;
}
}
private void showEnableDsdsConfirmDialog() {
ConfirmDialogFragment.show(
this,
ConfirmDialogFragment.OnConfirmListener.class,
DIALOG_TAG_ENABLE_DSDS_CONFIRMATION,
getString(R.string.sim_action_enable_dsds_title),
getString(R.string.sim_action_enable_dsds_text),
getString(R.string.sim_action_continue),
getString(R.string.sim_action_no_thanks));
}
private void showRebootConfirmDialog() {
ConfirmDialogFragment.show(
this,
ConfirmDialogFragment.OnConfirmListener.class,
DIALOG_TAG_ENABLE_DSDS_REBOOT_CONFIRMATION,
getString(R.string.sim_action_restart_title),
getString(R.string.sim_action_enable_dsds_text),
getString(R.string.sim_action_reboot),
getString(R.string.cancel));
}
private void startChooseSimActivity() {
Intent intent = ChooseSimActivity.getIntent(this);
intent.putExtra(ChooseSimActivity.KEY_HAS_PSIM, true);
startActivity(intent);
finish();
}
}

View File

@@ -26,8 +26,10 @@ import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.telephony.CarrierConfigManager;
import android.telephony.SubscriptionInfo;
import android.telephony.SubscriptionManager;
import android.telephony.TelephonyManager;
import android.text.TextUtils;
import android.util.Log;
@@ -40,6 +42,8 @@ import com.android.settings.network.SubscriptionUtil;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import javax.annotation.Nullable;
/**
* This class manages the notification of SIM activation notification including creating and
* canceling the notifications.
@@ -48,21 +52,26 @@ public class SimActivationNotifier {
private static final String TAG = "SimActivationNotifier";
private static final String SIM_SETUP_CHANNEL_ID = "sim_setup";
private static final String SWITCH_SLOT_CHANNEL_ID = "carrier_switching";
private static final String SIM_PREFS = "sim_prefs";
private static final String KEY_SHOW_SIM_SETTINGS_NOTIFICATION =
"show_sim_settings_notification";
public static final int SIM_ACTIVATION_NOTIFICATION_ID = 1;
public static final int SWITCH_TO_REMOVABLE_SLOT_NOTIFICATION_ID = 2;
/** Notification types */
@Retention(RetentionPolicy.SOURCE)
@IntDef(
value = {
NotificationType.NETWORK_CONFIG,
NotificationType.SWITCH_TO_REMOVABLE_SLOT,
})
public @interface NotificationType {
// The notification to remind users to config network Settings.
int NETWORK_CONFIG = 1;
// The notification to notify users that the device is switched to the removable slot.
int SWITCH_TO_REMOVABLE_SLOT = 2;
}
private final Context mContext;
@@ -104,13 +113,7 @@ public class SimActivationNotifier {
/** Sends a push notification for the SIM activation. It should be called after DSDS reboot. */
public void sendNetworkConfigNotification() {
SubscriptionManager subscriptionManager =
mContext.getSystemService(SubscriptionManager.class);
SubscriptionInfo activeRemovableSub =
SubscriptionUtil.getActiveSubscriptions(subscriptionManager).stream()
.filter(sub -> !sub.isEmbedded())
.findFirst()
.orElse(null);
SubscriptionInfo activeRemovableSub = getActiveRemovableSub();
if (activeRemovableSub == null) {
Log.e(TAG, "No removable subscriptions found. Do not show notification.");
@@ -143,4 +146,65 @@ public class SimActivationNotifier {
.setAutoCancel(true);
mNotificationManager.notify(SIM_ACTIVATION_NOTIFICATION_ID, builder.build());
}
/** Sends a push notification for switching to the removable slot. */
public void sendSwitchedToRemovableSlotNotification() {
String carrierName = getActiveCarrierName();
Intent clickIntent = new Intent(mContext, Settings.MobileNetworkListActivity.class);
TaskStackBuilder stackBuilder =
TaskStackBuilder.create(mContext).addNextIntent(clickIntent);
PendingIntent contentIntent =
stackBuilder.getPendingIntent(
0 /* requestCode */, PendingIntent.FLAG_UPDATE_CURRENT);
String titleText =
TextUtils.isEmpty(carrierName)
? mContext.getString(
R.string.switch_to_removable_notification_no_carrier_name)
: mContext.getString(
R.string.switch_to_removable_notification, carrierName);
Notification.Builder builder =
new Notification.Builder(mContext, SWITCH_SLOT_CHANNEL_ID)
.setContentTitle(titleText)
.setContentText(
mContext.getString(R.string.network_changed_notification_text))
.setContentIntent(contentIntent)
.setSmallIcon(R.drawable.ic_sim_alert)
.setColor(
mContext.getResources()
.getColor(
R.color.homepage_generic_icon_background,
null /* theme */))
.setAutoCancel(true);
mNotificationManager.notify(SWITCH_TO_REMOVABLE_SLOT_NOTIFICATION_ID, builder.build());
}
@Nullable
private SubscriptionInfo getActiveRemovableSub() {
SubscriptionManager subscriptionManager =
mContext.getSystemService(SubscriptionManager.class);
return SubscriptionUtil.getActiveSubscriptions(subscriptionManager).stream()
.filter(sub -> !sub.isEmbedded())
.findFirst()
.orElse(null);
}
@Nullable
private String getActiveCarrierName() {
CarrierConfigManager configManager = mContext.getSystemService(CarrierConfigManager.class);
TelephonyManager telManager = mContext.getSystemService(TelephonyManager.class);
String telName = telManager.getSimOperatorName();
if (configManager != null && configManager.getConfig() != null) {
boolean override =
configManager
.getConfig()
.getBoolean(CarrierConfigManager.KEY_CARRIER_NAME_OVERRIDE_BOOL);
String configName =
configManager
.getConfig()
.getString(CarrierConfigManager.KEY_CARRIER_NAME_STRING);
return override || TextUtils.isEmpty(telName) ? configName : telName;
}
return telName;
}
}

View File

@@ -35,6 +35,7 @@ public class SimNotificationService extends JobService {
/**
* Schedules a service to send SIM push notifications.
*
* @param context
* @param notificationType indicates which SIM notification to send.
*/
@@ -67,6 +68,9 @@ public class SimNotificationService extends JobService {
SimActivationNotifier.setShowSimSettingsNotification(this, false);
new SimActivationNotifier(this).sendNetworkConfigNotification();
break;
case SimActivationNotifier.NotificationType.SWITCH_TO_REMOVABLE_SLOT:
new SimActivationNotifier(this).sendSwitchedToRemovableSlotNotification();
break;
default:
Log.e(TAG, "Invalid notification type: " + notificationType);
break;

View File

@@ -0,0 +1,119 @@
/*
* Copyright (C) 2021 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.os.Bundle;
import android.telephony.SubscriptionInfo;
import android.util.Log;
import com.android.settings.R;
import com.android.settings.SidecarFragment;
import com.android.settings.network.SwitchToEuiccSubscriptionSidecar;
import com.android.settings.network.telephony.AlertDialogFragment;
import com.android.settings.network.telephony.ConfirmDialogFragment;
import com.android.settings.network.telephony.SubscriptionActionDialogActivity;
/**
* Starts a confirm dialog asking the user to switch to the eSIM slot/subscription. The caller needs
* to pass in the current enabled eSIM subscription, which is also the subscription to switch to.
*/
public class SwitchToEsimConfirmDialogActivity extends SubscriptionActionDialogActivity
implements SidecarFragment.Listener, ConfirmDialogFragment.OnConfirmListener {
public static final String KEY_SUB_TO_ENABLE = "sub_to_enable";
private static final String TAG = "SwitchToEsimConfirmDialogActivity";
private static final int TAG_CONFIRM = 1;
private SubscriptionInfo mSubToEnabled = null;
private SwitchToEuiccSubscriptionSidecar mSwitchToEuiccSubscriptionSidecar;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mSubToEnabled = getIntent().getParcelableExtra(KEY_SUB_TO_ENABLE);
mSwitchToEuiccSubscriptionSidecar =
SwitchToEuiccSubscriptionSidecar.get(getFragmentManager());
if (mSubToEnabled == null) {
Log.e(TAG, "Cannot find SIM to enable.");
finish();
return;
}
if (savedInstanceState == null) {
ConfirmDialogFragment.show(
this,
ConfirmDialogFragment.OnConfirmListener.class,
TAG_CONFIRM,
getString(R.string.switch_sim_dialog_title, mSubToEnabled.getDisplayName()),
getString(R.string.switch_sim_dialog_text, mSubToEnabled.getDisplayName()),
getString(R.string.okay),
getString(R.string.cancel));
}
}
@Override
public void onResume() {
super.onResume();
mSwitchToEuiccSubscriptionSidecar.addListener(this);
}
@Override
public void onPause() {
mSwitchToEuiccSubscriptionSidecar.removeListener(this);
super.onPause();
}
@Override
public void onStateChange(SidecarFragment fragment) {
if (fragment == mSwitchToEuiccSubscriptionSidecar) {
switch (mSwitchToEuiccSubscriptionSidecar.getState()) {
case SidecarFragment.State.SUCCESS:
mSwitchToEuiccSubscriptionSidecar.reset();
Log.i(TAG, "Successfully switched to eSIM slot.");
dismissProgressDialog();
finish();
break;
case SidecarFragment.State.ERROR:
mSwitchToEuiccSubscriptionSidecar.reset();
Log.e(TAG, "Failed switching to eSIM slot.");
dismissProgressDialog();
finish();
break;
}
}
}
@Override
public void onConfirm(int tag, boolean confirmed) {
if (!confirmed) {
AlertDialogFragment.show(
this,
getString(R.string.switch_sim_dialog_no_switch_title),
getString(R.string.switch_sim_dialog_no_switch_text));
return;
}
Log.i(TAG, "User confirmed to switch to embedded slot.");
mSwitchToEuiccSubscriptionSidecar.run(mSubToEnabled.getSubscriptionId());
showProgressDialog(
getString(
R.string.sim_action_switch_sub_dialog_progress,
mSubToEnabled.getDisplayName()));
}
}

View File

@@ -19,6 +19,7 @@ package com.android.settings.sim.receivers;
import static android.content.Context.MODE_PRIVATE;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Looper;
import android.provider.Settings;
@@ -29,6 +30,13 @@ import android.telephony.UiccSlotInfo;
import android.util.Log;
import com.android.settings.network.SubscriptionUtil;
import com.android.settings.network.UiccSlotUtil;
import com.android.settings.network.UiccSlotsException;
import com.android.settings.sim.ChooseSimActivity;
import com.android.settings.sim.DsdsDialogActivity;
import com.android.settings.sim.SimActivationNotifier;
import com.android.settings.sim.SimNotificationService;
import com.android.settings.sim.SwitchToEsimConfirmDialogActivity;
import com.google.common.collect.ImmutableList;
@@ -121,14 +129,13 @@ public class SimSlotChangeHandler {
return;
}
if (!hasActiveEsimSubscription()) {
if (mTelMgr.isMultiSimEnabled()) {
if (hasActiveEsimSubscription()) {
if (mTelMgr.isMultiSimSupported() == TelephonyManager.MULTISIM_ALLOWED) {
Log.i(TAG, "Enabled profile exists. DSDS condition satisfied.");
// TODO(b/170508680): Display DSDS dialog to ask users whether to enable DSDS.
startDsdsDialogActivity();
} else {
Log.i(TAG, "Enabled profile exists. DSDS condition not satisfied.");
// TODO(b/170508680): Display Choose a number to use screen for subscription
// selection.
startChooseSimActivity(true);
}
return;
}
@@ -137,7 +144,15 @@ public class SimSlotChangeHandler {
TAG,
"No enabled eSIM profile. Ready to switch to removable slot and show"
+ " notification.");
// TODO(b/170508680): Switch the slot to the removebale slot and show the notification.
try {
UiccSlotUtil.switchToRemovableSlot(
UiccSlotUtil.INVALID_PHYSICAL_SLOT_ID, mContext.getApplicationContext());
} catch (UiccSlotsException e) {
Log.e(TAG, "Failed to switch to removable slot.");
return;
}
SimNotificationService.scheduleSimNotification(
mContext, SimActivationNotifier.NotificationType.SWITCH_TO_REMOVABLE_SLOT);
}
private void handleSimRemove(UiccSlotInfo removableSlotInfo) {
@@ -160,14 +175,14 @@ public class SimSlotChangeHandler {
// profile.
if (groupedEmbeddedSubscriptions.size() == 1) {
Log.i(TAG, "Only 1 eSIM profile found. Ask user's consent to switch.");
// TODO(b/170508680): Display a dialog to ask users to switch.
startSwitchSlotConfirmDialogActivity(groupedEmbeddedSubscriptions.get(0));
return;
}
// If there are more than 1 eSIM profiles installed, we show a screen to let users to choose
// the number they want to use.
Log.i(TAG, "Multiple eSIM profiles found. Ask user which subscription to use.");
// TODO(b/170508680): Display a dialog to ask user which SIM to switch.
startChooseSimActivity(false);
}
private int getLastRemovableSimSlotState(Context context) {
@@ -225,5 +240,25 @@ public class SimSlotChangeHandler {
.collect(Collectors.toList()));
}
private void startChooseSimActivity(boolean psimInserted) {
Intent intent = ChooseSimActivity.getIntent(mContext);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.putExtra(ChooseSimActivity.KEY_HAS_PSIM, psimInserted);
mContext.startActivity(intent);
}
private void startSwitchSlotConfirmDialogActivity(SubscriptionInfo subscriptionInfo) {
Intent intent = new Intent(mContext, SwitchToEsimConfirmDialogActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.putExtra(SwitchToEsimConfirmDialogActivity.KEY_SUB_TO_ENABLE, subscriptionInfo);
mContext.startActivity(intent);
}
private void startDsdsDialogActivity() {
Intent intent = new Intent(mContext, DsdsDialogActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
mContext.startActivity(intent);
}
private SimSlotChangeHandler() {}
}

View File

@@ -48,14 +48,16 @@ public class SimSlotChangeReceiver extends BroadcastReceiver {
return;
}
final PendingResult pendingResult = goAsync();
ThreadUtils.postOnBackgroundThread(
() -> {
synchronized (mLock) {
if (!shouldHandleSlotChange(context)) {
return;
}
mSlotChangeHandler.onSlotsStatusChange(context);
mSlotChangeHandler.onSlotsStatusChange(context.getApplicationContext());
}
ThreadUtils.postOnMainThread(pendingResult::finish);
});
}