blob: 94d6d5f383eee43264e72694f23fe5e0aaa139fb [file] [log] [blame]
/*
* Copyright (C) 2014 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.internal.telephony;
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
import static android.telephony.TelephonyManager.MULTISIM_ALLOWED;
import static android.telephony.TelephonyManager.SET_OPPORTUNISTIC_SUB_REMOTE_SERVICE_EXCEPTION;
import static android.telephony.UiccSlotInfo.CARD_STATE_INFO_PRESENT;
import android.Manifest;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.AppOpsManager;
import android.app.PendingIntent;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.Binder;
import android.os.Handler;
import android.os.ParcelUuid;
import android.os.PersistableBundle;
import android.os.RegistrantList;
import android.os.RemoteException;
import android.os.TelephonyServiceManager.ServiceRegisterer;
import android.os.UserHandle;
import android.provider.Settings;
import android.telecom.PhoneAccountHandle;
import android.telecom.TelecomManager;
import android.telephony.AnomalyReporter;
import android.telephony.CarrierConfigManager;
import android.telephony.RadioAccessFamily;
import android.telephony.SubscriptionInfo;
import android.telephony.SubscriptionManager;
import android.telephony.SubscriptionManager.SimDisplayNameSource;
import android.telephony.TelephonyFrameworkInitializer;
import android.telephony.TelephonyManager;
import android.telephony.TelephonyRegistryManager;
import android.telephony.UiccAccessRule;
import android.telephony.UiccSlotInfo;
import android.telephony.euicc.EuiccManager;
import android.text.TextUtils;
import android.util.LocalLog;
import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.telephony.IccCardConstants.State;
import com.android.internal.telephony.dataconnection.DataEnabledOverride;
import com.android.internal.telephony.metrics.TelephonyMetrics;
import com.android.internal.telephony.uicc.IccUtils;
import com.android.internal.telephony.uicc.UiccCard;
import com.android.internal.telephony.uicc.UiccController;
import com.android.internal.telephony.uicc.UiccProfile;
import com.android.internal.telephony.uicc.UiccSlot;
import com.android.internal.telephony.util.ArrayUtils;
import com.android.internal.telephony.util.TelephonyUtils;
import com.android.telephony.Rlog;
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Collectors;
/**
* Implementation of the ISub interface.
*
* Any setters which take subId, slotIndex or phoneId as a parameter will throw an exception if the
* parameter equals the corresponding INVALID_XXX_ID or DEFAULT_XXX_ID.
*
* All getters will lookup the corresponding default if the parameter is DEFAULT_XXX_ID. Ie calling
* getPhoneId(DEFAULT_SUB_ID) will return the same as getPhoneId(getDefaultSubId()).
*
* Finally, any getters which perform the mapping between subscriptions, slots and phones will
* return the corresponding INVALID_XXX_ID if the parameter is INVALID_XXX_ID. All other getters
* will fail and return the appropriate error value. Ie calling
* getSlotIndex(INVALID_SUBSCRIPTION_ID) will return INVALID_SIM_SLOT_INDEX and calling
* getSubInfoForSubscriber(INVALID_SUBSCRIPTION_ID) will return null.
*
*/
public class SubscriptionController extends ISub.Stub {
private static final String LOG_TAG = "SubscriptionController";
private static final boolean DBG = true;
private static final boolean VDBG = Rlog.isLoggable(LOG_TAG, Log.VERBOSE);
private static final boolean DBG_CACHE = false;
private static final int DEPRECATED_SETTING = -1;
private static final ParcelUuid INVALID_GROUP_UUID =
ParcelUuid.fromString(CarrierConfigManager.REMOVE_GROUP_UUID_STRING);
private final LocalLog mLocalLog = new LocalLog(200);
private static final int SUB_ID_FOUND = 1;
private static final int NO_ENTRY_FOR_SLOT_INDEX = -1;
private static final int SUB_ID_NOT_IN_SLOT = -2;
// Lock that both mCacheActiveSubInfoList and mCacheOpportunisticSubInfoList use.
private Object mSubInfoListLock = new Object();
/* The Cache of Active SubInfoRecord(s) list of currently in use SubInfoRecord(s) */
private final List<SubscriptionInfo> mCacheActiveSubInfoList = new ArrayList<>();
/* Similar to mCacheActiveSubInfoList but only caching opportunistic subscriptions. */
private List<SubscriptionInfo> mCacheOpportunisticSubInfoList = new ArrayList<>();
private AtomicBoolean mOpptSubInfoListChangedDirtyBit = new AtomicBoolean();
private static final Comparator<SubscriptionInfo> SUBSCRIPTION_INFO_COMPARATOR =
(arg0, arg1) -> {
// Primary sort key on SimSlotIndex
int flag = arg0.getSimSlotIndex() - arg1.getSimSlotIndex();
if (flag == 0) {
// Secondary sort on SubscriptionId
return arg0.getSubscriptionId() - arg1.getSubscriptionId();
}
return flag;
};
@UnsupportedAppUsage
protected final Object mLock = new Object();
/** The singleton instance. */
protected static SubscriptionController sInstance = null;
@UnsupportedAppUsage
protected Context mContext;
protected TelephonyManager mTelephonyManager;
protected UiccController mUiccController;
private AppOpsManager mAppOps;
// Allows test mocks to avoid SELinux failures on invalidate calls.
private static boolean sCachingEnabled = true;
// Each slot can have multiple subs.
private static class WatchedSlotIndexToSubIds {
private Map<Integer, ArrayList<Integer>> mSlotIndexToSubIds = new ConcurrentHashMap<>();
WatchedSlotIndexToSubIds() {
}
public void clear() {
mSlotIndexToSubIds.clear();
invalidateDefaultSubIdCaches();
invalidateSlotIndexCaches();
}
public Set<Entry<Integer, ArrayList<Integer>>> entrySet() {
return mSlotIndexToSubIds.entrySet();
}
// Force all updates to data structure through wrapper.
public ArrayList<Integer> getCopy(int slotIndex) {
ArrayList<Integer> subIdList = mSlotIndexToSubIds.get(slotIndex);
if (subIdList == null) {
return null;
}
return new ArrayList<Integer>(subIdList);
}
public void put(int slotIndex, ArrayList<Integer> value) {
mSlotIndexToSubIds.put(slotIndex, value);
invalidateDefaultSubIdCaches();
invalidateSlotIndexCaches();
}
public void remove(int slotIndex) {
mSlotIndexToSubIds.remove(slotIndex);
invalidateDefaultSubIdCaches();
invalidateSlotIndexCaches();
}
public int size() {
return mSlotIndexToSubIds.size();
}
@VisibleForTesting
public Map<Integer, ArrayList<Integer>> getMap() {
return mSlotIndexToSubIds;
}
public int removeFromSubIdList(int slotIndex, int subId) {
ArrayList<Integer> subIdList = mSlotIndexToSubIds.get(slotIndex);
if (subIdList == null) {
return NO_ENTRY_FOR_SLOT_INDEX;
} else {
if (subIdList.contains(subId)) {
subIdList.remove(new Integer(subId));
if (subIdList.isEmpty()) {
mSlotIndexToSubIds.remove(slotIndex);
}
invalidateDefaultSubIdCaches();
invalidateSlotIndexCaches();
return SUB_ID_FOUND;
} else {
return SUB_ID_NOT_IN_SLOT;
}
}
}
public void addToSubIdList(int slotIndex, Integer value) {
ArrayList<Integer> subIdList = mSlotIndexToSubIds.get(slotIndex);
if (subIdList == null) {
subIdList = new ArrayList<Integer>();
subIdList.add(value);
mSlotIndexToSubIds.put(slotIndex, subIdList);
} else {
subIdList.add(value);
}
invalidateDefaultSubIdCaches();
invalidateSlotIndexCaches();
}
public void clearSubIdList(int slotIndex) {
ArrayList<Integer> subIdList = mSlotIndexToSubIds.get(slotIndex);
if (subIdList != null) {
subIdList.clear();
invalidateDefaultSubIdCaches();
invalidateSlotIndexCaches();
}
}
}
public static class WatchedInt {
private int mValue;
public WatchedInt(int initialValue) {
mValue = initialValue;
}
public int get() {
return mValue;
}
public void set(int newValue) {
mValue = newValue;
}
}
private static WatchedSlotIndexToSubIds sSlotIndexToSubIds = new WatchedSlotIndexToSubIds();
protected static WatchedInt sDefaultFallbackSubId =
new WatchedInt(SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
@Override
public void set(int newValue) {
super.set(newValue);
invalidateDefaultSubIdCaches();
invalidateSlotIndexCaches();
}
};
@UnsupportedAppUsage
private static int mDefaultPhoneId = SubscriptionManager.DEFAULT_PHONE_INDEX;
@UnsupportedAppUsage
private int[] colorArr;
private long mLastISubServiceRegTime;
private RegistrantList mUiccAppsEnableChangeRegList = new RegistrantList();
// The properties that should be shared and synced across grouped subscriptions.
private static final Set<String> GROUP_SHARING_PROPERTIES = new HashSet<>(Arrays.asList(
SubscriptionManager.ENHANCED_4G_MODE_ENABLED,
SubscriptionManager.VT_IMS_ENABLED,
SubscriptionManager.WFC_IMS_ENABLED,
SubscriptionManager.WFC_IMS_MODE,
SubscriptionManager.WFC_IMS_ROAMING_MODE,
SubscriptionManager.WFC_IMS_ROAMING_ENABLED,
SubscriptionManager.DATA_ROAMING,
SubscriptionManager.DISPLAY_NAME,
SubscriptionManager.DATA_ENABLED_OVERRIDE_RULES,
SubscriptionManager.UICC_APPLICATIONS_ENABLED,
SubscriptionManager.IMS_RCS_UCE_ENABLED));
public static SubscriptionController init(Context c) {
synchronized (SubscriptionController.class) {
if (sInstance == null) {
sInstance = new SubscriptionController(c);
} else {
Log.wtf(LOG_TAG, "init() called multiple times! sInstance = " + sInstance);
}
return sInstance;
}
}
@UnsupportedAppUsage
public static SubscriptionController getInstance() {
if (sInstance == null) {
Log.wtf(LOG_TAG, "getInstance null");
}
return sInstance;
}
protected SubscriptionController(Context c) {
internalInit(c);
migrateImsSettings();
}
protected void internalInit(Context c) {
mContext = c;
mTelephonyManager = TelephonyManager.from(mContext);
try {
mUiccController = UiccController.getInstance();
} catch(RuntimeException ex) {
throw new RuntimeException(
"UiccController has to be initialised before SubscriptionController init");
}
mAppOps = (AppOpsManager)mContext.getSystemService(Context.APP_OPS_SERVICE);
ServiceRegisterer subscriptionServiceRegisterer = TelephonyFrameworkInitializer
.getTelephonyServiceManager()
.getSubscriptionServiceRegisterer();
if (subscriptionServiceRegisterer.get() == null) {
subscriptionServiceRegisterer.register(this);
mLastISubServiceRegTime = System.currentTimeMillis();
}
// clear SLOT_INDEX for all subs
clearSlotIndexForSubInfoRecords();
// Cache Setting values
cacheSettingValues();
// Initial invalidate activates caching.
invalidateDefaultSubIdCaches();
invalidateDefaultDataSubIdCaches();
invalidateDefaultSmsSubIdCaches();
invalidateActiveDataSubIdCaches();
invalidateSlotIndexCaches();
if (DBG) logdl("[SubscriptionController] init by Context");
}
/**
* Should only be triggered once.
*/
public void notifySubInfoReady() {
// broadcast default subId.
sendDefaultChangedBroadcast(SubscriptionManager.getDefaultSubscriptionId());
}
@UnsupportedAppUsage
private boolean isSubInfoReady() {
return SubscriptionInfoUpdater.isSubInfoInitialized();
}
/**
* This function marks SIM_SLOT_INDEX as INVALID for all subscriptions in the database. This
* should be done as part of initialization.
*
* TODO: SIM_SLOT_INDEX is based on current state and should not even be persisted in the
* database.
*/
private void clearSlotIndexForSubInfoRecords() {
if (mContext == null) {
logel("[clearSlotIndexForSubInfoRecords] TelephonyManager or mContext is null");
return;
}
// Update all subscriptions in simInfo db with invalid slot index
ContentValues value = new ContentValues(1);
value.put(SubscriptionManager.SIM_SLOT_INDEX, SubscriptionManager.INVALID_SIM_SLOT_INDEX);
mContext.getContentResolver().update(SubscriptionManager.CONTENT_URI, value, null, null);
}
/**
* Cache the Settings values by reading these values from Setting from disk to prevent disk I/O
* access during the API calling. This is based on an assumption that the Settings system will
* itself cache this value after the first read and thus only the first read after boot will
* access the disk.
*/
private void cacheSettingValues() {
Settings.Global.getInt(mContext.getContentResolver(),
Settings.Global.MULTI_SIM_SMS_SUBSCRIPTION,
SubscriptionManager.INVALID_SUBSCRIPTION_ID);
Settings.Global.getInt(mContext.getContentResolver(),
Settings.Global.MULTI_SIM_VOICE_CALL_SUBSCRIPTION,
SubscriptionManager.INVALID_SUBSCRIPTION_ID);
Settings.Global.getInt(mContext.getContentResolver(),
Settings.Global.MULTI_SIM_DATA_CALL_SUBSCRIPTION,
SubscriptionManager.INVALID_SUBSCRIPTION_ID);
}
@UnsupportedAppUsage
protected void enforceModifyPhoneState(String message) {
mContext.enforceCallingOrSelfPermission(
android.Manifest.permission.MODIFY_PHONE_STATE, message);
}
private void enforceReadPrivilegedPhoneState(String message) {
mContext.enforceCallingOrSelfPermission(
Manifest.permission.READ_PRIVILEGED_PHONE_STATE, message);
}
/**
* Returns whether the {@code callingPackage} has access to subscriber identifiers on the
* specified {@code subId} using the provided {@code message} in any resulting
* SecurityException.
*/
private boolean hasSubscriberIdentifierAccess(int subId, String callingPackage,
String callingFeatureId, String message) {
try {
return TelephonyPermissions.checkCallingOrSelfReadSubscriberIdentifiers(mContext, subId,
callingPackage, callingFeatureId, message);
} catch (SecurityException e) {
// A SecurityException indicates that the calling package is targeting at least the
// minimum level that enforces identifier access restrictions and the new access
// requirements are not met.
return false;
}
}
/**
* Returns whether the {@code callingPackage} has access to the phone number on the specified
* {@code subId} using the provided {@code message} in any resulting SecurityException.
*/
private boolean hasPhoneNumberAccess(int subId, String callingPackage, String callingFeatureId,
String message) {
try {
return TelephonyPermissions.checkCallingOrSelfReadPhoneNumber(mContext, subId,
callingPackage, callingFeatureId, message);
} catch (SecurityException e) {
return false;
}
}
/**
* Broadcast when SubscriptionInfo has changed
* FIXME: Hopefully removed if the API council accepts SubscriptionInfoListener
*/
private void broadcastSimInfoContentChanged() {
Intent intent = new Intent(TelephonyIntents.ACTION_SUBINFO_CONTENT_CHANGE);
mContext.sendBroadcast(intent);
intent = new Intent(TelephonyIntents.ACTION_SUBINFO_RECORD_UPDATED);
mContext.sendBroadcast(intent);
}
/**
* Notify the changed of subscription info.
*/
@UnsupportedAppUsage
public void notifySubscriptionInfoChanged() {
TelephonyRegistryManager trm =
(TelephonyRegistryManager)
mContext.getSystemService(Context.TELEPHONY_REGISTRY_SERVICE);
if (DBG) logd("notifySubscriptionInfoChanged:");
trm.notifySubscriptionInfoChanged();
// FIXME: Remove if listener technique accepted.
broadcastSimInfoContentChanged();
MultiSimSettingController.getInstance().notifySubscriptionInfoChanged();
TelephonyMetrics metrics = TelephonyMetrics.getInstance();
List<SubscriptionInfo> subInfos;
synchronized (mSubInfoListLock) {
subInfos = new ArrayList<>(mCacheActiveSubInfoList);
}
if (mOpptSubInfoListChangedDirtyBit.getAndSet(false)) {
notifyOpportunisticSubscriptionInfoChanged();
}
metrics.updateActiveSubscriptionInfoList(subInfos);
}
/**
* New SubInfoRecord instance and fill in detail info
* @param cursor
* @return the query result of desired SubInfoRecord
*/
@UnsupportedAppUsage
private SubscriptionInfo getSubInfoRecord(Cursor cursor) {
int id = cursor.getInt(cursor.getColumnIndexOrThrow(
SubscriptionManager.UNIQUE_KEY_SUBSCRIPTION_ID));
String iccId = cursor.getString(cursor.getColumnIndexOrThrow(
SubscriptionManager.ICC_ID));
int simSlotIndex = cursor.getInt(cursor.getColumnIndexOrThrow(
SubscriptionManager.SIM_SLOT_INDEX));
String displayName = cursor.getString(cursor.getColumnIndexOrThrow(
SubscriptionManager.DISPLAY_NAME));
String carrierName = cursor.getString(cursor.getColumnIndexOrThrow(
SubscriptionManager.CARRIER_NAME));
int nameSource = cursor.getInt(cursor.getColumnIndexOrThrow(
SubscriptionManager.NAME_SOURCE));
int iconTint = cursor.getInt(cursor.getColumnIndexOrThrow(
SubscriptionManager.HUE));
String number = cursor.getString(cursor.getColumnIndexOrThrow(
SubscriptionManager.NUMBER));
int dataRoaming = cursor.getInt(cursor.getColumnIndexOrThrow(
SubscriptionManager.DATA_ROAMING));
// Get the blank bitmap for this SubInfoRecord
Bitmap iconBitmap = BitmapFactory.decodeResource(mContext.getResources(),
com.android.internal.R.drawable.ic_sim_card_multi_24px_clr);
String mcc = cursor.getString(cursor.getColumnIndexOrThrow(
SubscriptionManager.MCC_STRING));
String mnc = cursor.getString(cursor.getColumnIndexOrThrow(
SubscriptionManager.MNC_STRING));
String ehplmnsRaw = cursor.getString(cursor.getColumnIndexOrThrow(
SubscriptionManager.EHPLMNS));
String hplmnsRaw = cursor.getString(cursor.getColumnIndexOrThrow(
SubscriptionManager.HPLMNS));
String[] ehplmns = ehplmnsRaw == null ? null : ehplmnsRaw.split(",");
String[] hplmns = hplmnsRaw == null ? null : hplmnsRaw.split(",");
// cardId is the private ICCID/EID string, also known as the card string
String cardId = cursor.getString(cursor.getColumnIndexOrThrow(
SubscriptionManager.CARD_ID));
String countryIso = cursor.getString(cursor.getColumnIndexOrThrow(
SubscriptionManager.ISO_COUNTRY_CODE));
// publicCardId is the publicly exposed int card ID
int publicCardId = mUiccController.convertToPublicCardId(cardId);
boolean isEmbedded = cursor.getInt(cursor.getColumnIndexOrThrow(
SubscriptionManager.IS_EMBEDDED)) == 1;
int carrierId = cursor.getInt(cursor.getColumnIndexOrThrow(
SubscriptionManager.CARRIER_ID));
UiccAccessRule[] accessRules;
if (isEmbedded) {
accessRules = UiccAccessRule.decodeRules(cursor.getBlob(
cursor.getColumnIndexOrThrow(SubscriptionManager.ACCESS_RULES)));
} else {
accessRules = null;
}
UiccAccessRule[] carrierConfigAccessRules = UiccAccessRule.decodeRules(cursor.getBlob(
cursor.getColumnIndexOrThrow(SubscriptionManager.ACCESS_RULES_FROM_CARRIER_CONFIGS)));
boolean isOpportunistic = cursor.getInt(cursor.getColumnIndexOrThrow(
SubscriptionManager.IS_OPPORTUNISTIC)) == 1;
String groupUUID = cursor.getString(cursor.getColumnIndexOrThrow(
SubscriptionManager.GROUP_UUID));
int profileClass = cursor.getInt(cursor.getColumnIndexOrThrow(
SubscriptionManager.PROFILE_CLASS));
int subType = cursor.getInt(cursor.getColumnIndexOrThrow(
SubscriptionManager.SUBSCRIPTION_TYPE));
String groupOwner = getOptionalStringFromCursor(cursor, SubscriptionManager.GROUP_OWNER,
/*defaultVal*/ null);
boolean areUiccApplicationsEnabled = cursor.getInt(cursor.getColumnIndexOrThrow(
SubscriptionManager.UICC_APPLICATIONS_ENABLED)) == 1;
if (VDBG) {
String iccIdToPrint = SubscriptionInfo.givePrintableIccid(iccId);
String cardIdToPrint = SubscriptionInfo.givePrintableIccid(cardId);
logd("[getSubInfoRecord] id:" + id + " iccid:" + iccIdToPrint + " simSlotIndex:"
+ simSlotIndex + " carrierid:" + carrierId + " displayName:" + displayName
+ " nameSource:" + nameSource + " iconTint:" + iconTint
+ " dataRoaming:" + dataRoaming + " mcc:" + mcc + " mnc:" + mnc
+ " countIso:" + countryIso + " isEmbedded:"
+ isEmbedded + " accessRules:" + Arrays.toString(accessRules)
+ " carrierConfigAccessRules: " + Arrays.toString(carrierConfigAccessRules)
+ " cardId:" + cardIdToPrint + " publicCardId:" + publicCardId
+ " isOpportunistic:" + isOpportunistic + " groupUUID:" + groupUUID
+ " profileClass:" + profileClass + " subscriptionType: " + subType
+ " carrierConfigAccessRules:" + carrierConfigAccessRules
+ " areUiccApplicationsEnabled: " + areUiccApplicationsEnabled);
}
// If line1number has been set to a different number, use it instead.
String line1Number = mTelephonyManager.getLine1Number(id);
if (!TextUtils.isEmpty(line1Number) && !line1Number.equals(number)) {
number = line1Number;
}
SubscriptionInfo info = new SubscriptionInfo(id, iccId, simSlotIndex, displayName,
carrierName, nameSource, iconTint, number, dataRoaming, iconBitmap, mcc, mnc,
countryIso, isEmbedded, accessRules, cardId, publicCardId, isOpportunistic,
groupUUID, false /* isGroupDisabled */, carrierId, profileClass, subType,
groupOwner, carrierConfigAccessRules, areUiccApplicationsEnabled);
info.setAssociatedPlmns(ehplmns, hplmns);
return info;
}
private String getOptionalStringFromCursor(Cursor cursor, String column, String defaultVal) {
// Return defaultVal if the column doesn't exist.
int columnIndex = cursor.getColumnIndex(column);
return (columnIndex == -1) ? defaultVal : cursor.getString(columnIndex);
}
/**
* Get a subscription that matches IccId.
* @return null if there isn't a match, or subscription info if there is one.
*/
public SubscriptionInfo getSubInfoForIccId(String iccId) {
List<SubscriptionInfo> info = getSubInfo(
SubscriptionManager.ICC_ID + "=\'" + iccId + "\'", null);
if (info == null || info.size() == 0) return null;
// Should be at most one subscription with the iccid.
return info.get(0);
}
/**
* Query SubInfoRecord(s) from subinfo database
* @param selection A filter declaring which rows to return
* @param queryKey query key content
* @return Array list of queried result from database
*/
@UnsupportedAppUsage
public List<SubscriptionInfo> getSubInfo(String selection, Object queryKey) {
if (VDBG) logd("selection:" + selection + ", querykey: " + queryKey);
String[] selectionArgs = null;
if (queryKey != null) {
selectionArgs = new String[] {queryKey.toString()};
}
ArrayList<SubscriptionInfo> subList = null;
Cursor cursor = mContext.getContentResolver().query(SubscriptionManager.CONTENT_URI,
null, selection, selectionArgs, null);
try {
if (cursor != null) {
while (cursor.moveToNext()) {
SubscriptionInfo subInfo = getSubInfoRecord(cursor);
if (subInfo != null) {
if (subList == null) {
subList = new ArrayList<SubscriptionInfo>();
}
subList.add(subInfo);
}
}
} else {
if (DBG) logd("Query fail");
}
} finally {
if (cursor != null) {
cursor.close();
}
}
return subList;
}
/**
* Find unused color to be set for new SubInfoRecord
* @param callingPackage The package making the IPC.
* @param callingFeatureId The feature in the package
* @return RGB integer value of color
*/
private int getUnusedColor(String callingPackage, String callingFeatureId) {
List<SubscriptionInfo> availableSubInfos = getActiveSubscriptionInfoList(callingPackage,
callingFeatureId);
colorArr = mContext.getResources().getIntArray(com.android.internal.R.array.sim_colors);
int colorIdx = 0;
if (availableSubInfos != null) {
for (int i = 0; i < colorArr.length; i++) {
int j;
for (j = 0; j < availableSubInfos.size(); j++) {
if (colorArr[i] == availableSubInfos.get(j).getIconTint()) {
break;
}
}
if (j == availableSubInfos.size()) {
return colorArr[i];
}
}
colorIdx = availableSubInfos.size() % colorArr.length;
}
return colorArr[colorIdx];
}
@Deprecated
@UnsupportedAppUsage
public SubscriptionInfo getActiveSubscriptionInfo(int subId, String callingPackage) {
return getActiveSubscriptionInfo(subId, callingPackage, null);
}
/**
* Get the active SubscriptionInfo with the subId key
* @param subId The unique SubscriptionInfo key in database
* @param callingPackage The package making the IPC.
* @param callingFeatureId The feature in the package
* @return SubscriptionInfo, maybe null if its not active
*/
@Override
public SubscriptionInfo getActiveSubscriptionInfo(int subId, String callingPackage,
String callingFeatureId) {
if (!TelephonyPermissions.checkCallingOrSelfReadPhoneState(mContext, subId, callingPackage,
callingFeatureId, "getActiveSubscriptionInfo")) {
return null;
}
// Now that all security checks passes, perform the operation as ourselves.
final long identity = Binder.clearCallingIdentity();
List<SubscriptionInfo> subList;
try {
subList = getActiveSubscriptionInfoList(
mContext.getOpPackageName(), mContext.getAttributionTag());
} finally {
Binder.restoreCallingIdentity(identity);
}
if (subList != null) {
for (SubscriptionInfo si : subList) {
if (si.getSubscriptionId() == subId) {
if (VDBG) {
logd("[getActiveSubscriptionInfo]+ subId=" + subId + " subInfo=" + si);
}
return conditionallyRemoveIdentifiers(si, callingPackage, callingFeatureId,
"getActiveSubscriptionInfo");
}
}
}
if (DBG) {
logd("[getActiveSubscriptionInfo]- subId=" + subId
+ " subList=" + subList + " subInfo=null");
}
return null;
}
/**
* Get a single subscription info record for a given subscription.
*
* @param subId the subId to query.
*
* @hide
*/
public SubscriptionInfo getSubscriptionInfo(int subId) {
synchronized (mSubInfoListLock) {
// check cache for active subscriptions first, before querying db
for (SubscriptionInfo subInfo : mCacheActiveSubInfoList) {
if (subInfo.getSubscriptionId() == subId) {
return subInfo;
}
}
// check cache for opportunistic subscriptions too, before querying db
for (SubscriptionInfo subInfo : mCacheOpportunisticSubInfoList) {
if (subInfo.getSubscriptionId() == subId) {
return subInfo;
}
}
}
List<SubscriptionInfo> subInfoList = getSubInfo(
SubscriptionManager.UNIQUE_KEY_SUBSCRIPTION_ID + "=" + subId, null);
if (subInfoList == null || subInfoList.isEmpty()) return null;
return subInfoList.get(0);
}
/**
* Get the active SubscriptionInfo associated with the iccId
* @param iccId the IccId of SIM card
* @param callingPackage The package making the IPC.
* @param callingFeatureId The feature in the package
* @return SubscriptionInfo, maybe null if its not active
*/
@Override
public SubscriptionInfo getActiveSubscriptionInfoForIccId(String iccId, String callingPackage,
String callingFeatureId) {
enforceReadPrivilegedPhoneState("getActiveSubscriptionInfoForIccId");
return getActiveSubscriptionInfoForIccIdInternal(iccId);
}
/**
* Get the active SubscriptionInfo associated with the given iccId. The caller *must* perform
* permission checks when using this method.
*/
private SubscriptionInfo getActiveSubscriptionInfoForIccIdInternal(String iccId) {
if (iccId == null) {
return null;
}
final long identity = Binder.clearCallingIdentity();
try {
List<SubscriptionInfo> subList = getActiveSubscriptionInfoList(
mContext.getOpPackageName(), mContext.getAttributionTag());
if (subList != null) {
for (SubscriptionInfo si : subList) {
if (iccId.equals(si.getIccId())) {
if (DBG)
logd("[getActiveSubInfoUsingIccId]+ iccId=" + iccId + " subInfo=" + si);
return si;
}
}
}
if (DBG) {
logd("[getActiveSubInfoUsingIccId]+ iccId=" + iccId
+ " subList=" + subList + " subInfo=null");
}
} finally {
Binder.restoreCallingIdentity(identity);
}
return null;
}
/**
* Get the active SubscriptionInfo associated with the slotIndex.
* This API does not return details on Remote-SIM subscriptions.
* @param slotIndex the slot which the subscription is inserted
* @param callingPackage The package making the IPC.
* @param callingFeatureId The feature in the package
* @return SubscriptionInfo, null for Remote-SIMs or non-active slotIndex.
*/
@Override
public SubscriptionInfo getActiveSubscriptionInfoForSimSlotIndex(int slotIndex,
String callingPackage, String callingFeatureId) {
Phone phone = PhoneFactory.getPhone(slotIndex);
if (phone == null) {
if (DBG) {
loge("[getActiveSubscriptionInfoForSimSlotIndex] no phone, slotIndex=" + slotIndex);
}
return null;
}
if (!TelephonyPermissions.checkCallingOrSelfReadPhoneState(
mContext, phone.getSubId(), callingPackage, callingFeatureId,
"getActiveSubscriptionInfoForSimSlotIndex")) {
return null;
}
// Now that all security checks passes, perform the operation as ourselves.
final long identity = Binder.clearCallingIdentity();
List<SubscriptionInfo> subList;
try {
subList = getActiveSubscriptionInfoList(
mContext.getOpPackageName(), mContext.getAttributionTag());
} finally {
Binder.restoreCallingIdentity(identity);
}
if (subList != null) {
for (SubscriptionInfo si : subList) {
if (si.getSimSlotIndex() == slotIndex) {
if (DBG) {
logd("[getActiveSubscriptionInfoForSimSlotIndex]+ slotIndex="
+ slotIndex + " subId=" + si);
}
return conditionallyRemoveIdentifiers(si, callingPackage, callingFeatureId,
"getActiveSubscriptionInfoForSimSlotIndex");
}
}
if (DBG) {
logd("[getActiveSubscriptionInfoForSimSlotIndex]+ slotIndex=" + slotIndex
+ " subId=null");
}
} else {
if (DBG) {
logd("[getActiveSubscriptionInfoForSimSlotIndex]+ subList=null");
}
}
return null;
}
/**
* @param callingPackage The package making the IPC.
* @param callingFeatureId The feature in the package
* @return List of all SubscriptionInfo records in database,
* include those that were inserted before, maybe empty but not null.
* @hide
*/
@Override
public List<SubscriptionInfo> getAllSubInfoList(String callingPackage,
String callingFeatureId) {
if (VDBG) logd("[getAllSubInfoList]+");
// This API isn't public, so no need to provide a valid subscription ID - we're not worried
// about carrier-privileged callers not having access.
if (!TelephonyPermissions.checkCallingOrSelfReadPhoneState(
mContext, SubscriptionManager.INVALID_SUBSCRIPTION_ID, callingPackage,
callingFeatureId, "getAllSubInfoList")) {
return null;
}
// Now that all security checks passes, perform the operation as ourselves.
final long identity = Binder.clearCallingIdentity();
try {
List<SubscriptionInfo> subList = null;
subList = getSubInfo(null, null);
if (subList != null) {
if (VDBG) logd("[getAllSubInfoList]- " + subList.size() + " infos return");
subList.stream().map(
subscriptionInfo -> conditionallyRemoveIdentifiers(subscriptionInfo,
callingPackage, callingFeatureId, "getAllSubInfoList"))
.collect(Collectors.toList());
} else {
if (VDBG) logd("[getAllSubInfoList]- no info return");
}
return subList;
} finally {
Binder.restoreCallingIdentity(identity);
}
}
private List<SubscriptionInfo> makeCacheListCopyWithLock(List<SubscriptionInfo> cacheSubList) {
synchronized (mSubInfoListLock) {
return new ArrayList<>(cacheSubList);
}
}
@Deprecated
@UnsupportedAppUsage
public List<SubscriptionInfo> getActiveSubscriptionInfoList(String callingPackage) {
return getSubscriptionInfoListFromCacheHelper(callingPackage, null,
makeCacheListCopyWithLock(mCacheActiveSubInfoList));
}
/**
* Get the SubInfoRecord(s) of the currently active SIM(s) - which include both local
* and remote SIMs.
* @param callingPackage The package making the IPC.
* @param callingFeatureId The feature in the package
* @return Array list of currently inserted SubInfoRecord(s)
*/
@Override
public List<SubscriptionInfo> getActiveSubscriptionInfoList(String callingPackage,
String callingFeatureId) {
return getSubscriptionInfoListFromCacheHelper(callingPackage, callingFeatureId,
makeCacheListCopyWithLock(mCacheActiveSubInfoList));
}
/**
* Refresh the cache of SubInfoRecord(s) of the currently available SIM(s) - including
* local & remote SIMs.
*/
@VisibleForTesting // For mockito to mock this method
public void refreshCachedActiveSubscriptionInfoList() {
boolean opptSubListChanged;
List<SubscriptionInfo> activeSubscriptionInfoList = getSubInfo(
SubscriptionManager.SIM_SLOT_INDEX + ">=0 OR "
+ SubscriptionManager.SUBSCRIPTION_TYPE + "="
+ SubscriptionManager.SUBSCRIPTION_TYPE_REMOTE_SIM,
null);
synchronized (mSubInfoListLock) {
if (activeSubscriptionInfoList != null) {
// Log when active sub info changes.
if (mCacheActiveSubInfoList.size() != activeSubscriptionInfoList.size()
|| !mCacheActiveSubInfoList.containsAll(activeSubscriptionInfoList)) {
logdl("Active subscription info list changed. " + activeSubscriptionInfoList);
}
mCacheActiveSubInfoList.clear();
activeSubscriptionInfoList.sort(SUBSCRIPTION_INFO_COMPARATOR);
mCacheActiveSubInfoList.addAll(activeSubscriptionInfoList);
} else {
logd("activeSubscriptionInfoList is null.");
mCacheActiveSubInfoList.clear();
}
if (DBG_CACHE) {
if (!mCacheActiveSubInfoList.isEmpty()) {
for (SubscriptionInfo si : mCacheActiveSubInfoList) {
logd("[refreshCachedActiveSubscriptionInfoList] Setting Cached info="
+ si);
}
} else {
logdl("[refreshCachedActiveSubscriptionInfoList]- no info return");
}
}
}
// Refresh cached opportunistic sub list and detect whether it's changed.
refreshCachedOpportunisticSubscriptionInfoList();
}
@Deprecated
@UnsupportedAppUsage
public int getActiveSubInfoCount(String callingPackage) {
return getActiveSubInfoCount(callingPackage, null);
}
/**
* Get the SUB count of active SUB(s)
* @param callingPackage The package making the IPC.
* @param callingFeatureId The feature in the package.
* @return active SIM count
*/
@Override
public int getActiveSubInfoCount(String callingPackage, String callingFeatureId) {
// Let getActiveSubscriptionInfoList perform permission checks / filtering.
List<SubscriptionInfo> records = getActiveSubscriptionInfoList(callingPackage,
callingFeatureId);
if (records == null) {
if (VDBG) logd("[getActiveSubInfoCount] records null");
return 0;
}
if (VDBG) logd("[getActiveSubInfoCount]- count: " + records.size());
return records.size();
}
/**
* Get the SUB count of all SUB(s) in SubscriptoinInfo database
* @param callingPackage The package making the IPC.
* @param callingFeatureId The feature in the package
* @return all SIM count in database, include what was inserted before
*/
@Override
public int getAllSubInfoCount(String callingPackage, String callingFeatureId) {
if (DBG) logd("[getAllSubInfoCount]+");
// This API isn't public, so no need to provide a valid subscription ID - we're not worried
// about carrier-privileged callers not having access.
if (!TelephonyPermissions.checkCallingOrSelfReadPhoneState(
mContext, SubscriptionManager.INVALID_SUBSCRIPTION_ID, callingPackage,
callingFeatureId, "getAllSubInfoCount")) {
return 0;
}
// Now that all security checks passes, perform the operation as ourselves.
final long identity = Binder.clearCallingIdentity();
try {
Cursor cursor = mContext.getContentResolver().query(SubscriptionManager.CONTENT_URI,
null, null, null, null);
try {
if (cursor != null) {
int count = cursor.getCount();
if (DBG) logd("[getAllSubInfoCount]- " + count + " SUB(s) in DB");
return count;
}
} finally {
if (cursor != null) {
cursor.close();
}
}
if (DBG) logd("[getAllSubInfoCount]- no SUB in DB");
return 0;
} finally {
Binder.restoreCallingIdentity(identity);
}
}
/**
* @return the maximum number of local subscriptions this device will support at any one time.
*/
@Override
public int getActiveSubInfoCountMax() {
// FIXME: This valid now but change to use TelephonyDevController in the future
return mTelephonyManager.getSimCount();
}
@Override
public List<SubscriptionInfo> getAvailableSubscriptionInfoList(String callingPackage,
String callingFeatureId) {
// This API isn't public, so no need to provide a valid subscription ID - we're not worried
// about carrier-privileged callers not having access.
if (!TelephonyPermissions.checkCallingOrSelfReadPhoneState(
mContext, SubscriptionManager.INVALID_SUBSCRIPTION_ID, callingPackage,
callingFeatureId, "getAvailableSubscriptionInfoList")) {
throw new SecurityException("Need READ_PHONE_STATE to call "
+ " getAvailableSubscriptionInfoList");
}
// Now that all security checks pass, perform the operation as ourselves.
final long identity = Binder.clearCallingIdentity();
try {
String selection = SubscriptionManager.SIM_SLOT_INDEX + ">=0 OR "
+ SubscriptionManager.SUBSCRIPTION_TYPE + "="
+ SubscriptionManager.SUBSCRIPTION_TYPE_REMOTE_SIM;
EuiccManager euiccManager =
(EuiccManager) mContext.getSystemService(Context.EUICC_SERVICE);
if (euiccManager.isEnabled()) {
selection += " OR " + SubscriptionManager.IS_EMBEDDED + "=1";
}
// Available eSIM profiles are reported by EuiccManager. However for physical SIMs if
// they are in inactive slot or programmatically disabled, they are still considered
// available. In this case we get their iccid from slot info and include their
// subscriptionInfos.
List<String> iccIds = getIccIdsOfInsertedPhysicalSims();
if (!iccIds.isEmpty()) {
selection += " OR (" + getSelectionForIccIdList(iccIds.toArray(new String[0]))
+ ")";
}
List<SubscriptionInfo> subList = getSubInfo(selection, null /* queryKey */);
if (subList != null) {
subList.sort(SUBSCRIPTION_INFO_COMPARATOR);
if (VDBG) logdl("[getAvailableSubInfoList]- " + subList.size() + " infos return");
} else {
if (DBG) logdl("[getAvailableSubInfoList]- no info return");
}
return subList;
} finally {
Binder.restoreCallingIdentity(identity);
}
}
private List<String> getIccIdsOfInsertedPhysicalSims() {
List<String> ret = new ArrayList<>();
UiccSlot[] uiccSlots = UiccController.getInstance().getUiccSlots();
if (uiccSlots == null) return ret;
for (UiccSlot uiccSlot : uiccSlots) {
if (uiccSlot != null && uiccSlot.getCardState() != null
&& uiccSlot.getCardState().isCardPresent()
&& !uiccSlot.isEuicc()
&& !TextUtils.isEmpty(uiccSlot.getIccId())) {
ret.add(IccUtils.stripTrailingFs(uiccSlot.getIccId()));
}
}
return ret;
}
@Override
public List<SubscriptionInfo> getAccessibleSubscriptionInfoList(String callingPackage) {
EuiccManager euiccManager = (EuiccManager) mContext.getSystemService(Context.EUICC_SERVICE);
if (!euiccManager.isEnabled()) {
if (DBG) {
logdl("[getAccessibleSubInfoList] Embedded subscriptions are disabled");
}
return null;
}
// Verify that the given package belongs to the calling UID.
mAppOps.checkPackage(Binder.getCallingUid(), callingPackage);
// Perform the operation as ourselves. If the caller cannot read phone state, they may still
// have carrier privileges per the subscription metadata, so we always need to make the
// query and then filter the results.
final long identity = Binder.clearCallingIdentity();
List<SubscriptionInfo> subList;
try {
subList = getSubInfo(SubscriptionManager.IS_EMBEDDED + "=1", null);
} finally {
Binder.restoreCallingIdentity(identity);
}
if (subList == null) {
if (DBG) logdl("[getAccessibleSubInfoList] No info returned");
return null;
}
// Filter the list to only include subscriptions which the (restored) caller can manage.
List<SubscriptionInfo> filteredList = subList.stream()
.filter(subscriptionInfo ->
subscriptionInfo.canManageSubscription(mContext, callingPackage))
.sorted(SUBSCRIPTION_INFO_COMPARATOR)
.collect(Collectors.toList());
if (VDBG) {
logdl("[getAccessibleSubInfoList] " + filteredList.size() + " infos returned");
}
return filteredList;
}
/**
* Return the list of subscriptions in the database which are either:
* <ul>
* <li>Embedded (but see note about {@code includeNonRemovableSubscriptions}, or
* <li>In the given list of current embedded ICCIDs (which may not yet be in the database, or
* which may not currently be marked as embedded).
* </ul>
*
* <p>NOTE: This is not accessible to external processes, so it does not need a permission
* check. It is only intended for use by {@link SubscriptionInfoUpdater}.
*
* @param embeddedIccids all ICCIDs of available embedded subscriptions. This is used to surface
* entries for profiles which had been previously deleted.
* @param isEuiccRemovable whether the current ICCID is removable. Non-removable subscriptions
* will only be returned if the current ICCID is not removable; otherwise, they are left
* alone (not returned here unless in the embeddedIccids list) under the assumption that
* they will still be accessible when the eUICC containing them is activated.
*/
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
public List<SubscriptionInfo> getSubscriptionInfoListForEmbeddedSubscriptionUpdate(
String[] embeddedIccids, boolean isEuiccRemovable) {
StringBuilder whereClause = new StringBuilder();
whereClause.append("(").append(SubscriptionManager.IS_EMBEDDED).append("=1");
if (isEuiccRemovable) {
// Current eUICC is removable, so don't return non-removable subscriptions (which would
// be deleted), as these are expected to still be present on a different, non-removable
// eUICC.
whereClause.append(" AND ").append(SubscriptionManager.IS_REMOVABLE).append("=1");
}
// Else, return both removable and non-removable subscriptions. This is expected to delete
// all removable subscriptions, which is desired as they may not be accessible.
whereClause.append(") OR ").append(SubscriptionManager.ICC_ID).append(" IN (");
// ICCIDs are validated to contain only numbers when passed in, and come from a trusted
// app, so no need to escape.
for (int i = 0; i < embeddedIccids.length; i++) {
if (i > 0) {
whereClause.append(",");
}
whereClause.append("\"").append(embeddedIccids[i]).append("\"");
}
whereClause.append(")");
List<SubscriptionInfo> list = getSubInfo(whereClause.toString(), null);
if (list == null) {
return Collections.emptyList();
}
return list;
}
@Override
public void requestEmbeddedSubscriptionInfoListRefresh(int cardId) {
mContext.enforceCallingOrSelfPermission(Manifest.permission.WRITE_EMBEDDED_SUBSCRIPTIONS,
"requestEmbeddedSubscriptionInfoListRefresh");
long token = Binder.clearCallingIdentity();
try {
PhoneFactory.requestEmbeddedSubscriptionInfoListRefresh(cardId, null /* callback */);
} finally {
Binder.restoreCallingIdentity(token);
}
}
/**
* Asynchronously refresh the embedded subscription info list for the embedded card has the
* given card id {@code cardId}.
*
* @param callback Optional callback to execute after the refresh completes. Must terminate
* quickly as it will be called from SubscriptionInfoUpdater's handler thread.
*/
// No permission check needed as this is not exposed via AIDL.
public void requestEmbeddedSubscriptionInfoListRefresh(
int cardId, @Nullable Runnable callback) {
PhoneFactory.requestEmbeddedSubscriptionInfoListRefresh(cardId, callback);
}
/**
* Asynchronously refresh the embedded subscription info list for the embedded card has the
* default card id return by {@link TelephonyManager#getCardIdForDefaultEuicc()}.
*
* @param callback Optional callback to execute after the refresh completes. Must terminate
* quickly as it will be called from SubscriptionInfoUpdater's handler thread.
*/
// No permission check needed as this is not exposed via AIDL.
public void requestEmbeddedSubscriptionInfoListRefresh(@Nullable Runnable callback) {
PhoneFactory.requestEmbeddedSubscriptionInfoListRefresh(
mTelephonyManager.getCardIdForDefaultEuicc(), callback);
}
/**
* Add a new SubInfoRecord to subinfo database if needed
* @param iccId the IccId of the SIM card
* @param slotIndex the slot which the SIM is inserted
* @return 0 if success, < 0 on error.
*/
@Override
public int addSubInfoRecord(String iccId, int slotIndex) {
return addSubInfo(iccId, null, slotIndex, SubscriptionManager.SUBSCRIPTION_TYPE_LOCAL_SIM);
}
/**
* Add a new subscription info record, if needed.
* @param uniqueId This is the unique identifier for the subscription within the specific
* subscription type.
* @param displayName human-readable name of the device the subscription corresponds to.
* @param slotIndex value for {@link SubscriptionManager#SIM_SLOT_INDEX}
* @param subscriptionType the type of subscription to be added.
* @return 0 if success, < 0 on error.
*/
@Override
public int addSubInfo(String uniqueId, String displayName, int slotIndex,
int subscriptionType) {
if (DBG) {
String iccIdStr = uniqueId;
if (!isSubscriptionForRemoteSim(subscriptionType)) {
iccIdStr = SubscriptionInfo.givePrintableIccid(uniqueId);
}
logdl("[addSubInfoRecord]+ iccid: " + iccIdStr
+ ", slotIndex: " + slotIndex
+ ", subscriptionType: " + subscriptionType);
}
enforceModifyPhoneState("addSubInfo");
// Now that all security checks passes, perform the operation as ourselves.
final long identity = Binder.clearCallingIdentity();
try {
if (uniqueId == null) {
if (DBG) logdl("[addSubInfo]- null iccId");
return -1;
}
ContentResolver resolver = mContext.getContentResolver();
String selection = SubscriptionManager.ICC_ID + "=?";
String[] args;
if (isSubscriptionForRemoteSim(subscriptionType)) {
selection += " AND " + SubscriptionManager.SUBSCRIPTION_TYPE + "=?";
args = new String[]{uniqueId, Integer.toString(subscriptionType)};
} else {
selection += " OR " + SubscriptionManager.ICC_ID + "=?";
args = new String[]{uniqueId, IccUtils.getDecimalSubstring(uniqueId)};
}
Cursor cursor = resolver.query(SubscriptionManager.CONTENT_URI,
new String[]{SubscriptionManager.UNIQUE_KEY_SUBSCRIPTION_ID,
SubscriptionManager.SIM_SLOT_INDEX, SubscriptionManager.NAME_SOURCE,
SubscriptionManager.ICC_ID, SubscriptionManager.CARD_ID},
selection, args, null);
boolean setDisplayName = false;
try {
boolean recordsDoNotExist = (cursor == null || !cursor.moveToFirst());
if (isSubscriptionForRemoteSim(subscriptionType)) {
if (recordsDoNotExist) {
// create a Subscription record
slotIndex = SubscriptionManager.SLOT_INDEX_FOR_REMOTE_SIM_SUB;
Uri uri = insertEmptySubInfoRecord(uniqueId, displayName,
slotIndex, subscriptionType);
if (DBG) logd("[addSubInfoRecord] New record created: " + uri);
} else {
if (DBG) logdl("[addSubInfoRecord] Record already exists");
}
} else { // Handle Local SIM devices
if (recordsDoNotExist) {
setDisplayName = true;
Uri uri = insertEmptySubInfoRecord(uniqueId, slotIndex);
if (DBG) logdl("[addSubInfoRecord] New record created: " + uri);
} else { // there are matching records in the database for the given ICC_ID
int subId = cursor.getInt(0);
int oldSimInfoId = cursor.getInt(1);
int nameSource = cursor.getInt(2);
String oldIccId = cursor.getString(3);
String oldCardId = cursor.getString(4);
ContentValues value = new ContentValues();
if (slotIndex != oldSimInfoId) {
value.put(SubscriptionManager.SIM_SLOT_INDEX, slotIndex);
}
if (oldIccId != null && oldIccId.length() < uniqueId.length()
&& (oldIccId.equals(IccUtils.getDecimalSubstring(uniqueId)))) {
value.put(SubscriptionManager.ICC_ID, uniqueId);
}
UiccCard card = mUiccController.getUiccCardForPhone(slotIndex);
if (card != null) {
String cardId = card.getCardId();
if (cardId != null && cardId != oldCardId) {
value.put(SubscriptionManager.CARD_ID, cardId);
}
}
if (value.size() > 0) {
resolver.update(SubscriptionManager.getUriForSubscriptionId(subId),
value, null, null);
}
if (DBG) logdl("[addSubInfoRecord] Record already exists");
}
}
} finally {
if (cursor != null) {
cursor.close();
}
}
selection = SubscriptionManager.SIM_SLOT_INDEX + "=?";
args = new String[] {String.valueOf(slotIndex)};
if (isSubscriptionForRemoteSim(subscriptionType)) {
selection = SubscriptionManager.ICC_ID + "=? AND "
+ SubscriptionManager.SUBSCRIPTION_TYPE + "=?";
args = new String[]{uniqueId, Integer.toString(subscriptionType)};
}
cursor = resolver.query(SubscriptionManager.CONTENT_URI, null,
selection, args, null);
try {
if (cursor != null && cursor.moveToFirst()) {
do {
int subId = cursor.getInt(cursor.getColumnIndexOrThrow(
SubscriptionManager.UNIQUE_KEY_SUBSCRIPTION_ID));
// If sSlotIndexToSubIds already has the same subId for a slotIndex/phoneId,
// do not add it.
if (addToSubIdList(slotIndex, subId, subscriptionType)) {
// TODO While two subs active, if user deactivats first
// one, need to update the default subId with second one.
// FIXME: Currently we assume phoneId == slotIndex which in the future
// may not be true, for instance with multiple subs per slot.
// But is true at the moment.
int subIdCountMax = getActiveSubInfoCountMax();
int defaultSubId = getDefaultSubId();
if (DBG) {
logdl("[addSubInfoRecord]"
+ " sSlotIndexToSubIds.size=" + sSlotIndexToSubIds.size()
+ " slotIndex=" + slotIndex + " subId=" + subId
+ " defaultSubId=" + defaultSubId
+ " simCount=" + subIdCountMax);
}
// Set the default sub if not set or if single sim device
if (!isSubscriptionForRemoteSim(subscriptionType)) {
if (!SubscriptionManager.isValidSubscriptionId(defaultSubId)
|| subIdCountMax == 1) {
logdl("setting default fallback subid to " + subId);
setDefaultFallbackSubId(subId, subscriptionType);
}
// If single sim device, set this subscription as the default for
// everything
if (subIdCountMax == 1) {
if (DBG) {
logdl("[addSubInfoRecord] one sim set defaults to subId="
+ subId);
}
setDefaultDataSubId(subId);
setDefaultSmsSubId(subId);
setDefaultVoiceSubId(subId);
}
} else {
updateDefaultSubIdsIfNeeded(subId, subscriptionType);
}
} else {
if (DBG) {
logdl("[addSubInfoRecord] current SubId is already known, "
+ "IGNORE");
}
}
if (DBG) {
logdl("[addSubInfoRecord] hashmap(" + slotIndex + "," + subId + ")");
}
} while (cursor.moveToNext());
}
} finally {
if (cursor != null) {
cursor.close();
}
}
// Refresh the Cache of Active Subscription Info List. This should be done after
// updating sSlotIndexToSubIds which is done through addToSubIdList() above.
refreshCachedActiveSubscriptionInfoList();
if (isSubscriptionForRemoteSim(subscriptionType)) {
notifySubscriptionInfoChanged();
} else { // Handle Local SIM devices
// Set Display name after sub id is set above so as to get valid simCarrierName
int subId = getSubIdUsingPhoneId(slotIndex);
if (!SubscriptionManager.isValidSubscriptionId(subId)) {
if (DBG) {
logdl("[addSubInfoRecord]- getSubId failed invalid subId = " + subId);
}
return -1;
}
if (setDisplayName) {
String simCarrierName = mTelephonyManager.getSimOperatorName(subId);
String nameToSet;
if (!TextUtils.isEmpty(simCarrierName)) {
nameToSet = simCarrierName;
} else {
nameToSet = "CARD " + Integer.toString(slotIndex + 1);
}
ContentValues value = new ContentValues();
value.put(SubscriptionManager.DISPLAY_NAME, nameToSet);
resolver.update(SubscriptionManager.getUriForSubscriptionId(subId), value,
null, null);
// Refresh the Cache of Active Subscription Info List
refreshCachedActiveSubscriptionInfoList();
if (DBG) logdl("[addSubInfoRecord] sim name = " + nameToSet);
}
if (DBG) logdl("[addSubInfoRecord]- info size=" + sSlotIndexToSubIds.size());
}
} finally {
Binder.restoreCallingIdentity(identity);
}
return 0;
}
private void updateDefaultSubIdsIfNeeded(int newDefault, int subscriptionType) {
if (DBG) {
logdl("[updateDefaultSubIdsIfNeeded] newDefault=" + newDefault
+ ", subscriptionType=" + subscriptionType);
}
// Set the default ot new value only if the current default is invalid.
if (!isActiveSubscriptionId(getDefaultSubId())) {
// current default is not valid anylonger. set a new default
if (DBG) {
logdl("[updateDefaultSubIdsIfNeeded] set sDefaultFallbackSubId=" + newDefault);
}
setDefaultFallbackSubId(newDefault, subscriptionType);
}
int value = getDefaultSmsSubId();
if (!isActiveSubscriptionId(value)) {
// current default is not valid. set it to the given newDefault value
setDefaultSmsSubId(newDefault);
}
value = getDefaultDataSubId();
if (!isActiveSubscriptionId(value)) {
setDefaultDataSubId(newDefault);
}
value = getDefaultVoiceSubId();
if (!isActiveSubscriptionId(value)) {
setDefaultVoiceSubId(newDefault);
}
}
/**
* This method returns true if the given subId is among the list of currently active
* subscriptions.
*/
private boolean isActiveSubscriptionId(int subId) {
if (!SubscriptionManager.isValidSubscriptionId(subId)) return false;
ArrayList<Integer> subIdList = getActiveSubIdArrayList();
if (subIdList.isEmpty()) return false;
return subIdList.contains(new Integer(subId));
}
/*
* Delete subscription info record for the given device.
* @param uniqueId This is the unique identifier for the subscription within the specific
* subscription type.
* @param subscriptionType the type of subscription to be removed
* @return 0 if success, < 0 on error.
*/
@Override
public int removeSubInfo(String uniqueId, int subscriptionType) {
enforceModifyPhoneState("removeSubInfo");
if (DBG) {
logd("[removeSubInfo] uniqueId: " + uniqueId
+ ", subscriptionType: " + subscriptionType);
}
// validate the given info - does it exist in the active subscription list
int subId = SubscriptionManager.INVALID_SUBSCRIPTION_ID;
int slotIndex = SubscriptionManager.INVALID_SIM_SLOT_INDEX;
synchronized (mSubInfoListLock) {
for (SubscriptionInfo info : mCacheActiveSubInfoList) {
if ((info.getSubscriptionType() == subscriptionType)
&& info.getIccId().equalsIgnoreCase(uniqueId)) {
subId = info.getSubscriptionId();
slotIndex = info.getSimSlotIndex();
break;
}
}
}
if (subId == SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
if (DBG) {
logd("Invalid subscription details: subscriptionType = " + subscriptionType
+ ", uniqueId = " + uniqueId);
}
return -1;
}
if (DBG) logd("removing the subid : " + subId);
// Now that all security checks passes, perform the operation as ourselves.
int result = 0;
final long identity = Binder.clearCallingIdentity();
try {
ContentResolver resolver = mContext.getContentResolver();
result = resolver.delete(SubscriptionManager.CONTENT_URI,
SubscriptionManager.UNIQUE_KEY_SUBSCRIPTION_ID + "=? AND "
+ SubscriptionManager.SUBSCRIPTION_TYPE + "=?",
new String[]{Integer.toString(subId), Integer.toString(subscriptionType)});
if (result != 1) {
if (DBG) {
logd("found NO subscription to remove with subscriptionType = "
+ subscriptionType + ", uniqueId = " + uniqueId);
}
return -1;
}
refreshCachedActiveSubscriptionInfoList();
result = sSlotIndexToSubIds.removeFromSubIdList(slotIndex, subId);
if (result == NO_ENTRY_FOR_SLOT_INDEX) {
loge("sSlotIndexToSubIds has no entry for slotIndex = " + slotIndex);
} else if (result == SUB_ID_NOT_IN_SLOT) {
loge("sSlotIndexToSubIds has no subid: " + subId + ", in index: " + slotIndex);
}
// Since a subscription is removed, if this one is set as default for any setting,
// set some other subid as the default.
int newDefault = SubscriptionManager.INVALID_SUBSCRIPTION_ID;
SubscriptionInfo info = null;
final List<SubscriptionInfo> records = getActiveSubscriptionInfoList(
mContext.getOpPackageName(), mContext.getAttributionTag());
if (!records.isEmpty()) {
// yes, we have more subscriptions. pick the first one.
// FIXME do we need a policy to figure out which one is to be next default
info = records.get(0);
}
updateDefaultSubIdsIfNeeded(info.getSubscriptionId(), info.getSubscriptionType());
notifySubscriptionInfoChanged();
} finally {
Binder.restoreCallingIdentity(identity);
}
return result;
}
/**
* Clear an subscriptionInfo to subinfo database if needed by updating slot index to invalid.
* @param slotIndex the slot which the SIM is removed
*/
public void clearSubInfoRecord(int slotIndex) {
if (DBG) logdl("[clearSubInfoRecord]+ iccId:" + " slotIndex:" + slotIndex);
// update simInfo db with invalid slot index
ContentResolver resolver = mContext.getContentResolver();
ContentValues value = new ContentValues(1);
value.put(SubscriptionManager.SIM_SLOT_INDEX, SubscriptionManager.INVALID_SIM_SLOT_INDEX);
String where = "(" + SubscriptionManager.SIM_SLOT_INDEX + "=" + slotIndex + ")";
resolver.update(SubscriptionManager.CONTENT_URI, value, where, null);
// Refresh the Cache of Active Subscription Info List
refreshCachedActiveSubscriptionInfoList();
sSlotIndexToSubIds.remove(slotIndex);
}
/**
* Insert an empty SubInfo record into the database.
*
* <p>NOTE: This is not accessible to external processes, so it does not need a permission
* check. It is only intended for use by {@link SubscriptionInfoUpdater}.
*
* <p>Precondition: No record exists with this iccId.
*/
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
public Uri insertEmptySubInfoRecord(String iccId, int slotIndex) {
return insertEmptySubInfoRecord(iccId, null, slotIndex,
SubscriptionManager.SUBSCRIPTION_TYPE_LOCAL_SIM);
}
Uri insertEmptySubInfoRecord(String uniqueId, String displayName, int slotIndex,
int subscriptionType) {
ContentResolver resolver = mContext.getContentResolver();
ContentValues value = new ContentValues();
value.put(SubscriptionManager.ICC_ID, uniqueId);
int color = getUnusedColor(mContext.getOpPackageName(), mContext.getAttributionTag());
// default SIM color differs between slots
value.put(SubscriptionManager.HUE, color);
value.put(SubscriptionManager.SIM_SLOT_INDEX, slotIndex);
value.put(SubscriptionManager.CARRIER_NAME, "");
value.put(SubscriptionManager.CARD_ID, uniqueId);
value.put(SubscriptionManager.SUBSCRIPTION_TYPE, subscriptionType);
if (!TextUtils.isEmpty(displayName)) {
value.put(SubscriptionManager.DISPLAY_NAME, displayName);
}
if (!isSubscriptionForRemoteSim(subscriptionType)) {
UiccCard card = mUiccController.getUiccCardForPhone(slotIndex);
if (card != null) {
String cardId = card.getCardId();
if (cardId != null) {
value.put(SubscriptionManager.CARD_ID, cardId);
}
}
}
Uri uri = resolver.insert(SubscriptionManager.CONTENT_URI, value);
// Refresh the Cache of Active Subscription Info List
refreshCachedActiveSubscriptionInfoList();
return uri;
}
/**
* Generate and set carrier text based on input parameters
* @param showPlmn flag to indicate if plmn should be included in carrier text
* @param plmn plmn to be included in carrier text
* @param showSpn flag to indicate if spn should be included in carrier text
* @param spn spn to be included in carrier text
* @return true if carrier text is set, false otherwise
*/
@UnsupportedAppUsage
public boolean setPlmnSpn(int slotIndex, boolean showPlmn, String plmn, boolean showSpn,
String spn) {
synchronized (mLock) {
int subId = getSubIdUsingPhoneId(slotIndex);
if (mContext.getPackageManager().resolveContentProvider(
SubscriptionManager.CONTENT_URI.getAuthority(), 0) == null ||
!SubscriptionManager.isValidSubscriptionId(subId)) {
// No place to store this info. Notify registrants of the change anyway as they
// might retrieve the SPN/PLMN text from the SST sticky broadcast.
// TODO: This can be removed once SubscriptionController is not running on devices
// that don't need it, such as TVs.
if (DBG) logd("[setPlmnSpn] No valid subscription to store info");
notifySubscriptionInfoChanged();
return false;
}
String carrierText = "";
if (showPlmn) {
carrierText = plmn;
if (showSpn) {
// Need to show both plmn and spn if both are not same.
if(!Objects.equals(spn, plmn)) {
String separator = mContext.getString(
com.android.internal.R.string.kg_text_message_separator).toString();
carrierText = new StringBuilder().append(carrierText).append(separator)
.append(spn).toString();
}
}
} else if (showSpn) {
carrierText = spn;
}
setCarrierText(carrierText, subId);
return true;
}
}
/**
* Set carrier text by simInfo index
* @param text new carrier text
* @param subId the unique SubInfoRecord index in database
* @return the number of records updated
*/
private int setCarrierText(String text, int subId) {
if (DBG) logd("[setCarrierText]+ text:" + text + " subId:" + subId);
enforceModifyPhoneState("setCarrierText");
// Now that all security checks passes, perform the operation as ourselves.
final long identity = Binder.clearCallingIdentity();
try {
ContentValues value = new ContentValues(1);
value.put(SubscriptionManager.CARRIER_NAME, text);
int result = mContext.getContentResolver().update(
SubscriptionManager.getUriForSubscriptionId(subId), value, null, null);
// Refresh the Cache of Active Subscription Info List
refreshCachedActiveSubscriptionInfoList();
notifySubscriptionInfoChanged();
return result;
} finally {
Binder.restoreCallingIdentity(identity);
}
}
/**
* Set SIM color tint by simInfo index
* @param tint the tint color of the SIM
* @param subId the unique SubInfoRecord index in database
* @return the number of records updated
*/
@Override
public int setIconTint(int tint, int subId) {
if (DBG) logd("[setIconTint]+ tint:" + tint + " subId:" + subId);
enforceModifyPhoneState("setIconTint");
// Now that all security checks passes, perform the operation as ourselves.
final long identity = Binder.clearCallingIdentity();
try {
validateSubId(subId);
ContentValues value = new ContentValues(1);
value.put(SubscriptionManager.HUE, tint);
if (DBG) logd("[setIconTint]- tint:" + tint + " set");
int result = mContext.getContentResolver().update(
SubscriptionManager.getUriForSubscriptionId(subId), value, null, null);
// Refresh the Cache of Active Subscription Info List
refreshCachedActiveSubscriptionInfoList();
notifySubscriptionInfoChanged();
return result;
} finally {
Binder.restoreCallingIdentity(identity);
}
}
/**
* This is only for internal use and the returned priority is arbitrary. The idea is to give a
* higher value to name source that has higher priority to override other name sources.
* @param nameSource Source of display name
* @return int representing the priority. Higher value means higher priority.
*/
public static int getNameSourcePriority(@SimDisplayNameSource int nameSource) {
int index = Arrays.asList(
SubscriptionManager.NAME_SOURCE_CARRIER_ID,
SubscriptionManager.NAME_SOURCE_SIM_PNN,
SubscriptionManager.NAME_SOURCE_SIM_SPN,
SubscriptionManager.NAME_SOURCE_CARRIER,
SubscriptionManager.NAME_SOURCE_USER_INPUT // user has highest priority.
).indexOf(nameSource);
return (index < 0) ? 0 : index;
}
/**
* Validate whether the NAME_SOURCE_SIM_PNN, NAME_SOURCE_SIM_SPN and
* NAME_SOURCE_CARRIER exist or not.
*/
@VisibleForTesting
public boolean isExistingNameSourceStillValid(SubscriptionInfo subInfo) {
int subId = subInfo.getSubscriptionId();
int phoneId = getPhoneId(subInfo.getSubscriptionId());
Phone phone = PhoneFactory.getPhone(phoneId);
if (phone == null) {
return true;
}
String spn;
switch (subInfo.getNameSource()) {
case SubscriptionManager.NAME_SOURCE_SIM_PNN:
String pnn = phone.getPlmn();
return !TextUtils.isEmpty(pnn);
case SubscriptionManager.NAME_SOURCE_SIM_SPN:
spn = getServiceProviderName(phoneId);
return !TextUtils.isEmpty(spn);
case SubscriptionManager.NAME_SOURCE_CARRIER:
// Can not validate eSIM since it should not override with a lower priority source
// if the name is actually coming from eSIM and not from carrier config.
if (subInfo.isEmbedded()) {
return true;
}
CarrierConfigManager configLoader =
mContext.getSystemService(CarrierConfigManager.class);
PersistableBundle config =
configLoader.getConfigForSubId(subId);
if (config == null) {
return true;
}
boolean isCarrierNameOverride = config.getBoolean(
CarrierConfigManager.KEY_CARRIER_NAME_OVERRIDE_BOOL, false);
String carrierName = config.getString(
CarrierConfigManager.KEY_CARRIER_NAME_STRING);
spn = getServiceProviderName(phoneId);
return isCarrierNameOverride
|| (TextUtils.isEmpty(spn) && !TextUtils.isEmpty(carrierName));
case SubscriptionManager.NAME_SOURCE_CARRIER_ID:
case SubscriptionManager.NAME_SOURCE_USER_INPUT:
return true;
}
return false;
}
@VisibleForTesting
public String getServiceProviderName(int phoneId) {
UiccProfile profile = mUiccController.getUiccProfileForPhone(phoneId);
if (profile == null) {
return null;
}
return profile.getServiceProviderName();
}
/**
* Set display name by simInfo index with name source
* @param displayName the display name of SIM card
* @param subId the unique SubInfoRecord index in database
* @param nameSource SIM display name source
* @return the number of records updated
*/
@Override
public int setDisplayNameUsingSrc(String displayName, int subId,
@SimDisplayNameSource int nameSource) {
if (DBG) {
logd("[setDisplayName]+ displayName:" + displayName + " subId:" + subId
+ " nameSource:" + nameSource);
}
enforceModifyPhoneState("setDisplayNameUsingSrc");
// Now that all security checks passes, perform the operation as ourselves.
final long identity = Binder.clearCallingIdentity();
try {
validateSubId(subId);
List<SubscriptionInfo> allSubInfo = getSubInfo(null, null);
// if there is no sub in the db, return 0 since subId does not exist in db
if (allSubInfo == null || allSubInfo.isEmpty()) return 0;
for (SubscriptionInfo subInfo : allSubInfo) {
int subInfoNameSource = subInfo.getNameSource();
boolean isHigherPriority = (getNameSourcePriority(subInfoNameSource)
> getNameSourcePriority(nameSource));
boolean isEqualPriorityAndName = (getNameSourcePriority(subInfoNameSource)
== getNameSourcePriority(nameSource))
&& (TextUtils.equals(displayName, subInfo.getDisplayName()));
if (subInfo.getSubscriptionId() == subId
&& isExistingNameSourceStillValid(subInfo)
&& (isHigherPriority || isEqualPriorityAndName)) {
logd("Name source " + subInfoNameSource + "'s priority "
+ getNameSourcePriority(subInfoNameSource) + " is greater than "
+ "name source " + nameSource + "'s priority "
+ getNameSourcePriority(nameSource) + ", return now.");
return 0;
}
}
String nameToSet;
if (TextUtils.isEmpty(displayName) || displayName.trim().length() == 0) {
nameToSet = mTelephonyManager.getSimOperatorName(subId);
if (TextUtils.isEmpty(nameToSet)) {
if (nameSource == SubscriptionManager.NAME_SOURCE_USER_INPUT
&& SubscriptionManager.isValidSlotIndex(getSlotIndex(subId))) {
nameToSet = "CARD " + (getSlotIndex(subId) + 1);
} else {
nameToSet = mContext.getString(SubscriptionManager.DEFAULT_NAME_RES);
}
}
} else {
nameToSet = displayName;
}
ContentValues value = new ContentValues(1);
value.put(SubscriptionManager.DISPLAY_NAME, nameToSet);
if (nameSource >= SubscriptionManager.NAME_SOURCE_CARRIER_ID) {
if (DBG) logd("Set nameSource=" + nameSource);
value.put(SubscriptionManager.NAME_SOURCE, nameSource);
}
if (DBG) logd("[setDisplayName]- mDisplayName:" + nameToSet + " set");
// Update the nickname on the eUICC chip if it's an embedded subscription.
SubscriptionInfo sub = getSubscriptionInfo(subId);
if (sub != null && sub.isEmbedded()) {
// Ignore the result.
int cardId = sub.getCardId();
if (DBG) logd("Updating embedded sub nickname on cardId: " + cardId);
EuiccManager euiccManager = ((EuiccManager)
mContext.getSystemService(Context.EUICC_SERVICE)).createForCardId(cardId);
euiccManager.updateSubscriptionNickname(subId, displayName,
// This PendingIntent simply fulfills the requirement to pass in a callback;
// we don't care about the result (hence 0 requestCode and no action
// specified on the intent).
PendingIntent.getService(
mContext, 0 /* requestCode */, new Intent(), 0 /* flags */));
}
int result = updateDatabase(value, subId, true);
// Refresh the Cache of Active Subscription Info List
refreshCachedActiveSubscriptionInfoList();
notifySubscriptionInfoChanged();
return result;
} finally {
Binder.restoreCallingIdentity(identity);
}
}
/**
* Set phone number by subId
* @param number the phone number of the SIM
* @param subId the unique SubInfoRecord index in database
* @return the number of records updated
*/
@Override
public int setDisplayNumber(String number, int subId) {
if (DBG) logd("[setDisplayNumber]+ subId:" + subId);
enforceModifyPhoneState("setDisplayNumber");
// Now that all security checks passes, perform the operation as ourselves.
final long identity = Binder.clearCallingIdentity();
try {
validateSubId(subId);
int result;
int phoneId = getPhoneId(subId);
if (number == null || phoneId < 0 ||
phoneId >= mTelephonyManager.getPhoneCount()) {
if (DBG) logd("[setDispalyNumber]- fail");
return -1;
}
ContentValues value = new ContentValues(1);
value.put(SubscriptionManager.NUMBER, number);
// This function had a call to update number on the SIM (Phone.setLine1Number()) but
// that was removed as there doesn't seem to be a reason for that. If it is added
// back, watch out for deadlocks.
result = mContext.getContentResolver().update(
SubscriptionManager.getUriForSubscriptionId(subId), value, null, null);
// Refresh the Cache of Active Subscription Info List
refreshCachedActiveSubscriptionInfoList();
if (DBG) logd("[setDisplayNumber]- update result :" + result);
notifySubscriptionInfoChanged();
return result;
} finally {
Binder.restoreCallingIdentity(identity);
}
}
/**
* Set the EHPLMNs and HPLMNs associated with the subscription.
*/
public void setAssociatedPlmns(String[] ehplmns, String[] hplmns, int subId) {
if (DBG) logd("[setAssociatedPlmns]+ subId:" + subId);
validateSubId(subId);
int phoneId = getPhoneId(subId);
if (phoneId < 0 || phoneId >= mTelephonyManager.getPhoneCount()) {
if (DBG) logd("[setAssociatedPlmns]- fail");
return;
}
String formattedEhplmns = ehplmns == null ? "" : String.join(",", ehplmns);
String formattedHplmns = hplmns == null ? "" : String.join(",", hplmns);
ContentValues value = new ContentValues(2);
value.put(SubscriptionManager.EHPLMNS, formattedEhplmns);
value.put(SubscriptionManager.HPLMNS, formattedHplmns);
int count = mContext.getContentResolver().update(
SubscriptionManager.getUriForSubscriptionId(subId), value, null, null);
// Refresh the Cache of Active Subscription Info List
refreshCachedActiveSubscriptionInfoList();
if (DBG) logd("[setAssociatedPlmns]- update result :" + count);
notifySubscriptionInfoChanged();
}
/**
* Set data roaming by simInfo index
* @param roaming 0:Don't allow data when roaming, 1:Allow data when roaming
* @param subId the unique SubInfoRecord index in database
* @return the number of records updated
*/
@Override
public int setDataRoaming(int roaming, int subId) {
if (DBG) logd("[setDataRoaming]+ roaming:" + roaming + " subId:" + subId);
enforceModifyPhoneState("setDataRoaming");
// Now that all security checks passes, perform the operation as ourselves.
final long identity = Binder.clearCallingIdentity();
try {
validateSubId(subId);
if (roaming < 0) {
if (DBG) logd("[setDataRoaming]- fail");
return -1;
}
ContentValues value = new ContentValues(1);
value.put(SubscriptionManager.DATA_ROAMING, roaming);
if (DBG) logd("[setDataRoaming]- roaming:" + roaming + " set");
int result = updateDatabase(value, subId, true);
// Refresh the Cache of Active Subscription Info List
refreshCachedActiveSubscriptionInfoList();
notifySubscriptionInfoChanged();
return result;
} finally {
Binder.restoreCallingIdentity(identity);
}
}
public void syncGroupedSetting(int refSubId) {
logd("syncGroupedSetting");
try (Cursor cursor = mContext.getContentResolver().query(
SubscriptionManager.CONTENT_URI, null,
SubscriptionManager.UNIQUE_KEY_SUBSCRIPTION_ID + "=?",
new String[] {String.valueOf(refSubId)}, null)) {
if (cursor == null || !cursor.moveToFirst()) {
logd("[syncGroupedSetting] failed. Can't find refSubId " + refSubId);
return;
}
ContentValues values = new ContentValues(GROUP_SHARING_PROPERTIES.size());
for (String propKey : GROUP_SHARING_PROPERTIES) {
copyDataFromCursorToContentValue(propKey, cursor, values);
}
updateDatabase(values, refSubId, true);
}
}
private void copyDataFromCursorToContentValue(String propKey, Cursor cursor,
ContentValues values) {
int columnIndex = cursor.getColumnIndex(propKey);
if (columnIndex == -1) {
logd("[copyDataFromCursorToContentValue] can't find column " + propKey);
return;
}
switch (propKey) {
case SubscriptionManager.ENHANCED_4G_MODE_ENABLED:
case SubscriptionManager.VT_IMS_ENABLED:
case SubscriptionManager.WFC_IMS_ENABLED:
case SubscriptionManager.WFC_IMS_MODE:
case SubscriptionManager.WFC_IMS_ROAMING_MODE:
case SubscriptionManager.WFC_IMS_ROAMING_ENABLED:
case SubscriptionManager.DATA_ROAMING:
case SubscriptionManager.IMS_RCS_UCE_ENABLED:
values.put(propKey, cursor.getInt(columnIndex));
break;
case SubscriptionManager.DISPLAY_NAME:
case SubscriptionManager.DATA_ENABLED_OVERRIDE_RULES:
values.put(propKey, cursor.getString(columnIndex));
break;
default:
loge("[copyDataFromCursorToContentValue] invalid propKey " + propKey);
}
}
// TODO: replace all updates with this helper method.
private int updateDatabase(ContentValues value, int subId, boolean updateEntireGroup) {
List<SubscriptionInfo> infoList = getSubscriptionsInGroup(getGroupUuid(subId),
mContext.getOpPackageName(), mContext.getAttributionTag());
if (!updateEntireGroup || infoList == null || infoList.size() == 0) {
// Only update specified subscriptions.
return mContext.getContentResolver().update(
SubscriptionManager.getUriForSubscriptionId(subId), value, null, null);
} else {
// Update all subscriptions in the same group.
int[] subIdList = new int[infoList.size()];
for (int i = 0; i < infoList.size(); i++) {
subIdList[i] = infoList.get(i).getSubscriptionId();
}
return mContext.getContentResolver().update(SubscriptionManager.CONTENT_URI,
value, getSelectionForSubIdList(subIdList), null);
}
}
/**
* Set carrier id by subId
* @param carrierId the subscription carrier id.
* @param subId the unique SubInfoRecord index in database
* @return the number of records updated
*
* @see TelephonyManager#getSimCarrierId()
*/
public int setCarrierId(int carrierId, int subId) {
if (DBG) logd("[setCarrierId]+ carrierId:" + carrierId + " subId:" + subId);
enforceModifyPhoneState("setCarrierId");
// Now that all security checks passes, perform the operation as ourselves.
final long identity = Binder.clearCallingIdentity();
try {
validateSubId(subId);
ContentValues value = new ContentValues(1);
value.put(SubscriptionManager.CARRIER_ID, carrierId);
int result = mContext.getContentResolver().update(
SubscriptionManager.getUriForSubscriptionId(subId), value, null, null);
// Refresh the Cache of Active Subscription Info List
refreshCachedActiveSubscriptionInfoList();
notifySubscriptionInfoChanged();
return result;
} finally {
Binder.restoreCallingIdentity(identity);
}
}
/**
* Set MCC/MNC by subscription ID
* @param mccMnc MCC/MNC associated with the subscription
* @param subId the unique SubInfoRecord index in database
* @return the number of records updated
*/
public int setMccMnc(String mccMnc, int subId) {
String mccString = mccMnc.substring(0, 3);
String mncString = mccMnc.substring(3);
int mcc = 0;
int mnc = 0;
try {
mcc = Integer.parseInt(mccString);
mnc = Integer.parseInt(mncString);
} catch (NumberFormatException e) {
loge("[setMccMnc] - couldn't parse mcc/mnc: " + mccMnc);
}
if (DBG) logd("[setMccMnc]+ mcc/mnc:" + mcc + "/" + mnc + " subId:" + subId);
ContentValues value = new ContentValues(4);
value.put(SubscriptionManager.MCC, mcc);
value.put(SubscriptionManager.MNC, mnc);
value.put(SubscriptionManager.MCC_STRING, mccString);
value.put(SubscriptionManager.MNC_STRING, mncString);
int result = mContext.getContentResolver().update(
SubscriptionManager.getUriForSubscriptionId(subId), value, null, null);
// Refresh the Cache of Active Subscription Info List
refreshCachedActiveSubscriptionInfoList();
notifySubscriptionInfoChanged();
return result;
}
/**
* Set IMSI by subscription ID
* @param imsi IMSI (International Mobile Subscriber Identity)
* @return the number of records updated
*/
public int setImsi(String imsi, int subId) {
if (DBG) logd("[setImsi]+ imsi:" + imsi + " subId:" + subId);
ContentValues value = new ContentValues(1);
value.put(SubscriptionManager.IMSI, imsi);
int result = mContext.getContentResolver().update(
SubscriptionManager.getUriForSubscriptionId(subId), value, null, null);
// Refresh the Cache of Active Subscription Info List
refreshCachedActiveSubscriptionInfoList();
notifySubscriptionInfoChanged();
return result;
}
/**
* Set uicc applications being enabled or disabled.
* @param enabled whether uicc applications are enabled or disabled.
* @return the number of records updated
*/
public int setUiccApplicationsEnabled(boolean enabled, int subId) {
if (DBG) logd("[setUiccApplicationsEnabled]+ enabled:" + enabled + " subId:" + subId);
enforceModifyPhoneState("setUiccApplicationsEnabled");
long identity = Binder.clearCallingIdentity();
try {
ContentValues value = new ContentValues(1);
value.put(SubscriptionManager.UICC_APPLICATIONS_ENABLED, enabled);
int result = mContext.getContentResolver().update(
SubscriptionManager.getUriForSubscriptionId(subId), value, null, null);
// Refresh the Cache of Active Subscription Info List
refreshCachedActiveSubscriptionInfoList();
notifyUiccAppsEnableChanged();
notifySubscriptionInfoChanged();
return result;
} finally {
Binder.restoreCallingIdentity(identity);
}
}
/**
* Register to change of uicc applications enablement changes.
* @param notifyNow whether to notify target upon registration.
*/
public void registerForUiccAppsEnabled(Handler handler, int what, Object object,
boolean notifyNow) {
mUiccAppsEnableChangeRegList.addUnique(handler, what, object);
if (notifyNow) {
handler.obtainMessage(what, object).sendToTarget();
}
}
/**
* Unregister to change of uicc applications enablement changes.
*/
public void unregisterForUiccAppsEnabled(Handler handler) {
mUiccAppsEnableChangeRegList.remove(handler);
}
private void notifyUiccAppsEnableChanged() {
mUiccAppsEnableChangeRegList.notifyRegistrants();
}
/**
* Get IMSI by subscription ID
* For active subIds, this will always return the corresponding imsi
* For inactive subIds, once they are activated once, even if they are deactivated at the time
* of calling this function, the corresponding imsi will be returned
* When calling this method, the permission check should have already been done to allow
* only privileged read
*
* @return imsi
*/
public String getImsiPrivileged(int subId) {
try (Cursor cursor = mContext.getContentResolver().query(
SubscriptionManager.CONTENT_URI, null,
SubscriptionManager.UNIQUE_KEY_SUBSCRIPTION_ID + "=?",
new String[] {String.valueOf(subId)}, null)) {
String imsi = null;
if (cursor != null) {
if (cursor.moveToNext()) {
imsi = getOptionalStringFromCursor(cursor, SubscriptionManager.IMSI,
/*defaultVal*/ null);
}
} else {
logd("getImsiPrivileged: failed to retrieve imsi.");
}
return imsi;
}
}
/**
* Set ISO country code by subscription ID
* @param iso iso country code associated with the subscription
* @param subId the unique SubInfoRecord index in database
* @return the number of records updated
*/
public int setCountryIso(String iso, int subId) {
if (DBG) logd("[setCountryIso]+ iso:" + iso + " subId:" + subId);
ContentValues value = new ContentValues();
value.put(SubscriptionManager.ISO_COUNTRY_CODE, iso);
int result = mContext.getContentResolver().update(
SubscriptionManager.getUriForSubscriptionId(subId), value, null, null);
// Refresh the Cache of Active Subscription Info List
refreshCachedActiveSubscriptionInfoList();
notifySubscriptionInfoChanged();
return result;
}
@Override
public int getSlotIndex(int subId) {
if (VDBG) printStackTrace("[getSlotIndex] subId=" + subId);
if (subId == SubscriptionManager.DEFAULT_SUBSCRIPTION_ID) {
subId = getDefaultSubId();
}
if (!SubscriptionManager.isValidSubscriptionId(subId)) {
if (DBG) logd("[getSlotIndex]- subId invalid");
return SubscriptionManager.INVALID_SIM_SLOT_INDEX;
}
int size = sSlotIndexToSubIds.size();
if (size == 0) {
if (DBG) logd("[getSlotIndex]- size == 0, return SIM_NOT_INSERTED instead");
return SubscriptionManager.SIM_NOT_INSERTED;
}
for (Entry<Integer, ArrayList<Integer>> entry : sSlotIndexToSubIds.entrySet()) {
int sim = entry.getKey();
ArrayList<Integer> subs = entry.getValue();
if (subs != null && subs.contains(subId)) {
if (VDBG) logv("[getSlotIndex]- return = " + sim);
return sim;
}
}
if (DBG) logd("[getSlotIndex]- return fail");
return SubscriptionManager.INVALID_SIM_SLOT_INDEX;
}
/**
* Return the subId for specified slot Id.
* @deprecated
*/
@UnsupportedAppUsage
@Override
@Deprecated
public int[] getSubId(int slotIndex) {
if (VDBG) printStackTrace("[getSubId]+ slotIndex=" + slotIndex);
// Map default slotIndex to the current default subId.
// TODO: Not used anywhere sp consider deleting as it's somewhat nebulous
// as a slot maybe used for multiple different type of "connections"
// such as: voice, data and sms. But we're doing the best we can and using
// getDefaultSubId which makes a best guess.
if (slotIndex == SubscriptionManager.DEFAULT_SIM_SLOT_INDEX) {
slotIndex = getSlotIndex(getDefaultSubId());
if (VDBG) logd("[getSubId] map default slotIndex=" + slotIndex);
}
// Check that we have a valid slotIndex or the slotIndex is for a remote SIM (remote SIM
// uses special slot index that may be invalid otherwise)
if (!SubscriptionManager.isValidSlotIndex(slotIndex)
&& slotIndex != SubscriptionManager.SLOT_INDEX_FOR_REMOTE_SIM_SUB) {
if (DBG) logd("[getSubId]- invalid slotIndex=" + slotIndex);
return null;
}
// Check if we've got any SubscriptionInfo records using slotIndexToSubId as a surrogate.
int size = sSlotIndexToSubIds.size();
if (size == 0) {
if (VDBG) {
logd("[getSubId]- sSlotIndexToSubIds.size == 0, return null slotIndex="
+ slotIndex);
}
return null;
}
// Convert ArrayList to array
ArrayList<Integer> subIds = sSlotIndexToSubIds.getCopy(slotIndex);
if (subIds != null && subIds.size() > 0) {
int[] subIdArr = new int[subIds.size()];
for (int i = 0; i < subIds.size(); i++) {
subIdArr[i] = subIds.get(i);
}
if (VDBG) logd("[getSubId]- subIdArr=" + subIdArr);
return subIdArr;
} else {
if (DBG) logd("[getSubId]- numSubIds == 0, return null slotIndex=" + slotIndex);
return null;
}
}
@UnsupportedAppUsage
@Override
public int getPhoneId(int subId) {
if (VDBG) printStackTrace("[getPhoneId] subId=" + subId);
int phoneId;
if (subId == SubscriptionManager.DEFAULT_SUBSCRIPTION_ID) {
subId = getDefaultSubId();
if (DBG) logd("[getPhoneId] asked for default subId=" + subId);
}
if (!SubscriptionManager.isValidSubscriptionId(subId)) {
if (VDBG) {
logdl("[getPhoneId]- invalid subId return="
+ SubscriptionManager.INVALID_PHONE_INDEX);
}
return SubscriptionManager.INVALID_PHONE_INDEX;
}
int size = sSlotIndexToSubIds.size();
if (size == 0) {
phoneId = mDefaultPhoneId;
if (VDBG) logdl("[getPhoneId]- no sims, returning default phoneId=" + phoneId);
return phoneId;
}
// FIXME: Assumes phoneId == slotIndex
for (Entry<Integer, ArrayList<Integer>> entry: sSlotIndexToSubIds.entrySet()) {
int sim = entry.getKey();
ArrayList<Integer> subs = entry.getValue();
if (subs != null && subs.contains(subId)) {
if (VDBG) logdl("[getPhoneId]- found subId=" + subId + " phoneId=" + sim);
return sim;
}
}
phoneId = mDefaultPhoneId;
if (VDBG) {
logd("[getPhoneId]- subId=" + subId + " not found return default phoneId=" + phoneId);
}
return phoneId;
}
/**
* @return the number of records cleared
*/
@Override
public int clearSubInfo() {
enforceModifyPhoneState("clearSubInfo");
// Now that all security checks passes, perform the operation as ourselves.
final long identity = Binder.clearCallingIdentity();
try {
int size = sSlotIndexToSubIds.size();
if (size == 0) {
if (DBG) logdl("[clearSubInfo]- no simInfo size=" + size);
return 0;
}
sSlotIndexToSubIds.clear();
if (DBG) logdl("[clearSubInfo]- clear size=" + size);
return size;
} finally {
Binder.restoreCallingIdentity(identity);
}
}
private void logvl(String msg) {
logv(msg);
mLocalLog.log(msg);
}
private void logv(String msg) {
Rlog.v(LOG_TAG, msg);
}
@UnsupportedAppUsage
protected void logdl(String msg) {
logd(msg);
mLocalLog.log(msg);
}
@UnsupportedAppUsage
private void logd(String msg) {
Rlog.d(LOG_TAG, msg);
}
private void logel(String msg) {
loge(msg);
mLocalLog.log(msg);
}
@UnsupportedAppUsage
private void loge(String msg) {
Rlog.e(LOG_TAG, msg);
}
@UnsupportedAppUsage
@Override
public int getDefaultSubId() {
int subId;
boolean isVoiceCapable = mTelephonyManager.isVoiceCapable();
if (isVoiceCapable) {
subId = getDefaultVoiceSubId();
if (VDBG) logdl("[getDefaultSubId] isVoiceCapable subId=" + subId);
} else {
subId = getDefaultDataSubId();
if (VDBG) logdl("[getDefaultSubId] NOT VoiceCapable subId=" + subId);
}
if (!isActiveSubId(subId)) {
subId = sDefaultFallbackSubId.get();
if (VDBG) logdl("[getDefaultSubId] NOT active use fall back subId=" + subId);
}
if (VDBG) logv("[getDefaultSubId]- value = " + subId);
return subId;
}
@UnsupportedAppUsage
@Override
public void setDefaultSmsSubId(int subId) {
enforceModifyPhoneState("setDefaultSmsSubId");
if (subId == SubscriptionManager.DEFAULT_SUBSCRIPTION_ID) {
throw new RuntimeException("setDefaultSmsSubId called with DEFAULT_SUB_ID");
}
if (DBG) logdl("[setDefaultSmsSubId] subId=" + subId);
setGlobalSetting(Settings.Global.MULTI_SIM_SMS_SUBSCRIPTION, subId);
broadcastDefaultSmsSubIdChanged(subId);
}
private void broadcastDefaultSmsSubIdChanged(int subId) {
// Broadcast an Intent for default sms sub change
if (DBG) logdl("[broadcastDefaultSmsSubIdChanged] subId=" + subId);
Intent intent = new Intent(SubscriptionManager.ACTION_DEFAULT_SMS_SUBSCRIPTION_CHANGED);
intent.addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING);
SubscriptionManager.putSubscriptionIdExtra(intent, subId);
mContext.sendStickyBroadcastAsUser(intent, UserHandle.ALL);
}
@UnsupportedAppUsage
@Override
public int getDefaultSmsSubId() {
int subId = Settings.Global.getInt(mContext.getContentResolver(),
Settings.Global.MULTI_SIM_SMS_SUBSCRIPTION,
SubscriptionManager.INVALID_SUBSCRIPTION_ID);
if (VDBG) logd("[getDefaultSmsSubId] subId=" + subId);
return subId;
}
@UnsupportedAppUsage
@Override
public void setDefaultVoiceSubId(int subId) {
enforceModifyPhoneState("setDefaultVoiceSubId");
if (subId == SubscriptionManager.DEFAULT_SUBSCRIPTION_ID) {
throw new RuntimeException("setDefaultVoiceSubId called with DEFAULT_SUB_ID");
}
if (DBG) logdl("[setDefaultVoiceSubId] subId=" + subId);
int previousDefaultSub = getDefaultSubId();
setGlobalSetting(Settings.Global.MULTI_SIM_VOICE_CALL_SUBSCRIPTION, subId);
broadcastDefaultVoiceSubIdChanged(subId);
PhoneAccountHandle newHandle =
subId == SubscriptionManager.INVALID_SUBSCRIPTION_ID
? null : mTelephonyManager.getPhoneAccountHandleForSubscriptionId(
subId);
TelecomManager telecomManager = mContext.getSystemService(TelecomManager.class);
PhoneAccountHandle currentHandle = telecomManager.getUserSelectedOutgoingPhoneAccount();
if (!Objects.equals(currentHandle, newHandle)) {
telecomManager.setUserSelectedOutgoingPhoneAccount(newHandle);
logd("[setDefaultVoiceSubId] change to phoneAccountHandle=" + newHandle);
} else {
logd("[setDefaultVoiceSubId] default phone account not changed");
}
if (previousDefaultSub != getDefaultSubId()) {
sendDefaultChangedBroadcast(getDefaultSubId());
}
}
/**
* Broadcast intent of ACTION_DEFAULT_VOICE_SUBSCRIPTION_CHANGED.
* @hide
*/
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
public void broadcastDefaultVoiceSubIdChanged(int subId) {
// Broadcast an Intent for default voice sub change
if (DBG) logdl("[broadcastDefaultVoiceSubIdChanged] subId=" + subId);
Intent intent = new Intent(TelephonyIntents.ACTION_DEFAULT_VOICE_SUBSCRIPTION_CHANGED);
intent.addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING);
SubscriptionManager.putSubscriptionIdExtra(intent, subId);
mContext.sendStickyBroadcastAsUser(intent, UserHandle.ALL);
}
@UnsupportedAppUsage
@Override
public int getDefaultVoiceSubId() {
int subId = Settings.Global.getInt(mContext.getContentResolver(),
Settings.Global.MULTI_SIM_VOICE_CALL_SUBSCRIPTION,
SubscriptionManager.INVALID_SUBSCRIPTION_ID);
if (VDBG) logd("[getDefaultVoiceSubId] subId=" + subId);
return subId;
}
@UnsupportedAppUsage
@Override
public int getDefaultDataSubId() {
int subId = Settings.Global.getInt(mContext.getContentResolver(),
Settings.Global.MULTI_SIM_DATA_CALL_SUBSCRIPTION,
SubscriptionManager.INVALID_SUBSCRIPTION_ID);
if (VDBG) logd("[getDefaultDataSubId] subId=" + subId);
return subId;
}
@UnsupportedAppUsage
@Override
public void setDefaultDataSubId(int subId) {
enforceModifyPhoneState("setDefaultDataSubId");
final long identity = Binder.clearCallingIdentity();
try {
if (subId == SubscriptionManager.DEFAULT_SUBSCRIPTION_ID) {
throw new RuntimeException("setDefaultDataSubId called with DEFAULT_SUB_ID");
}
ProxyController proxyController = ProxyController.getInstance();
int len = TelephonyManager.from(mContext).getActiveModemCount();
logdl("[setDefaultDataSubId] num phones=" + len + ", subId=" + subId);
if (SubscriptionManager.isValidSubscriptionId(subId)) {
// Only re-map modems if the new default data sub is valid
RadioAccessFamily[] rafs = new RadioAccessFamily[len];
boolean atLeastOneMatch = false;
for (int phoneId = 0; phoneId < len; phoneId++) {
Phone phone = PhoneFactory.getPhone(phoneId);
int raf;
int id = phone.getSubId();
if (id == subId) {
// TODO Handle the general case of N modems and M subscriptions.
raf = proxyController.getMaxRafSupported();
atLeastOneMatch = true;
} else {
// TODO Handle the general case of N modems and M subscriptions.
raf = proxyController.getMinRafSupported();
}
logdl("[setDefaultDataSubId] phoneId=" + phoneId + " subId=" + id + " RAF="
+ raf);
rafs[phoneId] = new RadioAccessFamily(phoneId, raf);
}
if (atLeastOneMatch) {
proxyController.setRadioCapability(rafs);
} else {
if (DBG) logdl("[setDefaultDataSubId] no valid subId's found - not updating.");
}
}
int previousDefaultSub = getDefaultSubId();
setGlobalSetting(Settings.Global.MULTI_SIM_DATA_CALL_SUBSCRIPTION, subId);
MultiSimSettingController.getInstance().notifyDefaultDataSubChanged();
broadcastDefaultDataSubIdChanged(subId);
if (previousDefaultSub != getDefaultSubId()) {
sendDefaultChangedBroadcast(getDefaultSubId());
}
} finally {
Binder.restoreCallingIdentity(identity);
}
}
@UnsupportedAppUsage
private void broadcastDefaultDataSubIdChanged(int subId) {
// Broadcast an Intent for default data sub change
if (DBG) logdl("[broadcastDefaultDataSubIdChanged] subId=" + subId);
Intent intent = new Intent(TelephonyIntents.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED);
intent.addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING);
SubscriptionManager.putSubscriptionIdExtra(intent, subId);
mContext.sendStickyBroadcastAsUser(intent, UserHandle.ALL);
}
/* Sets the default subscription. If only one sub is active that
* sub is set as default subId. If two or more sub's are active
* the first sub is set as default subscription
*/
@UnsupportedAppUsage
protected void setDefaultFallbackSubId(int subId, int subscriptionType) {
if (subId == SubscriptionManager.DEFAULT_SUBSCRIPTION_ID) {
throw new RuntimeException("setDefaultSubId called with DEFAULT_SUB_ID");
}
if (DBG) {
logdl("[setDefaultFallbackSubId] subId=" + subId + ", subscriptionType="
+ subscriptionType);
}
int previousDefaultSub = getDefaultSubId();
if (isSubscriptionForRemoteSim(subscriptionType)) {
sDefaultFallbackSubId.set(subId);
return;
}
if (SubscriptionManager.isValidSubscriptionId(subId)) {
int phoneId = getPhoneId(subId);
if (phoneId >= 0 && (phoneId < mTelephonyManager.getPhoneCount()
|| mTelephonyManager.getSimCount() == 1)) {
if (DBG) logdl("[setDefaultFallbackSubId] set sDefaultFallbackSubId=" + subId);
sDefaultFallbackSubId.set(subId);
// Update MCC MNC device configuration information
String defaultMccMnc = mTelephonyManager.getSimOperatorNumericForPhone(phoneId);
MccTable.updateMccMncConfiguration(mContext, defaultMccMnc);
} else {
if (DBG) {
logdl("[setDefaultFallbackSubId] not set invalid phoneId=" + phoneId
+ " subId=" + subId);
}
}
}
if (previousDefaultSub != getDefaultSubId()) {
sendDefaultChangedBroadcast(getDefaultSubId());
}
}
public void sendDefaultChangedBroadcast(int subId) {
// Broadcast an Intent for default sub change
int phoneId = SubscriptionManager.getPhoneId(subId);
Intent intent = new Intent(SubscriptionManager.ACTION_DEFAULT_SUBSCRIPTION_CHANGED);
intent.addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING);
SubscriptionManager.putPhoneIdAndSubIdExtra(intent, phoneId, subId);
if (DBG) {
logdl("[sendDefaultChangedBroadcast] broadcast default subId changed phoneId="
+ phoneId + " subId=" + subId);
}
mContext.sendStickyBroadcastAsUser(intent, UserHandle.ALL);
}
/**
* Whether a subscription is opportunistic or not.
*/
public boolean isOpportunistic(int subId) {
SubscriptionInfo info = getActiveSubscriptionInfo(subId, mContext.getOpPackageName(),
mContext.getAttributionTag());
return (info != null) && info.isOpportunistic();
}
// FIXME: We need we should not be assuming phoneId == slotIndex as it will not be true
// when there are multiple subscriptions per sim and probably for other reasons.
@UnsupportedAppUsage
public int getSubIdUsingPhoneId(int phoneId) {
int[] subIds = getSubId(phoneId);
if (subIds == null || subIds.length == 0) {
return SubscriptionManager.INVALID_SUBSCRIPTION_ID;
}
return subIds[0];
}
/** Must be public for access from instrumentation tests. */
@VisibleForTesting
public List<SubscriptionInfo> getSubInfoUsingSlotIndexPrivileged(int slotIndex) {
if (DBG) logd("[getSubInfoUsingSlotIndexPrivileged]+ slotIndex:" + slotIndex);
if (slotIndex == SubscriptionManager.DEFAULT_SIM_SLOT_INDEX) {
slotIndex = getSlotIndex(getDefaultSubId());
}
if (!SubscriptionManager.isValidSlotIndex(slotIndex)) {
if (DBG) logd("[getSubInfoUsingSlotIndexPrivileged]- invalid slotIndex");
return null;
}
Cursor cursor = mContext.getContentResolver().query(SubscriptionManager.CONTENT_URI,
null, SubscriptionManager.SIM_SLOT_INDEX + "=?",
new String[]{String.valueOf(slotIndex)}, null);
ArrayList<SubscriptionInfo> subList = null;
try {
if (cursor != null) {
while (cursor.moveToNext()) {
SubscriptionInfo subInfo = getSubInfoRecord(cursor);
if (subInfo != null) {
if (subList == null) {
subList = new ArrayList<SubscriptionInfo>();
}
subList.add(subInfo);
}
}
}
} finally {
if (cursor != null) {
cursor.close();
}
}
if (DBG) logd("[getSubInfoUsingSlotIndex]- null info return");
return subList;
}
@UnsupportedAppUsage
private void validateSubId(int subId) {
if (DBG) logd("validateSubId subId: " + subId);
if (!SubscriptionManager.isValidSubscriptionId(subId)) {
throw new RuntimeException("Invalid sub id passed as parameter");
} else if (subId == SubscriptionManager.DEFAULT_SUBSCRIPTION_ID) {
throw new RuntimeException("Default sub id passed as parameter");
}
}
private synchronized ArrayList<Integer> getActiveSubIdArrayList() {
// Clone the sub id list so it can't change out from under us while iterating
List<Entry<Integer, ArrayList<Integer>>> simInfoList =
new ArrayList<>(sSlotIndexToSubIds.entrySet());
// Put the set of sub ids in slot index order
Collections.sort(simInfoList, (x, y) -> x.getKey().compareTo(y.getKey()));
// Collect the sub ids for each slot in turn
ArrayList<Integer> allSubs = new ArrayList<>();
for (Entry<Integer, ArrayList<Integer>> slot : simInfoList) {
allSubs.addAll(slot.getValue());
}
return allSubs;
}
private boolean isSubscriptionVisible(int subId) {
synchronized (mSubInfoListLock) {
for (SubscriptionInfo info : mCacheOpportunisticSubInfoList) {
if (info.getSubscriptionId() == subId) {
// If group UUID is null, it's stand alone opportunistic profile. So it's
// visible. Otherwise, it's bundled opportunistic profile, and is not visible.
return info.getGroupUuid() == null;
}
}
}
return true;
}
/**
* @return the list of subId's that are active, is never null but the length maybe 0.
*/
@Override
public int[] getActiveSubIdList(boolean visibleOnly) {
List<Integer> allSubs = getActiveSubIdArrayList();
if (visibleOnly) {
// Grouped opportunistic subscriptions should be hidden.
allSubs = allSubs.stream().filter(subId -> isSubscriptionVisible(subId))
.collect(Collectors.toList());
}
int[] subIdArr = new int[allSubs.size()];
int i = 0;
for (int sub : allSubs) {
subIdArr[i] = sub;
i++;
}
if (VDBG) {
logdl("[getActiveSubIdList] allSubs=" + allSubs + " subIdArr.length="
+ subIdArr.length);
}
return subIdArr;
}
@Override
public boolean isActiveSubId(int subId, String callingPackage, String callingFeatureId) {
if (!TelephonyPermissions.checkCallingOrSelfReadPhoneState(mContext, subId, callingPackage,
callingFeatureId, "isActiveSubId")) {
throw new SecurityException("Requires READ_PHONE_STATE permission.");
}
final long identity = Binder.clearCallingIdentity();
try {
return isActiveSubId(subId);
} finally {
Binder.restoreCallingIdentity(identity);
}
}
@UnsupportedAppUsage
@Deprecated // This should be moved into isActiveSubId(int, String)
public boolean isActiveSubId(int subId) {
boolean retVal = SubscriptionManager.isValidSubscriptionId(subId)
&& getActiveSubIdArrayList().contains(subId);
if (VDBG) logdl("[isActiveSubId]- " + retVal);
return retVal;
}
/**
* Get the SIM state for the slot index.
* For Remote-SIMs, this method returns {@link #IccCardConstants.State.UNKNOWN}
* @return SIM state as the ordinal of {@See IccCardConstants.State}
*/
@Override
public int getSimStateForSlotIndex(int slotIndex) {
State simState;
String err;
if (slotIndex < 0) {
simState = IccCardConstants.State.UNKNOWN;
err = "invalid slotIndex";
} else {
Phone phone = null;
try {
phone = PhoneFactory.getPhone(slotIndex);
} catch (IllegalStateException e) {
// ignore
}
if (phone == null) {
simState = IccCardConstants.State.UNKNOWN;
err = "phone == null";
} else {
IccCard icc = phone.getIccCard();
if (icc == null) {
simState = IccCardConstants.State.UNKNOWN;
err = "icc == null";
} else {
simState = icc.getState();
err = "";
}
}
}
if (VDBG) {
logd("getSimStateForSlotIndex: " + err + " simState=" + simState
+ " ordinal=" + simState.ordinal() + " slotIndex=" + slotIndex);
}
return simState.ordinal();
}
/**
* Store properties associated with SubscriptionInfo in database
* @param subId Subscription Id of Subscription
* @param propKey Column name in database associated with SubscriptionInfo
* @param propValue Value to store in DB for particular subId & column name
*
* @return number of rows updated.
* @hide
*/
@Override
public int setSubscriptionProperty(int subId, String propKey, String propValue) {
enforceModifyPhoneState("setSubscriptionProperty");
final long token = Binder.clearCallingIdentity();
try {
validateSubId(subId);
ContentResolver resolver = mContext.getContentResolver();
int result = setSubscriptionPropertyIntoContentResolver(
subId, propKey, propValue, resolver);
// Refresh the Cache of Active Subscription Info List
refreshCachedActiveSubscriptionInfoList();
return result;
} finally {
Binder.restoreCallingIdentity(token);
}
}
private int setSubscriptionPropertyIntoContentResolver(
int subId, String propKey, String propValue, ContentResolver resolver) {
ContentValues value = new ContentValues();
boolean updateEntireGroup = GROUP_SHARING_PROPERTIES.contains(propKey);
switch (propKey) {
case SubscriptionManager.CB_EXTREME_THREAT_ALERT:
case SubscriptionManager.CB_SEVERE_THREAT_ALERT:
case SubscriptionManager.CB_AMBER_ALERT:
case SubscriptionManager.CB_EMERGENCY_ALERT:
case SubscriptionManager.CB_ALERT_SOUND_DURATION:
case SubscriptionManager.CB_ALERT_REMINDER_INTERVAL:
case SubscriptionManager.CB_ALERT_VIBRATE:
case SubscriptionManager.CB_ALERT_SPEECH:
case SubscriptionManager.CB_ETWS_TEST_ALERT:
case SubscriptionManager.CB_CHANNEL_50_ALERT:
case SubscriptionManager.CB_CMAS_TEST_ALERT:
case SubscriptionManager.CB_OPT_OUT_DIALOG:
case SubscriptionManager.ENHANCED_4G_MODE_ENABLED:
case SubscriptionManager.IS_OPPORTUNISTIC:
case SubscriptionManager.VT_IMS_ENABLED:
case SubscriptionManager.WFC_IMS_ENABLED:
case SubscriptionManager.WFC_IMS_MODE:
case SubscriptionManager.WFC_IMS_ROAMING_MODE:
case SubscriptionManager.WFC_IMS_ROAMING_ENABLED:
case SubscriptionManager.IMS_RCS_UCE_ENABLED:
value.put(propKey, Integer.parseInt(propValue));
break;
case SubscriptionManager.ALLOWED_NETWORK_TYPES:
value.put(propKey, Long.parseLong(propValue));
break;
default:
if (DBG) logd("Invalid column name");
break;
}
return updateDatabase(value, subId, updateEntireGroup);
}
/**
* Get properties associated with SubscriptionInfo from database
*
* @param subId Subscription Id of Subscription
* @param propKey Column name in SubscriptionInfo database
* @return Value associated with subId and propKey column in database
*/
@Override
public String getSubscriptionProperty(int subId, String propKey, String callingPackage,
String callingFeatureId) {
if (!TelephonyPermissions.checkCallingOrSelfReadPhoneState(mContext, subId, callingPackage,
callingFeatureId, "getSubscriptionProperty")) {
return null;
}
final long identity = Binder.clearCallingIdentity();
try {
return getSubscriptionProperty(subId, propKey);
} finally {
Binder.restoreCallingIdentity(identity);
}
}
/**
* Get properties associated with SubscriptionInfo from database. Note this is the version
* without permission check for telephony internal use only.
*
* @param subId Subscription Id of Subscription
* @param propKey Column name in SubscriptionInfo database
* @return Value associated with subId and propKey column in database
*/
public String getSubscriptionProperty(int subId, String propKey) {
String resultValue = null;
try (Cursor cursor = mContext.getContentResolver().query(SubscriptionManager.CONTENT_URI,
new String[]{propKey},
SubscriptionManager.UNIQUE_KEY_SUBSCRIPTION_ID + "=?",
new String[]{subId + ""}, null)) {
if (cursor != null) {
if (cursor.moveToFirst()) {
switch (propKey) {
case SubscriptionManager.CB_EXTREME_THREAT_ALERT:
case SubscriptionManager.CB_SEVERE_THREAT_ALERT:
case SubscriptionManager.CB_AMBER_ALERT:
case SubscriptionManager.CB_EMERGENCY_ALERT:
case SubscriptionManager.CB_ALERT_SOUND_DURATION:
case SubscriptionManager.CB_ALERT_REMINDER_INTERVAL:
case SubscriptionManager.CB_ALERT_VIBRATE:
case SubscriptionManager.CB_ALERT_SPEECH:
case SubscriptionManager.CB_ETWS_TEST_ALERT:
case SubscriptionManager.CB_CHANNEL_50_ALERT:
case SubscriptionManager.CB_CMAS_TEST_ALERT:
case SubscriptionManager.CB_OPT_OUT_DIALOG:
case SubscriptionManager.ENHANCED_4G_MODE_ENABLED:
case SubscriptionManager.VT_IMS_ENABLED:
case SubscriptionManager.WFC_IMS_ENABLED:
case SubscriptionManager.WFC_IMS_MODE:
case SubscriptionManager.WFC_IMS_ROAMING_MODE:
case SubscriptionManager.WFC_IMS_ROAMING_ENABLED:
case SubscriptionManager.IMS_RCS_UCE_ENABLED:
case SubscriptionManager.IS_OPPORTUNISTIC:
case SubscriptionManager.GROUP_UUID:
case SubscriptionManager.DATA_ENABLED_OVERRIDE_RULES:
case SubscriptionManager.ALLOWED_NETWORK_TYPES:
resultValue = cursor.getString(0);
break;
default:
if(DBG) logd("Invalid column name");
break;
}
} else {
if(DBG) logd("Valid row not present in db");
}
} else {
if(DBG) logd("Query failed");
}
}
if (DBG) logd("getSubscriptionProperty Query value = " + resultValue);
return resultValue;
}
private void printStackTrace(String msg) {
RuntimeException re = new RuntimeException();
logd("StackTrace - " + msg);
StackTraceElement[] st = re.getStackTrace();
boolean first = true;
for (StackTraceElement ste : st) {
if (first) {
first = false;
} else {
logd(ste.toString());
}
}
}
@Override
public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
mContext.enforceCallingOrSelfPermission(android.Manifest.permission.DUMP,
"Requires DUMP");
final long token = Binder.clearCallingIdentity();
try {
pw.println("SubscriptionController:");
pw.println(" mLastISubServiceRegTime=" + mLastISubServiceRegTime);
pw.println(" defaultSubId=" + getDefaultSubId());
pw.println(" defaultDataSubId=" + getDefaultDataSubId());
pw.println(" defaultVoiceSubId=" + getDefaultVoiceSubId());
pw.println(" defaultSmsSubId=" + getDefaultSmsSubId());
pw.println(" defaultDataPhoneId=" + SubscriptionManager
.from(mContext).getDefaultDataPhoneId());
pw.println(" defaultVoicePhoneId=" + SubscriptionManager.getDefaultVoicePhoneId());
pw.println(" defaultSmsPhoneId=" + SubscriptionManager
.from(mContext).getDefaultSmsPhoneId());
pw.flush();
for (Entry<Integer, ArrayList<Integer>> entry : sSlotIndexToSubIds.entrySet()) {
pw.println(" sSlotIndexToSubId[" + entry.getKey() + "]: subIds=" + entry);
}
pw.flush();
pw.println("++++++++++++++++++++++++++++++++");
List<SubscriptionInfo> sirl = getActiveSubscriptionInfoList(
mContext.getOpPackageName(), mContext.getAttributionTag());
if (sirl != null) {
pw.println(" ActiveSubInfoList:");
for (SubscriptionInfo entry : sirl) {
pw.println(" " + entry.toString());
}
} else {
pw.println(" ActiveSubInfoList: is null");
}
pw.flush();
pw.println("++++++++++++++++++++++++++++++++");
sirl = getAllSubInfoList(mContext.getOpPackageName(), mContext.getAttributionTag());
if (sirl != null) {
pw.println(" AllSubInfoList:");
for (SubscriptionInfo entry : sirl) {
pw.println(" " + entry.toString());
}
} else {
pw.println(" AllSubInfoList: is null");
}
pw.flush();
pw.println("++++++++++++++++++++++++++++++++");
mLocalLog.dump(fd, pw, args);
pw.flush();
pw.println("++++++++++++++++++++++++++++++++");
pw.flush();
} finally {
Binder.restoreCallingIdentity(token);
}
}
/**
* Migrating Ims settings from global setting to subscription DB, if not already done.
*/
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
public void migrateImsSettings() {
migrateImsSettingHelper(
Settings.Global.ENHANCED_4G_MODE_ENABLED,
SubscriptionManager.ENHANCED_4G_MODE_ENABLED);
migrateImsSettingHelper(
Settings.Global.VT_IMS_ENABLED,
SubscriptionManager.VT_IMS_ENABLED);
migrateImsSettingHelper(
Settings.Global.WFC_IMS_ENABLED,
SubscriptionManager.WFC_IMS_ENABLED);
migrateImsSettingHelper(
Settings.Global.WFC_IMS_MODE,
SubscriptionManager.WFC_IMS_MODE);
migrateImsSettingHelper(
Settings.Global.WFC_IMS_ROAMING_MODE,
SubscriptionManager.WFC_IMS_ROAMING_MODE);
migrateImsSettingHelper(
Settings.Global.WFC_IMS_ROAMING_ENABLED,
SubscriptionManager.WFC_IMS_ROAMING_ENABLED);
}
private void migrateImsSettingHelper(String settingGlobal, String subscriptionProperty) {
ContentResolver resolver = mContext.getContentResolver();
int defaultSubId = getDefaultVoiceSubId();
if (defaultSubId == SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
return;
}
try {
int prevSetting = Settings.Global.getInt(resolver, settingGlobal);
if (prevSetting != DEPRECATED_SETTING) {
// Write previous setting into Subscription DB.
setSubscriptionPropertyIntoContentResolver(defaultSubId, subscriptionProperty,
Integer.toString(prevSetting), resolver);
// Write global setting value with DEPRECATED_SETTING making sure
// migration only happen once.
Settings.Global.putInt(resolver, settingGlobal, DEPRECATED_SETTING);
}
} catch (Settings.SettingNotFoundException e) {
}
}
/**
* Set whether a subscription is opportunistic.
*
* Throws SecurityException if doesn't have required permission.
*
* @param opportunistic whether it’s opportunistic subscription.
* @param subId the unique SubscriptionInfo index in database
* @param callingPackage The package making the IPC.
* @return the number of records updated
*/
@Override
public int setOpportunistic(boolean opportunistic, int subId, String callingPackage) {
try {
TelephonyPermissions.enforceCallingOrSelfModifyPermissionOrCarrierPrivilege(
mContext, subId, callingPackage);
} catch (SecurityException e) {
// The subscription may be inactive eSIM profile. If so, check the access rule in
// database.
enforceCarrierPrivilegeOnInactiveSub(subId, callingPackage,
"Caller requires permission on sub " + subId);
}
long token = Binder.clearCallingIdentity();
try {
int ret = setSubscriptionProperty(subId, SubscriptionManager.IS_OPPORTUNISTIC,
String.valueOf(opportunistic ? 1 : 0));
if (ret != 0) notifySubscriptionInfoChanged();
return ret;
} finally {
Binder.restoreCallingIdentity(token);
}
}
/**
* Get subscription info from database, and check whether caller has carrier privilege
* permission with it. If checking fails, throws SecurityException.
*/
private void enforceCarrierPrivilegeOnInactiveSub(int subId, String callingPackage,
String message) {
mAppOps.checkPackage(Binder.getCallingUid(), callingPackage);
SubscriptionManager subManager = (SubscriptionManager)
mContext.getSystemService(Context.TELEPHONY_SUBSCRIPTION_SERVICE);
List<SubscriptionInfo> subInfo = getSubInfo(
SubscriptionManager.UNIQUE_KEY_SUBSCRIPTION_ID + "=" + subId, null);
try {
if (!isActiveSubId(subId) && subInfo != null && subInfo.size() == 1
&& subManager.canManageSubscription(subInfo.get(0), callingPackage)) {
return;
}
throw new SecurityException(message);
} catch (IllegalArgumentException e) {
// canManageSubscription will throw IllegalArgumentException if sub is not embedded
// or package name is unknown. In this case, we also see it as permission check failure
// and throw a SecurityException.
throw new SecurityException(message);
}
}
@Override
public void setPreferredDataSubscriptionId(int subId, boolean needValidation,
ISetOpportunisticDataCallback callback) {
enforceModifyPhoneState("setPreferredDataSubscriptionId");
final long token = Binder.clearCallingIdentity();
try {
PhoneSwitcher phoneSwitcher = PhoneSwitcher.getInstance();
if (phoneSwitcher == null) {
logd("Set preferred data sub: phoneSwitcher is null.");
AnomalyReporter.reportAnomaly(
UUID.fromString("a3ab0b9d-f2aa-4baf-911d-7096c0d4645a"),
"Set preferred data sub: phoneSwitcher is null.");
if (callback != null) {
try {
callback.onComplete(SET_OPPORTUNISTIC_SUB_REMOTE_SERVICE_EXCEPTION);
} catch (RemoteException exception) {
logd("RemoteException " + exception);
}
}
return;
}
phoneSwitcher.trySetOpportunisticDataSubscription(subId, needValidation, callback);
} finally {
Binder.restoreCallingIdentity(token);
}
}
@Override
public int getPreferredDataSubscriptionId() {
enforceReadPrivilegedPhoneState("getPreferredDataSubscriptionId");
final long token = Binder.clearCallingIdentity();
try {
PhoneSwitcher phoneSwitcher = PhoneSwitcher.getInstance();
if (phoneSwitcher == null) {
AnomalyReporter.reportAnomaly(
UUID.fromString("a3ab0b9d-f2aa-4baf-911d-7096c0d4645a"),
"Get preferred data sub: phoneSwitcher is null.");
return SubscriptionManager.DEFAULT_SUBSCRIPTION_ID;
}
return phoneSwitcher.getOpportunisticDataSubscriptionId();
} finally {
Binder.restoreCallingIdentity(token);
}
}
@Override
public List<SubscriptionInfo> getOpportunisticSubscriptions(String callingPackage,
String callingFeatureId) {
return getSubscriptionInfoListFromCacheHelper(callingPackage, callingFeatureId,
makeCacheListCopyWithLock(mCacheOpportunisticSubInfoList));
}
/**
* Inform SubscriptionManager that subscriptions in the list are bundled
* as a group. Typically it's a primary subscription and an opportunistic
* subscription. It should only affect multi-SIM scenarios where primary
* and opportunistic subscriptions can be activated together.
* Being in the same group means they might be activated or deactivated
* together, some of them may be invisible to the users, etc.
*
* Caller will either have {@link android.Manifest.permission#MODIFY_PHONE_STATE}
* permission or had carrier privilege permission on the subscriptions:
* {@link TelephonyManager#hasCarrierPrivileges(int)} or
* {@link SubscriptionManager#canManageSubscription(SubscriptionInfo)}
*
* @throws SecurityException if the caller doesn't meet the requirements
* outlined above.
* @throws IllegalArgumentException if the some subscriptions in the list doesn't exist.
*
* @param subIdList list of subId that will be in the same group
* @return groupUUID a UUID assigned to the subscription group. It returns
* null if fails.
*
*/
@Override
public ParcelUuid createSubscriptionGroup(int[] subIdList, String callingPackage) {
if (subIdList == null || subIdList.length == 0) {
throw new IllegalArgumentException("Invalid subIdList " + subIdList);
}
// Makes sure calling package matches caller UID.
mAppOps.checkPackage(Binder.getCallingUid(), callingPackage);
// If it doesn't have modify phone state permission, or carrier privilege permission,
// a SecurityException will be thrown.
if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.MODIFY_PHONE_STATE)
!= PERMISSION_GRANTED && !checkCarrierPrivilegeOnSubList(
subIdList, callingPackage)) {
throw new SecurityException("CreateSubscriptionGroup needs MODIFY_PHONE_STATE or"
+ " carrier privilege permission on all specified subscriptions");
}
long identity = Binder.clearCallingIdentity();
try {
// Generate a UUID.
ParcelUuid groupUUID = new ParcelUuid(UUID.randomUUID());
ContentValues value = new ContentValues();
value.put(SubscriptionManager.GROUP_UUID, groupUUID.toString());
value.put(SubscriptionManager.GROUP_OWNER, callingPackage);
int result = mContext.getContentResolver().update(SubscriptionManager.CONTENT_URI,
value, getSelectionForSubIdList(subIdList), null);
if (DBG) logdl("createSubscriptionGroup update DB result: " + result);
refreshCachedActiveSubscriptionInfoList();
notifySubscriptionInfoChanged();
MultiSimSettingController.getInstance().notifySubscriptionGroupChanged(groupUUID);
return groupUUID;
} finally {
Binder.restoreCallingIdentity(identity);
}
}
private String getOwnerPackageOfSubGroup(ParcelUuid groupUuid) {
if (groupUuid == null) return null;
List<SubscriptionInfo> infoList = getSubInfo(SubscriptionManager.GROUP_UUID
+ "=\'" + groupUuid.toString() + "\'", null);
return ArrayUtils.isEmpty(infoList) ? null : infoList.get(0).getGroupOwner();
}
/**
* @param groupUuid a UUID assigned to the subscription group.
* @param callingPackage the package making the IPC.
* @return if callingPackage has carrier privilege on sublist.
*
*/
public boolean canPackageManageGroup(ParcelUuid groupUuid, String callingPackage) {
if (groupUuid == null) {
throw new IllegalArgumentException("Invalid groupUuid");
}
if (TextUtils.isEmpty(callingPackage)) {
throw new IllegalArgumentException("Empty callingPackage");
}
List<SubscriptionInfo> infoList;
// Getting all subscriptions in the group.
long identity = Binder.clearCallingIdentity();
try {
infoList = getSubInfo(SubscriptionManager.GROUP_UUID
+ "=\'" + groupUuid.toString() + "\'", null);
} finally {
Binder.restoreCallingIdentity(identity);
}
// If the group does not exist, then by default the UUID is up for grabs so no need to
// restrict management of a group (that someone may be attempting to create).
if (ArrayUtils.isEmpty(infoList)) {
return true;
}
// If the calling package is the group owner, skip carrier permission check and return
// true as it was done before.
if (callingPackage.equals(infoList.get(0).getGroupOwner())) return true;
// Check carrier privilege for all subscriptions in the group.
int[] subIdArray = infoList.stream().mapToInt(info -> info.getSubscriptionId())
.toArray();
return (checkCarrierPrivilegeOnSubList(subIdArray, callingPackage));
}
private int updateGroupOwner(ParcelUuid groupUuid, String groupOwner) {
// If the existing group owner is different from current caller, make caller the new
// owner of all subscriptions in group.
// This is for use-case of:
// 1) Both package1 and package2 has permission (MODIFY_PHONE_STATE or carrier
// privilege permission) of all related subscriptions.
// 2) Package 1 created a group.
// 3) Package 2 wants to add a subscription into it.
// Step 3 should be granted as all operations are permission based. Which means as
// long as the package passes the permission check, it can modify the subscription
// and the group. And package 2 becomes the new group owner as it's the last to pass
// permission checks on all members.
ContentValues value = new ContentValues(1);
value.put(SubscriptionManager.GROUP_OWNER, groupOwner);
return mContext.getContentResolver().update(SubscriptionManager.CONTENT_URI,
value, SubscriptionManager.GROUP_UUID + "=\"" + groupUuid + "\"", null);
}
@Override
public void addSubscriptionsIntoGroup(int[] subIdList, ParcelUuid groupUuid,
String callingPackage) {
if (subIdList == null || subIdList.length == 0) {
throw new IllegalArgumentException("Invalid subId list");
}
if (groupUuid == null || groupUuid.equals(INVALID_GROUP_UUID)) {
throw new IllegalArgumentException("Invalid groupUuid");
}
// Makes sure calling package matches caller UID.
mAppOps.checkPackage(Binder.getCallingUid(), callingPackage);
// If it doesn't have modify phone state permission, or carrier privilege permission,
// a SecurityException will be thrown.
if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.MODIFY_PHONE_STATE)
!= PERMISSION_GRANTED && !(checkCarrierPrivilegeOnSubList(subIdList, callingPackage)
&& canPackageManageGroup(groupUuid, callingPackage))) {
throw new SecurityException("Requires MODIFY_PHONE_STATE or carrier privilege"
+ " permissions on subscriptions and the group.");
}
long identity = Binder.clearCallingIdentity();
try {
if (DBG) {
logdl("addSubscriptionsIntoGroup sub list "
+ Arrays.toString(subIdList) + " into group " + groupUuid);
}
ContentValues value = new ContentValues();
value.put(SubscriptionManager.GROUP_UUID, groupUuid.toString());
int result = mContext.getContentResolver().update(SubscriptionManager.CONTENT_URI,
value, getSelectionForSubIdList(subIdList), null);
if (DBG) logdl("addSubscriptionsIntoGroup update DB result: " + result);
if (result > 0) {
updateGroupOwner(groupUuid, callingPackage);
refreshCachedActiveSubscriptionInfoList();
notifySubscriptionInfoChanged();
MultiSimSettingController.getInstance().notifySubscriptionGroupChanged(groupUuid);
}
} finally {
Binder.restoreCallingIdentity(identity);
}
}
/**
* Remove a list of subscriptions from their subscription group.
* See {@link SubscriptionManager#createSubscriptionGroup(List<Integer>)} for more details.
*
* Caller will either have {@link android.Manifest.permission#MODIFY_PHONE_STATE}
* permission or had carrier privilege permission on the subscriptions:
* {@link TelephonyManager#hasCarrierPrivileges()} or
* {@link SubscriptionManager#canManageSubscription(SubscriptionInfo)}
*
* @throws SecurityException if the caller doesn't meet the requirements
* outlined above.
* @throws IllegalArgumentException if the some subscriptions in the list doesn't belong
* the specified group.
*
* @param subIdList list of subId that need removing from their groups.
*
*/
public void removeSubscriptionsFromGroup(int[] subIdList, ParcelUuid groupUuid,
String callingPackage) {
if (subIdList == null || subIdList.length == 0) {
return;
}
// Makes sure calling package matches caller UID.
mAppOps.checkPackage(Binder.getCallingUid(), callingPackage);
// If it doesn't have modify phone state permission, or carrier privilege permission,
// a SecurityException will be thrown. If it's due to invalid parameter or internal state,
// it will return null.
if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.MODIFY_PHONE_STATE)
!= PERMISSION_GRANTED && !(checkCarrierPrivilegeOnSubList(subIdList, callingPackage)
&& canPackageManageGroup(groupUuid, callingPackage))) {
throw new SecurityException("removeSubscriptionsFromGroup needs MODIFY_PHONE_STATE or"
+ " carrier privilege permission on all specified subscriptions");
}
long identity = Binder.clearCallingIdentity();
try {
List<SubscriptionInfo> subInfoList = getSubInfo(getSelectionForSubIdList(subIdList),
null);
for (SubscriptionInfo info : subInfoList) {
if (!groupUuid.equals(info.getGroupUuid())) {
throw new IllegalArgumentException("Subscription " + info.getSubscriptionId()
+ " doesn't belong to group " + groupUuid);
}
}
ContentValues value = new ContentValues();
value.put(SubscriptionManager.GROUP_UUID, (String) null);
value.put(SubscriptionManager.GROUP_OWNER, (String) null);
int result = mContext.getContentResolver().update(SubscriptionManager.CONTENT_URI,
value, getSelectionForSubIdList(subIdList), null);
if (DBG) logdl("removeSubscriptionsFromGroup update DB result: " + result);
if (result > 0) {
updateGroupOwner(groupUuid, callingPackage);
refreshCachedActiveSubscriptionInfoList();
notifySubscriptionInfoChanged();
}
} finally {
Binder.restoreCallingIdentity(identity);
}
}
/**
* Helper function to check if the caller has carrier privilege permissions on a list of subId.
* The check can either be processed against access rules on currently active SIM cards, or
* the access rules we keep in our database for currently inactive eSIMs.
*
* @throws IllegalArgumentException if the some subId is invalid or doesn't exist.
*
* @return true if checking passes on all subId, false otherwise.
*/
private boolean checkCarrierPrivilegeOnSubList(int[] subIdList, String callingPackage) {
// Check carrier privilege permission on active subscriptions first.
// If it fails, they could be inactive. So keep them in a HashSet and later check
// access rules in our database.
Set<Integer> checkSubList = new HashSet<>();
for (int subId : subIdList) {
if (isActiveSubId(subId)) {
if (!mTelephonyManager.hasCarrierPrivileges(subId)) {
return false;
}
} else {
checkSubList.add(subId);
}
}
if (checkSubList.isEmpty()) {
return true;
}
long identity = Binder.clearCallingIdentity();
try {
// Check access rules for each sub info.
SubscriptionManager subscriptionManager = (SubscriptionManager)
mContext.getSystemService(Context.TELEPHONY_SUBSCRIPTION_SERVICE);
List<SubscriptionInfo> subInfoList = getSubInfo(
getSelectionForSubIdList(subIdList), null);
// Didn't find all the subscriptions specified in subIdList.
if (subInfoList == null || subInfoList.size() != subIdList.length) {
throw new IllegalArgumentException("Invalid subInfoList.");
}
for (SubscriptionInfo subInfo : subInfoList) {
if (checkSubList.contains(subInfo.getSubscriptionId())) {
if (subInfo.isEmbedded() && subscriptionManager.canManageSubscription(
subInfo, callingPackage)) {
checkSubList.remove(subInfo.getSubscriptionId());
} else {
return false;
}
}
}
return checkSubList.isEmpty();
} finally {
Binder.restoreCallingIdentity(identity);
}
}
/**
* Helper function to create selection argument of a list of subId.
* The result should be: "in (subId1, subId2, ...)".
*/
public static String getSelectionForSubIdList(int[] subId) {
StringBuilder selection = new StringBuilder();
selection.append(SubscriptionManager.UNIQUE_KEY_SUBSCRIPTION_ID);
selection.append(" IN (");
for (int i = 0; i < subId.length - 1; i++) {
selection.append(subId[i] + ", ");
}
selection.append(subId[subId.length - 1]);
selection.append(")");
return selection.toString();
}
/**
* Helper function to create selection argument of a list of subId.
* The result should be: "in (iccId1, iccId2, ...)".
*/
private String getSelectionForIccIdList(String[] iccIds) {
StringBuilder selection = new StringBuilder();
selection.append(SubscriptionManager.ICC_ID);
selection.append(" IN (");
for (int i = 0; i < iccIds.length - 1; i++) {
selection.append("\"" + iccIds[i] + "\", ");
}
selection.append("\"" + iccIds[iccIds.length - 1] + "\"");
selection.append(")");
return selection.toString();
}
/**
* Get subscriptionInfo list of subscriptions that are in the same group of given subId.
* See {@link #createSubscriptionGroup(int[], String)} for more details.
*
* Caller will either have {@link android.Manifest.permission#READ_PHONE_STATE}
* permission or had carrier privilege permission on the subscription.
* {@link TelephonyManager#hasCarrierPrivileges(int)}
*
* @throws SecurityException if the caller doesn't meet the requirements
* outlined above.
*
* @param groupUuid of which list of subInfo will be returned.
* @return list of subscriptionInfo that belong to the same group, including the given
* subscription itself. It will return an empty list if no subscription belongs to the group.
*
*/
@Override
public List<SubscriptionInfo> getSubscriptionsInGroup(ParcelUuid groupUuid,
String callingPackage, String callingFeatureId) {
long identity = Binder.clearCallingIdentity();
List<SubscriptionInfo> subInfoList;
try {
subInfoList = getAllSubInfoList(mContext.getOpPackageName(),
mContext.getAttributionTag());
if (groupUuid == null || subInfoList == null || subInfoList.isEmpty()) {
return new ArrayList<>();
}
} finally {
Binder.restoreCallingIdentity(identity);
}
return subInfoList.stream().filter(info -> {
if (!groupUuid.equals(info.getGroupUuid())) return false;
int subId = info.getSubscriptionId();
return TelephonyPermissions.checkCallingOrSelfReadPhoneState(mContext, subId,
callingPackage, callingFeatureId, "getSubscriptionsInGroup")
|| info.canManageSubscription(mContext, callingPackage);
}).map(subscriptionInfo -> conditionallyRemoveIdentifiers(subscriptionInfo,
callingPackage, callingFeatureId, "getSubscriptionsInGroup"))
.collect(Collectors.toList());
}
public ParcelUuid getGroupUuid(int subId) {
ParcelUuid groupUuid;
List<SubscriptionInfo> subInfo = getSubInfo(SubscriptionManager.UNIQUE_KEY_SUBSCRIPTION_ID
+ "=" + subId, null);
if (subInfo == null || subInfo.size() == 0) {
groupUuid = null;
} else {
groupUuid = subInfo.get(0).getGroupUuid();
}
return groupUuid;
}
/**
* Enable/Disable a subscription
* @param enable true if enabling, false if disabling
* @param subId the unique SubInfoRecord index in database
*
* @return true if success, false if fails or the further action is
* needed hence it's redirected to Euicc.
*/
@Override
public boolean setSubscriptionEnabled(boolean enable, int subId) {
enforceModifyPhoneState("setSubscriptionEnabled");
final long identity = Binder.clearCallingIdentity();
try {
logd("setSubscriptionEnabled" + (enable ? " enable " : " disable ")
+ " subId " + subId);
// Error checking.
if (!SubscriptionManager.isUsableSubscriptionId(subId)) {
throw new IllegalArgumentException(
"setSubscriptionEnabled not usable subId " + subId);
}
// Nothing to do if it's already active or inactive.
if (enable == isActiveSubscriptionId(subId)) return true;
SubscriptionInfo info = SubscriptionController.getInstance()
.getAllSubInfoList(mContext.getOpPackageName(), mContext.getAttributionTag())
.stream()
.filter(subInfo -> subInfo.getSubscriptionId() == subId)
.findFirst()
.get();
if (info == null) {
logd("setSubscriptionEnabled subId " + subId + " doesn't exist.");
return false;
}
// TODO: make sure after slot mapping, we enable the uicc applications for the
// subscription we are enabling.
if (info.isEmbedded()) {
return enableEmbeddedSubscription(info, enable);
} else {
return enablePhysicalSubscription(info, enable);
}
} finally {
Binder.restoreCallingIdentity(identity);
}
}
private boolean enableEmbeddedSubscription(SubscriptionInfo info, boolean enable) {
// We need to send intents to Euicc for operations:
// 1) In single SIM mode, turning on a eSIM subscription while pSIM is the active slot.
// Euicc will ask user to switch to DSDS if supported or to confirm SIM slot
// switching.
// 2) In DSDS mode, turning on / off an eSIM profile. Euicc can ask user whether
// to turn on DSDS, or whether to switch from current active eSIM profile to it, or
// to simply show a progress dialog.
// 3) In future, similar operations on triple SIM devices.
enableSubscriptionOverEuiccManager(info.getSubscriptionId(), enable,
SubscriptionManager.INVALID_SIM_SLOT_INDEX);
// returning false to indicate state is not changed. If changed, a subscriptionInfo
// change will be filed separately.
return false;
// TODO: uncomment or clean up if we decide whether to support standalone CBRS for Q.
// subId = enable ? subId : SubscriptionManager.INVALID_SUBSCRIPTION_ID;
// updateEnabledSubscriptionGlobalSetting(subId, physicalSlotIndex);
}
private boolean enablePhysicalSubscription(SubscriptionInfo info, boolean enable) {
if (info == null || !SubscriptionManager.isValidSubscriptionId(info.getSubscriptionId())) {
return false;
}
int subId = info.getSubscriptionId();
UiccSlotInfo slotInfo = null;
int physicalSlotIndex = SubscriptionManager.INVALID_SIM_SLOT_INDEX;
UiccSlotInfo[] slotsInfo = mTelephonyManager.getUiccSlotsInfo();
if (slotsInfo == null) return false;
for (int i = 0; i < slotsInfo.length; i++) {
UiccSlotInfo curSlotInfo = slotsInfo[i];
if (curSlotInfo.getCardStateInfo() == CARD_STATE_INFO_PRESENT) {
if (TextUtils.equals(IccUtils.stripTrailingFs(curSlotInfo.getCardId()),
IccUtils.stripTrailingFs(info.getCardString()))) {
slotInfo = curSlotInfo;
physicalSlotIndex = i;
break;
}
}
}
// Can't find the existing SIM.
if (slotInfo == null) return false;
if (enable && !slotInfo.getIsActive()) {
// We need to send intents to Euicc if we are turning on an inactive slot.
// Euicc will decide whether to ask user to switch to DSDS, or change SIM
// slot mapping.
EuiccManager euiccManager =
(EuiccManager) mContext.getSystemService(Context.EUICC_SERVICE);
if (euiccManager != null && euiccManager.isEnabled()) {
enableSubscriptionOverEuiccManager(subId, enable, physicalSlotIndex);
} else {
// Enable / disable uicc applications.
if (!info.areUiccApplicationsEnabled()) setUiccApplicationsEnabled(enable, subId);
// If euiccManager is not enabled, we try to switch to DSDS if possible,
// or switch slot if not.
if (mTelephonyManager.isMultiSimSupported() == MULTISIM_ALLOWED) {
PhoneConfigurationManager.getInstance().switchMultiSimConfig(
mTelephonyManager.getSupportedModemCount());
} else {
UiccController.getInstance().switchSlots(new int[]{physicalSlotIndex}, null);
}
}
return true;
} else {
// Enable / disable uicc applications.
setUiccApplicationsEnabled(enable, subId);
return true;
}
}
private void enableSubscriptionOverEuiccManager(int subId, boolean enable,
int physicalSlotIndex) {
logdl("enableSubscriptionOverEuiccManager" + (enable ? " enable " : " disable ")
+ "subId " + subId + " on slotIndex " + physicalSlotIndex);
Intent intent = new Intent(EuiccManager.ACTION_TOGGLE_SUBSCRIPTION_PRIVILEGED);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.putExtra(EuiccManager.EXTRA_SUBSCRIPTION_ID, subId);
intent.putExtra(EuiccManager.EXTRA_ENABLE_SUBSCRIPTION, enable);
if (physicalSlotIndex != SubscriptionManager.INVALID_SIM_SLOT_INDEX) {
intent.putExtra(EuiccManager.EXTRA_PHYSICAL_SLOT_ID, physicalSlotIndex);
}
mContext.startActivity(intent);
}
private void updateEnabledSubscriptionGlobalSetting(int subId, int physicalSlotIndex) {
// Write the value which subscription is enabled into global setting.
Settings.Global.putInt(mContext.getContentResolver(),
Settings.Global.ENABLED_SUBSCRIPTION_FOR_SLOT + physicalSlotIndex, subId);
}
private void updateModemStackEnabledGlobalSetting(boolean enabled, int physicalSlotIndex) {
// Write the whether a modem stack is disabled into global setting.
Settings.Global.putInt(mContext.getContentResolver(),
Settings.Global.MODEM_STACK_ENABLED_FOR_SLOT
+ physicalSlotIndex, enabled ? 1 : 0);
}
private int getPhysicalSlotIndex(boolean isEmbedded, int subId) {
UiccSlotInfo[] slotInfos = mTelephonyManager.getUiccSlotsInfo();
int logicalSlotIndex = getSlotIndex(subId);
int physicalSlotIndex = SubscriptionManager.INVALID_SIM_SLOT_INDEX;
boolean isLogicalSlotIndexValid = SubscriptionManager.isValidSlotIndex(logicalSlotIndex);
for (int i = 0; i < slotInfos.length; i++) {
// If we can know the logicalSlotIndex from subId, we should find the exact matching
// physicalSlotIndex. However for some cases like inactive eSIM, the logicalSlotIndex
// will be -1. In this case, we assume there's only one eSIM, and return the
// physicalSlotIndex of that eSIM.
if ((isLogicalSlotIndexValid && slotInfos[i].getLogicalSlotIdx() == logicalSlotIndex)
|| (!isLogicalSlotIndexValid && slotInfos[i].getIsEuicc() && isEmbedded)) {
physicalSlotIndex = i;
break;
}
}
return physicalSlotIndex;
}
private int getPhysicalSlotIndexFromLogicalSlotIndex(int logicalSlotIndex) {
int physicalSlotIndex = SubscriptionManager.INVALID_SIM_SLOT_INDEX;
UiccSlotInfo[] slotInfos = mTelephonyManager.getUiccSlotsInfo();
for (int i = 0; i < slotInfos.length; i++) {
if (slotInfos[i].getLogicalSlotIdx() == logicalSlotIndex) {
physicalSlotIndex = i;
break;
}
}
return physicalSlotIndex;
}
@Override
public boolean isSubscriptionEnabled(int subId) {
// TODO: b/123314365 support multi-eSIM and removable eSIM.
enforceReadPrivilegedPhoneState("isSubscriptionEnabled");
long identity = Binder.clearCallingIdentity();
try {
// Error checking.
if (!SubscriptionManager.isUsableSubscriptionId(subId)) {
throw new IllegalArgumentException(
"isSubscriptionEnabled not usable subId " + subId);
}
List<SubscriptionInfo> infoList = getSubInfo(
SubscriptionManager.UNIQUE_KEY_SUBSCRIPTION_ID + "=" + subId, null);
if (infoList == null || infoList.isEmpty()) {
// Subscription doesn't exist.
return false;
}
boolean isEmbedded = infoList.get(0).isEmbedded();
if (isEmbedded) {
return isActiveSubId(subId);
} else {
// For pSIM, we also need to check if modem is disabled or not.
return isActiveSubId(subId) && PhoneConfigurationManager.getInstance()
.getPhoneStatus(PhoneFactory.getPhone(getPhoneId(subId)));
}
} finally {
Binder.restoreCallingIdentity(identity);
}
}
@Override
public int getEnabledSubscriptionId(int logicalSlotIndex) {
// TODO: b/123314365 support multi-eSIM and removable eSIM.
enforceReadPrivilegedPhoneState("getEnabledSubscriptionId");
long identity = Binder.clearCallingIdentity();
try {
if (!SubscriptionManager.isValidPhoneId(logicalSlotIndex)) {
throw new IllegalArgumentException(
"getEnabledSubscriptionId with invalid logicalSlotIndex "
+ logicalSlotIndex);
}
// Getting and validating the physicalSlotIndex.
int physicalSlotIndex = getPhysicalSlotIndexFromLogicalSlotIndex(logicalSlotIndex);
if (physicalSlotIndex == SubscriptionManager.INVALID_SIM_SLOT_INDEX) {
return SubscriptionManager.INVALID_SUBSCRIPTION_ID;
}
// if modem stack is disabled, return INVALID_SUBSCRIPTION_ID without reading
// Settings.Global.ENABLED_SUBSCRIPTION_FOR_SLOT.
int modemStackEnabled = Settings.Global.getInt(mContext.getContentResolver(),
Settings.Global.MODEM_STACK_ENABLED_FOR_SLOT + physicalSlotIndex, 1);
if (modemStackEnabled != 1) {
return SubscriptionManager.INVALID_SUBSCRIPTION_ID;
}
int subId;
try {
subId = Settings.Global.getInt(mContext.getContentResolver(),
Settings.Global.ENABLED_SUBSCRIPTION_FOR_SLOT + physicalSlotIndex);
} catch (Settings.SettingNotFoundException e) {
// Value never set. Return whether it's currently active.
subId = getSubIdUsingPhoneId(logicalSlotIndex);
}
return subId;
} finally {
Binder.restoreCallingIdentity(identity);
}
}
/**
* Helper function of getOpportunisticSubscriptions and getActiveSubscriptionInfoList.
* They are doing similar things except operating on different cache.
*
* NOTE: the cacheSubList passed in is a *copy* of mCacheActiveSubInfoList or
* mCacheOpportunisticSubInfoList, so mSubInfoListLock is not required to access it. Also, this
* method may modify cacheSubList depending on the permissions the caller has.
*/
private List<SubscriptionInfo> getSubscriptionInfoListFromCacheHelper(
String callingPackage, String callingFeatureId, List<SubscriptionInfo> cacheSubList) {
boolean canReadPhoneState = false;
boolean canReadIdentifiers = false;
boolean canReadPhoneNumber = false;
try {
canReadPhoneState = TelephonyPermissions.checkReadPhoneState(mContext,
SubscriptionManager.INVALID_SUBSCRIPTION_ID, Binder.getCallingPid(),
Binder.getCallingUid(), callingPackage, callingFeatureId,
"getSubscriptionInfoList");
// If the calling package has the READ_PHONE_STATE permission then check if the caller
// also has access to subscriber identifiers and the phone number to ensure that the ICC
// ID and any other unique identifiers are removed if the caller should not have access.
if (canReadPhoneState) {
canReadIdentifiers = hasSubscriberIdentifierAccess(
SubscriptionManager.INVALID_SUBSCRIPTION_ID, callingPackage,
callingFeatureId, "getSubscriptionInfoList");
canReadPhoneNumber = hasPhoneNumberAccess(
SubscriptionManager.INVALID_SUBSCRIPTION_ID, callingPackage,
callingFeatureId, "getSubscriptionInfoList");
}
} catch (SecurityException e) {
// If a SecurityException is thrown during the READ_PHONE_STATE check then the only way
// to access a subscription is to have carrier privileges for its subId; an app with
// carrier privileges for a subscription is also granted access to all identifiers so
// the identifier and phone number access checks are not required.
}
if (canReadIdentifiers && canReadPhoneNumber) {
return cacheSubList;
}
// Filter the list to only include subscriptions which the caller can manage.
for (int subIndex = cacheSubList.size() - 1; subIndex >= 0; subIndex--) {
SubscriptionInfo subscriptionInfo = cacheSubList.get(subIndex);
int subId = subscriptionInfo.getSubscriptionId();
boolean hasCarrierPrivileges = TelephonyPermissions.checkCarrierPrivilegeForSubId(
mContext, subId);
// If the caller has carrier privileges then they are granted access to all
// identifiers for their subscription.
if (hasCarrierPrivileges) continue;
cacheSubList.remove(subIndex);
if (canReadPhoneState) {
// The caller does not have carrier privileges for this subId, filter the
// identifiers in the subscription based on the results of the initial
// permission checks.
cacheSubList.add(subIndex, conditionallyRemoveIdentifiers(
subscriptionInfo, canReadIdentifiers, canReadPhoneNumber));
}
}
return cacheSubList;
}
/**
* Conditionally removes identifiers from the provided {@code subInfo} if the {@code
* callingPackage} does not meet the access requirements for identifiers and returns the
* potentially modified object..
*
* <p>If the caller does not meet the access requirements for identifiers a clone of the
* provided SubscriptionInfo is created and modified to avoid altering SubscriptionInfo objects
* in a cache.
*/
private SubscriptionInfo conditionallyRemoveIdentifiers(SubscriptionInfo subInfo,
String callingPackage, String callingFeatureId, String message) {
SubscriptionInfo result = subInfo;
int subId = subInfo.getSubscriptionId();
boolean hasIdentifierAccess = hasSubscriberIdentifierAccess(subId, callingPackage,
callingFeatureId, message);
boolean hasPhoneNumberAccess = hasPhoneNumberAccess(subId, callingPackage, callingFeatureId,
message);
return conditionallyRemoveIdentifiers(subInfo, hasIdentifierAccess, hasPhoneNumberAccess);
}
/**
* Conditionally removes identifiers from the provided {@code subInfo} based on if the calling
* package {@code hasIdentifierAccess} and {@code hasPhoneNumberAccess} and returns the
* potentially modified object.
*
* <p>If the caller specifies the package does not have identifier or phone number access
* a clone of the provided SubscriptionInfo is created and modified to avoid altering
* SubscriptionInfo objects in a cache.
*/
private SubscriptionInfo conditionallyRemoveIdentifiers(SubscriptionInfo subInfo,
boolean hasIdentifierAccess, boolean hasPhoneNumberAccess) {
if (hasIdentifierAccess && hasPhoneNumberAccess) {
return subInfo;
}
SubscriptionInfo result = new SubscriptionInfo(subInfo);
if (!hasIdentifierAccess) {
result.clearIccId();
result.clearCardString();
}
if (!hasPhoneNumberAccess) {
result.clearNumber();
}
return result;
}
private synchronized boolean addToSubIdList(int slotIndex, int subId, int subscriptionType) {
ArrayList<Integer> subIdsList = sSlotIndexToSubIds.getCopy(slotIndex);
if (subIdsList == null) {
subIdsList = new ArrayList<>();
sSlotIndexToSubIds.put(slotIndex, subIdsList);
}
// add the given subId unless it already exists
if (subIdsList.contains(subId)) {
logdl("slotIndex, subId combo already exists in the map. Not adding it again.");
return false;
}
if (isSubscriptionForRemoteSim(subscriptionType)) {
// For Remote SIM subscriptions, a slot can have multiple subscriptions.
sSlotIndexToSubIds.addToSubIdList(slotIndex, subId);
} else {
// for all other types of subscriptions, a slot can have only one subscription at a time
sSlotIndexToSubIds.clearSubIdList(slotIndex);
sSlotIndexToSubIds.addToSubIdList(slotIndex, subId);
}
// Remove the slot from sSlotIndexToSubIds if it has the same sub id with the added slot
for (Entry<Integer, ArrayList<Integer>> entry : sSlotIndexToSubIds.entrySet()) {
if (entry.getKey() != slotIndex && entry.getValue() != null
&& entry.getValue().contains(subId)) {
logdl("addToSubIdList - remove " + entry.getKey());
sSlotIndexToSubIds.remove(entry.getKey());
}
}
if (DBG) logdl("slotIndex, subId combo is added to the map.");
return true;
}
private boolean isSubscriptionForRemoteSim(int subscriptionType) {
return subscriptionType == SubscriptionManager.SUBSCRIPTION_TYPE_REMOTE_SIM;
}
/**
* This is only for testing
* @hide
*/
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
public Map<Integer, ArrayList<Integer>> getSlotIndexToSubIdsMap() {
return sSlotIndexToSubIds.getMap();
}
/**
* This is only for testing
* @hide
*/
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
public void resetStaticMembers() {
sDefaultFallbackSubId.set(SubscriptionManager.INVALID_SUBSCRIPTION_ID);
mDefaultPhoneId = SubscriptionManager.DEFAULT_PHONE_INDEX;
}
private void notifyOpportunisticSubscriptionInfoChanged() {
TelephonyRegistryManager trm =
(TelephonyRegistryManager)
mContext.getSystemService(Context.TELEPHONY_REGISTRY_SERVICE);
if (DBG) logd("notifyOpptSubscriptionInfoChanged:");
trm.notifyOpportunisticSubscriptionInfoChanged();
}
private void refreshCachedOpportunisticSubscriptionInfoList() {
List<SubscriptionInfo> subList = getSubInfo(
SubscriptionManager.IS_OPPORTUNISTIC + "=1 AND ("
+ SubscriptionManager.SIM_SLOT_INDEX + ">=0 OR "
+ SubscriptionManager.IS_EMBEDDED + "=1)", null);
synchronized (mSubInfoListLock) {
List<SubscriptionInfo> oldOpptCachedList = mCacheOpportunisticSubInfoList;
if (subList != null) {
subList.sort(SUBSCRIPTION_INFO_COMPARATOR);
} else {
subList = new ArrayList<>();
}
mCacheOpportunisticSubInfoList = subList;
for (SubscriptionInfo info : mCacheOpportunisticSubInfoList) {
if (shouldDisableSubGroup(info.getGroupUuid())) {
info.setGroupDisabled(true);
}
}
if (DBG_CACHE) {
if (!mCacheOpportunisticSubInfoList.isEmpty()) {
for (SubscriptionInfo si : mCacheOpportunisticSubInfoList) {
logd("[refreshCachedOpptSubscriptionInfoList] Setting Cached info="
+ si);
}
} else {
logdl("[refreshCachedOpptSubscriptionInfoList]- no info return");
}
}
if (!oldOpptCachedList.equals(mCacheOpportunisticSubInfoList)) {
mOpptSubInfoListChangedDirtyBit.set(true);
}
}
}
private boolean shouldDisableSubGroup(ParcelUuid groupUuid) {
if (groupUuid == null) return false;
synchronized (mSubInfoListLock) {
for (SubscriptionInfo activeInfo : mCacheActiveSubInfoList) {
if (!activeInfo.isOpportunistic() && groupUuid.equals(activeInfo.getGroupUuid())) {
return false;
}
}
}
return true;
}
/**
* Set allowing mobile data during voice call.
*
* @param subId Subscription index
* @param rules Data enabled override rules in string format. See {@link DataEnabledOverride}
* for details.
* @return {@code true} if settings changed, otherwise {@code false}.
*/
public boolean setDataEnabledOverrideRules(int subId, @NonNull String rules) {
if (DBG) logd("[setDataEnabledOverrideRules]+ rules:" + rules + " subId:" + subId);
validateSubId(subId);
ContentValues value = new ContentValues(1);
value.put(SubscriptionManager.DATA_ENABLED_OVERRIDE_RULES, rules);
boolean result = updateDatabase(value, subId, true) > 0;
if (result) {
// Refresh the Cache of Active Subscription Info List
refreshCachedActiveSubscriptionInfoList();
notifySubscriptionInfoChanged();
}
return result;
}
/**
* Get data enabled override rules.
*
* @param subId Subscription index
* @return Data enabled override rules in string
*/
@NonNull
public String getDataEnabledOverrideRules(int subId) {
return TelephonyUtils.emptyIfNull(getSubscriptionProperty(subId,
SubscriptionManager.DATA_ENABLED_OVERRIDE_RULES));
}
/**
* Get active data subscription id.
*
* @return Active data subscription id
*
* @hide
*/
@Override
public int getActiveDataSubscriptionId() {
final long token = Binder.clearCallingIdentity();
try {
PhoneSwitcher phoneSwitcher = PhoneSwitcher.getInstance();
if (phoneSwitcher != null) {
int activeDataSubId = phoneSwitcher.getActiveDataSubId();
if (SubscriptionManager.isUsableSubscriptionId(activeDataSubId)) {
return activeDataSubId;
}
}
// If phone switcher isn't ready, or active data sub id is not available, use default
// sub id from settings.
return getDefaultDataSubId();
} finally {
Binder.restoreCallingIdentity(token);
}
}
/**
* Whether it's supported to disable / re-enable a subscription on a physical (non-euicc) SIM.
*/
@Override
public boolean canDisablePhysicalSubscription() {
enforceReadPrivilegedPhoneState("canToggleUiccApplicationsEnablement");
final long identity = Binder.clearCallingIdentity();
try {
Phone phone = PhoneFactory.getDefaultPhone();
return phone != null && phone.canDisablePhysicalSubscription();
} finally {
Binder.restoreCallingIdentity(identity);
}
}
/**
* @hide
*/
private void setGlobalSetting(String name, int value) {
Settings.Global.putInt(mContext.getContentResolver(), name, value);
if (name == Settings.Global.MULTI_SIM_DATA_CALL_SUBSCRIPTION) {
invalidateDefaultDataSubIdCaches();
invalidateActiveDataSubIdCaches();
invalidateDefaultSubIdCaches();
invalidateSlotIndexCaches();
} else if (name == Settings.Global.MULTI_SIM_VOICE_CALL_SUBSCRIPTION) {
invalidateDefaultSubIdCaches();
invalidateSlotIndexCaches();
} else if (name == Settings.Global.MULTI_SIM_SMS_SUBSCRIPTION) {
invalidateDefaultSmsSubIdCaches();
}
}
/**
* @hide
*/
private static void invalidateDefaultSubIdCaches() {
if (sCachingEnabled) {
SubscriptionManager.invalidateDefaultSubIdCaches();
}
}
/**
* @hide
*/
private static void invalidateDefaultDataSubIdCaches() {
if (sCachingEnabled) {
SubscriptionManager.invalidateDefaultDataSubIdCaches();
}
}
/**
* @hide
*/
private static void invalidateDefaultSmsSubIdCaches() {
if (sCachingEnabled) {
SubscriptionManager.invalidateDefaultSmsSubIdCaches();
}
}
/**
* @hide
*/
protected static void invalidateActiveDataSubIdCaches() {
if (sCachingEnabled) {
SubscriptionManager.invalidateActiveDataSubIdCaches();
}
}
/**
* @hide
*/
protected static void invalidateSlotIndexCaches() {
if (sCachingEnabled) {
SubscriptionManager.invalidateSlotIndexCaches();
}
}
/**
* @hide
*/
@VisibleForTesting
public static void disableCaching() {
sCachingEnabled = false;
}
/**
* @hide
*/
@VisibleForTesting
public static void enableCaching() {
sCachingEnabled = true;
}
}