| /* |
| * Copyright (C) 2015 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.data; |
| |
| import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR; |
| import static android.telephony.CarrierConfigManager.KEY_DATA_SWITCH_VALIDATION_TIMEOUT_LONG; |
| import static android.telephony.SubscriptionManager.DEFAULT_SUBSCRIPTION_ID; |
| import static android.telephony.SubscriptionManager.INVALID_PHONE_INDEX; |
| import static android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID; |
| import static android.telephony.TelephonyManager.SET_OPPORTUNISTIC_SUB_INACTIVE_SUBSCRIPTION; |
| import static android.telephony.TelephonyManager.SET_OPPORTUNISTIC_SUB_SUCCESS; |
| import static android.telephony.TelephonyManager.SET_OPPORTUNISTIC_SUB_VALIDATION_FAILED; |
| import static android.telephony.ims.stub.ImsRegistrationImplBase.REGISTRATION_TECH_CROSS_SIM; |
| import static android.telephony.ims.stub.ImsRegistrationImplBase.REGISTRATION_TECH_IWLAN; |
| import static android.telephony.ims.stub.ImsRegistrationImplBase.REGISTRATION_TECH_NONE; |
| |
| import static java.util.Arrays.copyOf; |
| |
| import android.annotation.NonNull; |
| import android.annotation.Nullable; |
| import android.app.Notification; |
| import android.app.NotificationManager; |
| import android.app.PendingIntent; |
| import android.compat.annotation.UnsupportedAppUsage; |
| import android.content.BroadcastReceiver; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.IntentFilter; |
| import android.net.ConnectivityManager; |
| import android.net.MatchAllNetworkSpecifier; |
| import android.net.Network; |
| import android.net.NetworkCapabilities; |
| import android.net.NetworkFactory; |
| import android.net.NetworkRequest; |
| import android.net.NetworkSpecifier; |
| import android.net.TelephonyNetworkSpecifier; |
| import android.os.AsyncResult; |
| import android.os.Build; |
| import android.os.Bundle; |
| import android.os.Handler; |
| import android.os.Looper; |
| import android.os.Message; |
| import android.os.PersistableBundle; |
| import android.os.Registrant; |
| import android.os.RegistrantList; |
| import android.os.RemoteException; |
| import android.provider.Settings; |
| import android.telephony.AccessNetworkConstants; |
| import android.telephony.CarrierConfigManager; |
| import android.telephony.NetworkRegistrationInfo; |
| import android.telephony.PhoneCapability; |
| import android.telephony.PhoneStateListener; |
| import android.telephony.SubscriptionInfo; |
| import android.telephony.SubscriptionManager; |
| import android.telephony.TelephonyManager; |
| import android.telephony.TelephonyRegistryManager; |
| import android.telephony.ims.ImsReasonInfo; |
| import android.telephony.ims.ImsRegistrationAttributes; |
| import android.telephony.ims.RegistrationManager; |
| import android.telephony.ims.stub.ImsRegistrationImplBase; |
| import android.util.ArrayMap; |
| import android.util.LocalLog; |
| import android.util.Log; |
| |
| import com.android.ims.ImsException; |
| import com.android.ims.ImsManager; |
| import com.android.internal.annotations.VisibleForTesting; |
| import com.android.internal.telephony.CommandException; |
| import com.android.internal.telephony.ISetOpportunisticDataCallback; |
| import com.android.internal.telephony.IccCard; |
| import com.android.internal.telephony.Phone; |
| import com.android.internal.telephony.PhoneConfigurationManager; |
| import com.android.internal.telephony.PhoneFactory; |
| import com.android.internal.telephony.RadioConfig; |
| import com.android.internal.telephony.SubscriptionController; |
| import com.android.internal.telephony.SubscriptionController.WatchedInt; |
| import com.android.internal.telephony.TelephonyIntents; |
| import com.android.internal.telephony.data.DataNetworkController.NetworkRequestList; |
| import com.android.internal.telephony.data.DataSettingsManager.DataSettingsManagerCallback; |
| import com.android.internal.telephony.metrics.TelephonyMetrics; |
| import com.android.internal.telephony.nano.TelephonyProto.TelephonyEvent; |
| import com.android.internal.telephony.nano.TelephonyProto.TelephonyEvent.DataSwitch; |
| import com.android.internal.telephony.nano.TelephonyProto.TelephonyEvent.OnDemandDataSwitch; |
| import com.android.internal.telephony.subscription.SubscriptionInfoInternal; |
| import com.android.internal.telephony.subscription.SubscriptionManagerService; |
| import com.android.internal.telephony.util.NotificationChannelController; |
| import com.android.internal.util.IndentingPrintWriter; |
| import com.android.telephony.Rlog; |
| |
| import java.io.FileDescriptor; |
| import java.io.PrintWriter; |
| import java.util.ArrayList; |
| import java.util.Calendar; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.concurrent.CompletableFuture; |
| |
| /** |
| * Utility singleton to monitor subscription changes and incoming NetworkRequests |
| * and determine which phone/phones are active. |
| * |
| * Manages the ALLOW_DATA calls to modems and notifies phones about changes to |
| * the active phones. Note we don't wait for data attach (which may not happen anyway). |
| */ |
| public class PhoneSwitcher extends Handler { |
| private static final String LOG_TAG = "PhoneSwitcher"; |
| protected static final boolean VDBG = Rlog.isLoggable(LOG_TAG, Log.VERBOSE); |
| |
| /** Fragment "key" argument passed thru {@link #SETTINGS_EXTRA_SHOW_FRAGMENT_ARGUMENTS} */ |
| private static final String SETTINGS_EXTRA_FRAGMENT_ARG_KEY = ":settings:fragment_args_key"; |
| /** |
| * When starting this activity, this extra can also be specified to supply a Bundle of arguments |
| * to pass to that fragment when it is instantiated during the initial creation of the activity. |
| */ |
| private static final String SETTINGS_EXTRA_SHOW_FRAGMENT_ARGUMENTS = |
| ":settings:show_fragment_args"; |
| /** The res Id of the auto data switch fragment in settings. **/ |
| private static final String AUTO_DATA_SWITCH_SETTING_R_ID = "auto_data_switch"; |
| /** Notification tag **/ |
| private static final String AUTO_DATA_SWITCH_NOTIFICATION_TAG = "auto_data_switch"; |
| /** Notification ID **/ |
| private static final int AUTO_DATA_SWITCH_NOTIFICATION_ID = 1; |
| |
| private static final int MODEM_COMMAND_RETRY_PERIOD_MS = 5000; |
| // After the emergency call ends, wait for a few seconds to see if we enter ECBM before starting |
| // the countdown to remove the emergency DDS override. |
| @VisibleForTesting |
| // not final for testing. |
| public static int ECBM_DEFAULT_DATA_SWITCH_BASE_TIME_MS = 5000; |
| // Wait for a few seconds after the override request comes in to receive the outgoing call |
| // event. If it does not happen before the timeout specified, cancel the override. |
| @VisibleForTesting |
| public static int DEFAULT_DATA_OVERRIDE_TIMEOUT_MS = 5000; |
| |
| // If there are no subscriptions in a device, then the phone to be used for emergency should |
| // always be the "first" phone. |
| private static final int DEFAULT_EMERGENCY_PHONE_ID = 0; |
| |
| /** |
| * Container for an ongoing request to override the DDS in the context of an ongoing emergency |
| * call to allow for carrier specific operations, such as provide SUPL updates during or after |
| * the emergency call, since some modems do not support these operations on the non DDS. |
| */ |
| private static final class EmergencyOverrideRequest { |
| /* The Phone ID that the DDS should be set to. */ |
| int mPhoneId = INVALID_PHONE_INDEX; |
| /* The time after the emergency call ends that the DDS should be overridden for. */ |
| int mGnssOverrideTimeMs = -1; |
| /* A callback to the requester notifying them if the initial call to the modem to override |
| * the DDS was successful. |
| */ |
| CompletableFuture<Boolean> mOverrideCompleteFuture; |
| /* In the special case that the device goes into emergency callback mode after the emergency |
| * call ends, keep the override until ECM finishes and then start the mGnssOverrideTimeMs |
| * timer to leave DDS override. |
| */ |
| boolean mRequiresEcmFinish = false; |
| |
| /* |
| * Keeps track of whether or not this request has already serviced the outgoing emergency |
| * call. Once finished, do not delay for any other calls. |
| */ |
| boolean mPendingOriginatingCall = true; |
| |
| /** |
| * @return true if there is a pending override complete callback. |
| */ |
| boolean isCallbackAvailable() { |
| return mOverrideCompleteFuture != null; |
| } |
| |
| /** |
| * Send the override complete callback the result of setting the DDS to the new value. |
| */ |
| void sendOverrideCompleteCallbackResultAndClear(boolean result) { |
| if (isCallbackAvailable()) { |
| mOverrideCompleteFuture.complete(result); |
| mOverrideCompleteFuture = null; |
| } |
| } |
| |
| |
| @Override |
| public String toString() { |
| return String.format("EmergencyOverrideRequest: [phoneId= %d, overrideMs= %d," |
| + " hasCallback= %b, ecmFinishStatus= %b]", mPhoneId, mGnssOverrideTimeMs, |
| isCallbackAvailable(), mRequiresEcmFinish); |
| } |
| } |
| |
| private final @NonNull NetworkRequestList mNetworkRequestList = new NetworkRequestList(); |
| protected final RegistrantList mActivePhoneRegistrants; |
| protected final SubscriptionController mSubscriptionController; |
| private final SubscriptionManagerService mSubscriptionManagerService; |
| protected final Context mContext; |
| private final LocalLog mLocalLog; |
| protected PhoneState[] mPhoneStates; |
| protected int[] mPhoneSubscriptions; |
| private boolean mIsRegisteredForImsRadioTechChange; |
| @VisibleForTesting |
| protected final CellularNetworkValidator mValidator; |
| private int mPendingSwitchSubId = INVALID_SUBSCRIPTION_ID; |
| /** The reason for the last time changing preferred data sub **/ |
| private int mLastSwitchPreferredDataReason = -1; |
| /** {@code true} if we've displayed the notification the first time auto switch occurs **/ |
| private boolean mDisplayedAutoSwitchNotification = false; |
| private boolean mPendingSwitchNeedValidation; |
| @VisibleForTesting |
| public final CellularNetworkValidator.ValidationCallback mValidationCallback = |
| new CellularNetworkValidator.ValidationCallback() { |
| @Override |
| public void onValidationDone(boolean validated, int subId) { |
| Message.obtain(PhoneSwitcher.this, |
| EVENT_NETWORK_VALIDATION_DONE, subId, validated ? 1 : 0).sendToTarget(); |
| } |
| |
| @Override |
| public void onNetworkAvailable(Network network, int subId) { |
| Message.obtain(PhoneSwitcher.this, |
| EVENT_NETWORK_AVAILABLE, subId, 0, network).sendToTarget(); |
| |
| } |
| }; |
| |
| @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) |
| // How many phones (correspondingly logical modems) are allowed for PS attach. This is used |
| // when we specifically use setDataAllowed to initiate on-demand PS(data) attach for each phone. |
| protected int mMaxDataAttachModemCount; |
| // Local cache of TelephonyManager#getActiveModemCount(). 1 if in single SIM mode, 2 if in dual |
| // SIM mode. |
| protected int mActiveModemCount; |
| protected static PhoneSwitcher sPhoneSwitcher = null; |
| |
| // Which primary (non-opportunistic) subscription is set as data subscription among all primary |
| // subscriptions. This value usually comes from user setting, and it's the subscription used for |
| // Internet data if mOpptDataSubId is not set. |
| protected int mPrimaryDataSubId = SubscriptionManager.INVALID_SUBSCRIPTION_ID; |
| |
| // The automatically suggested preferred data subId (by e.g. CBRS or auto data switch), a |
| // candidate for preferred data subId, which is eventually presided by |
| // updatePreferredDataPhoneId(). |
| // If CBRS/auto switch feature selects the primary data subId as the preferred data subId, |
| // its value will be DEFAULT_SUBSCRIPTION_ID. |
| private int mAutoSelectedDataSubId = SubscriptionManager.DEFAULT_SUBSCRIPTION_ID; |
| |
| /** The count of consecutive auto switch validation failure **/ |
| private int mAutoSwitchRetryFailedCount = 0; |
| |
| // The phone ID that has an active voice call. If set, and its mobile data setting is on, |
| // it will become the mPreferredDataPhoneId. |
| protected int mPhoneIdInVoiceCall = SubscriptionManager.INVALID_PHONE_INDEX; |
| |
| @VisibleForTesting |
| // It decides: |
| // 1. In modem layer, which modem is DDS (preferred to have data traffic on) |
| // 2. In TelephonyNetworkFactory, which subscription will apply default network requests, which |
| // are requests without specifying a subId. |
| // Corresponding phoneId after considering mOpptDataSubId, mPrimaryDataSubId and |
| // mPhoneIdInVoiceCall above. |
| protected int mPreferredDataPhoneId = SubscriptionManager.INVALID_PHONE_INDEX; |
| |
| // Subscription ID corresponds to mPreferredDataPhoneId. |
| protected WatchedInt mPreferredDataSubId = |
| new WatchedInt(SubscriptionManager.INVALID_SUBSCRIPTION_ID) { |
| @Override |
| public void set(int newValue) { |
| super.set(newValue); |
| SubscriptionManager.invalidateActiveDataSubIdCaches(); |
| } |
| }; |
| |
| // If non-null, An emergency call is about to be started, is ongoing, or has just ended and we |
| // are overriding the DDS. |
| // Internal state, should ONLY be accessed/modified inside of the handler. |
| private EmergencyOverrideRequest mEmergencyOverride; |
| |
| private ISetOpportunisticDataCallback mSetOpptSubCallback; |
| |
| /** Data config manager callback for updating device config. **/ |
| private final DataConfigManager.DataConfigManagerCallback mDataConfigManagerCallback = |
| new DataConfigManager.DataConfigManagerCallback(this::post) { |
| @Override |
| public void onDeviceConfigChanged() { |
| log("onDeviceConfigChanged"); |
| PhoneSwitcher.this.updateConfig(); |
| } |
| }; |
| |
| private static final int EVENT_PRIMARY_DATA_SUB_CHANGED = 101; |
| protected static final int EVENT_SUBSCRIPTION_CHANGED = 102; |
| private static final int EVENT_REQUEST_NETWORK = 103; |
| private static final int EVENT_RELEASE_NETWORK = 104; |
| // ECBM has started/ended. If we just ended an emergency call and mEmergencyOverride is not |
| // null, we will wait for EVENT_EMERGENCY_TOGGLE again with ECBM ending to send the message |
| // EVENT_REMOVE_DDS_EMERGENCY_OVERRIDE to remove the override after the mEmergencyOverride |
| // override timer ends. |
| private static final int EVENT_EMERGENCY_TOGGLE = 105; |
| private static final int EVENT_RADIO_CAPABILITY_CHANGED = 106; |
| private static final int EVENT_OPPT_DATA_SUB_CHANGED = 107; |
| private static final int EVENT_RADIO_ON = 108; |
| // A call has either started or ended. If an emergency ended and DDS is overridden using |
| // mEmergencyOverride, start the countdown to remove the override using the message |
| // EVENT_REMOVE_DDS_EMERGENCY_OVERRIDE. The only exception to this is if the device moves to |
| // ECBM, which is detected by EVENT_EMERGENCY_TOGGLE. |
| private static final int EVENT_PRECISE_CALL_STATE_CHANGED = 109; |
| private static final int EVENT_NETWORK_VALIDATION_DONE = 110; |
| private static final int EVENT_EVALUATE_AUTO_SWITCH = 111; |
| private static final int EVENT_MODEM_COMMAND_DONE = 112; |
| private static final int EVENT_MODEM_COMMAND_RETRY = 113; |
| private static final int EVENT_SERVICE_STATE_CHANGED = 114; |
| // An emergency call is about to be originated and requires the DDS to be overridden. |
| // Uses EVENT_PRECISE_CALL_STATE_CHANGED message to start countdown to finish override defined |
| // in mEmergencyOverride. If EVENT_PRECISE_CALL_STATE_CHANGED does not come in |
| // DEFAULT_DATA_OVERRIDE_TIMEOUT_MS milliseconds, then the override will be removed. |
| private static final int EVENT_OVERRIDE_DDS_FOR_EMERGENCY = 115; |
| // If it exists, remove the current mEmergencyOverride DDS override. |
| private static final int EVENT_REMOVE_DDS_EMERGENCY_OVERRIDE = 116; |
| // If it exists, remove the current mEmergencyOverride DDS override. |
| private static final int EVENT_MULTI_SIM_CONFIG_CHANGED = 117; |
| private static final int EVENT_NETWORK_AVAILABLE = 118; |
| private static final int EVENT_PROCESS_SIM_STATE_CHANGE = 119; |
| private static final int EVENT_IMS_RADIO_TECH_CHANGED = 120; |
| private static final int EVENT_MEETS_AUTO_DATA_SWITCH_STATE = 121; |
| |
| // List of events triggers re-evaluations |
| private static final String EVALUATION_REASON_RADIO_ON = "EVENT_RADIO_ON"; |
| |
| // Depending on version of IRadioConfig, we need to send either RIL_REQUEST_ALLOW_DATA if it's |
| // 1.0, or RIL_REQUEST_SET_PREFERRED_DATA if it's 1.1 or later. So internally mHalCommandToUse |
| // will be either HAL_COMMAND_ALLOW_DATA or HAL_COMMAND_ALLOW_DATA or HAL_COMMAND_UNKNOWN. |
| protected static final int HAL_COMMAND_UNKNOWN = 0; |
| protected static final int HAL_COMMAND_ALLOW_DATA = 1; |
| protected static final int HAL_COMMAND_PREFERRED_DATA = 2; |
| protected int mHalCommandToUse = HAL_COMMAND_UNKNOWN; |
| |
| protected RadioConfig mRadioConfig; |
| |
| private static final int MAX_LOCAL_LOG_LINES = 256; |
| |
| // Default timeout value of network validation in millisecond. |
| private final static int DEFAULT_VALIDATION_EXPIRATION_TIME = 2000; |
| |
| private ConnectivityManager mConnectivityManager; |
| private int mImsRegistrationTech = REGISTRATION_TECH_NONE; |
| |
| private List<Set<CommandException.Error>> mCurrentDdsSwitchFailure; |
| |
| /** |
| * Time threshold in ms to define a internet connection status to be stable(e.g. out of service, |
| * in service, wifi is the default active network.etc), while -1 indicates auto switch |
| * feature disabled. |
| */ |
| private long mAutoDataSwitchAvailabilityStabilityTimeThreshold = -1; |
| |
| /** |
| * The maximum number of retries when a validation for switching failed. |
| */ |
| private int mAutoDataSwitchValidationMaxRetry = |
| DataConfigManager.DEFAULT_AUTO_DATA_SWITCH_MAX_RETRY; |
| |
| /** Data settings manager callback. Key is the phone id. */ |
| private final @NonNull Map<Integer, DataSettingsManagerCallback> mDataSettingsManagerCallbacks = |
| new ArrayMap<>(); |
| |
| private class DefaultNetworkCallback extends ConnectivityManager.NetworkCallback { |
| public int mExpectedSubId = SubscriptionManager.INVALID_SUBSCRIPTION_ID; |
| public int mSwitchReason = TelephonyEvent.DataSwitch.Reason.DATA_SWITCH_REASON_UNKNOWN; |
| public boolean isDefaultNetworkOnCellular = false; |
| @Override |
| public void onCapabilitiesChanged(Network network, |
| NetworkCapabilities networkCapabilities) { |
| if (networkCapabilities.hasTransport(TRANSPORT_CELLULAR)) { |
| isDefaultNetworkOnCellular = true; |
| if (SubscriptionManager.isValidSubscriptionId(mExpectedSubId) |
| && mExpectedSubId == getSubIdFromNetworkSpecifier( |
| networkCapabilities.getNetworkSpecifier())) { |
| logDataSwitchEvent( |
| mExpectedSubId, |
| TelephonyEvent.EventState.EVENT_STATE_END, |
| mSwitchReason); |
| mExpectedSubId = SubscriptionManager.INVALID_SUBSCRIPTION_ID; |
| mSwitchReason = TelephonyEvent.DataSwitch.Reason.DATA_SWITCH_REASON_UNKNOWN; |
| } |
| } else { |
| if (isDefaultNetworkOnCellular) { |
| // non-cellular transport is active |
| isDefaultNetworkOnCellular = false; |
| log("default network is active on non cellular"); |
| evaluateIfAutoSwitchIsNeeded(); |
| } |
| } |
| } |
| |
| @Override |
| public void onLost(Network network) { |
| // try find an active sub to switch to |
| if (!hasMessages(EVENT_EVALUATE_AUTO_SWITCH)) { |
| sendEmptyMessage(EVENT_EVALUATE_AUTO_SWITCH); |
| } |
| } |
| } |
| |
| private RegistrationManager.RegistrationCallback mRegistrationCallback = |
| new RegistrationManager.RegistrationCallback() { |
| @Override |
| public void onRegistered(ImsRegistrationAttributes attributes) { |
| int imsRegistrationTech = attributes.getRegistrationTechnology(); |
| if (imsRegistrationTech != mImsRegistrationTech) { |
| mImsRegistrationTech = imsRegistrationTech; |
| sendMessage(obtainMessage(EVENT_IMS_RADIO_TECH_CHANGED)); |
| } |
| } |
| |
| @Override |
| public void onUnregistered(ImsReasonInfo info) { |
| if (mImsRegistrationTech != REGISTRATION_TECH_NONE) { |
| mImsRegistrationTech = REGISTRATION_TECH_NONE; |
| sendMessage(obtainMessage(EVENT_IMS_RADIO_TECH_CHANGED)); |
| } |
| } |
| }; |
| |
| private final DefaultNetworkCallback mDefaultNetworkCallback = new DefaultNetworkCallback(); |
| |
| /** |
| * Interface to get ImsRegistrationTech. It's a wrapper of ImsManager#getRegistrationTech, |
| * to make it mock-able in unittests. |
| */ |
| public interface ImsRegTechProvider { |
| /** Get IMS registration tech. */ |
| @ImsRegistrationImplBase.ImsRegistrationTech int get(Context context, int phoneId); |
| } |
| |
| @VisibleForTesting |
| public ImsRegTechProvider mImsRegTechProvider = |
| (context, phoneId) -> ImsManager.getInstance(context, phoneId).getRegistrationTech(); |
| |
| /** |
| * Method to get singleton instance. |
| */ |
| public static PhoneSwitcher getInstance() { |
| return sPhoneSwitcher; |
| } |
| |
| /** |
| * Method to create singleton instance. |
| */ |
| public static PhoneSwitcher make(int maxDataAttachModemCount, Context context, Looper looper) { |
| if (sPhoneSwitcher == null) { |
| sPhoneSwitcher = new PhoneSwitcher(maxDataAttachModemCount, context, looper); |
| SubscriptionManager.invalidateActiveDataSubIdCaches(); |
| } |
| |
| return sPhoneSwitcher; |
| } |
| |
| private boolean updatesIfPhoneInVoiceCallChanged() { |
| int oldPhoneIdInVoiceCall = mPhoneIdInVoiceCall; |
| // If there's no active call, the value will become INVALID_PHONE_INDEX |
| // and internet data will be switched back to system selected or user selected |
| // subscription. |
| mPhoneIdInVoiceCall = SubscriptionManager.INVALID_PHONE_INDEX; |
| for (Phone phone : PhoneFactory.getPhones()) { |
| if (isPhoneInVoiceCall(phone) || isPhoneInVoiceCall(phone.getImsPhone())) { |
| mPhoneIdInVoiceCall = phone.getPhoneId(); |
| break; |
| } |
| } |
| |
| if (mPhoneIdInVoiceCall != oldPhoneIdInVoiceCall) { |
| logl("isPhoneInVoiceCallChanged from phoneId " + oldPhoneIdInVoiceCall |
| + " to phoneId " + mPhoneIdInVoiceCall); |
| return true; |
| } else { |
| return false; |
| } |
| } |
| |
| private void registerForImsRadioTechChange(Context context, int phoneId) { |
| try { |
| ImsManager.getInstance(context, phoneId).addRegistrationCallback( |
| mRegistrationCallback, this::post); |
| mIsRegisteredForImsRadioTechChange = true; |
| } catch (ImsException imsException) { |
| mIsRegisteredForImsRadioTechChange = false; |
| } |
| } |
| |
| private void registerForImsRadioTechChange() { |
| // register for radio tech change to listen to radio tech handover. |
| if (!mIsRegisteredForImsRadioTechChange) { |
| for (int i = 0; i < mActiveModemCount; i++) { |
| registerForImsRadioTechChange(mContext, i); |
| } |
| } |
| } |
| |
| private void evaluateIfImmediateDataSwitchIsNeeded(String evaluationReason, int switchReason) { |
| if (onEvaluate(REQUESTS_UNCHANGED, evaluationReason)) { |
| logDataSwitchEvent(mPreferredDataSubId.get(), |
| TelephonyEvent.EventState.EVENT_STATE_START, |
| switchReason); |
| registerDefaultNetworkChangeCallback(mPreferredDataSubId.get(), |
| switchReason); |
| } |
| } |
| |
| @VisibleForTesting |
| public PhoneSwitcher(int maxActivePhones, Context context, Looper looper) { |
| super(looper); |
| mContext = context; |
| mActiveModemCount = getTm().getActiveModemCount(); |
| mPhoneSubscriptions = new int[mActiveModemCount]; |
| mPhoneStates = new PhoneState[mActiveModemCount]; |
| mMaxDataAttachModemCount = maxActivePhones; |
| mLocalLog = new LocalLog(MAX_LOCAL_LOG_LINES); |
| |
| mSubscriptionController = SubscriptionController.getInstance(); |
| mSubscriptionManagerService = SubscriptionManagerService.getInstance(); |
| mRadioConfig = RadioConfig.getInstance(); |
| mValidator = CellularNetworkValidator.getInstance(); |
| |
| mCurrentDdsSwitchFailure = new ArrayList<Set<CommandException.Error>>(); |
| IntentFilter filter = new IntentFilter(); |
| filter.addAction(TelephonyManager.ACTION_SIM_APPLICATION_STATE_CHANGED); |
| mContext.registerReceiver(mSimStateIntentReceiver, filter); |
| |
| mActivePhoneRegistrants = new RegistrantList(); |
| for (int phoneId = 0; phoneId < mActiveModemCount; phoneId++) { |
| mPhoneStates[phoneId] = new PhoneState(); |
| Phone phone = PhoneFactory.getPhone(phoneId); |
| if (phone != null) { |
| phone.registerForEmergencyCallToggle( |
| this, EVENT_EMERGENCY_TOGGLE, null); |
| // TODO (b/135566422): combine register for both GsmCdmaPhone and ImsPhone. |
| phone.registerForPreciseCallStateChanged( |
| this, EVENT_PRECISE_CALL_STATE_CHANGED, null); |
| if (phone.getImsPhone() != null) { |
| phone.getImsPhone().registerForPreciseCallStateChanged( |
| this, EVENT_PRECISE_CALL_STATE_CHANGED, null); |
| } |
| mDataSettingsManagerCallbacks.computeIfAbsent(phoneId, |
| v -> new DataSettingsManagerCallback(this::post) { |
| @Override |
| public void onDataEnabledChanged(boolean enabled, |
| @TelephonyManager.DataEnabledChangedReason int reason, |
| @NonNull String callingPackage) { |
| PhoneSwitcher.this.onDataEnabledChanged(); |
| }}); |
| phone.getDataSettingsManager().registerCallback( |
| mDataSettingsManagerCallbacks.get(phoneId)); |
| phone.getServiceStateTracker().registerForServiceStateChanged(this, |
| EVENT_SERVICE_STATE_CHANGED, phoneId); |
| registerForImsRadioTechChange(context, phoneId); |
| } |
| Set<CommandException.Error> ddsFailure = new HashSet<CommandException.Error>(); |
| mCurrentDdsSwitchFailure.add(ddsFailure); |
| } |
| |
| if (mActiveModemCount > 0) { |
| PhoneFactory.getPhone(0).mCi.registerForOn(this, EVENT_RADIO_ON, null); |
| } |
| |
| TelephonyRegistryManager telephonyRegistryManager = (TelephonyRegistryManager) |
| context.getSystemService(Context.TELEPHONY_REGISTRY_SERVICE); |
| telephonyRegistryManager.addOnSubscriptionsChangedListener( |
| mSubscriptionsChangedListener, mSubscriptionsChangedListener.getHandlerExecutor()); |
| |
| mConnectivityManager = |
| (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); |
| |
| mContext.registerReceiver(mDefaultDataChangedReceiver, |
| new IntentFilter(TelephonyIntents.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED)); |
| |
| PhoneConfigurationManager.registerForMultiSimConfigChange( |
| this, EVENT_MULTI_SIM_CONFIG_CHANGED, null); |
| |
| mConnectivityManager.registerDefaultNetworkCallback(mDefaultNetworkCallback, this); |
| |
| final NetworkCapabilities.Builder builder = new NetworkCapabilities.Builder() |
| .addTransportType(TRANSPORT_CELLULAR) |
| .addCapability(NetworkCapabilities.NET_CAPABILITY_MMS) |
| .addCapability(NetworkCapabilities.NET_CAPABILITY_SUPL) |
| .addCapability(NetworkCapabilities.NET_CAPABILITY_DUN) |
| .addCapability(NetworkCapabilities.NET_CAPABILITY_FOTA) |
| .addCapability(NetworkCapabilities.NET_CAPABILITY_IMS) |
| .addCapability(NetworkCapabilities.NET_CAPABILITY_CBS) |
| .addCapability(NetworkCapabilities.NET_CAPABILITY_IA) |
| .addCapability(NetworkCapabilities.NET_CAPABILITY_RCS) |
| .addCapability(NetworkCapabilities.NET_CAPABILITY_MMTEL) |
| .addCapability(NetworkCapabilities.NET_CAPABILITY_XCAP) |
| .addCapability(NetworkCapabilities.NET_CAPABILITY_ENTERPRISE) |
| .addCapability(NetworkCapabilities.NET_CAPABILITY_EIMS) |
| .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED) |
| .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) |
| .addCapability(NetworkCapabilities.NET_CAPABILITY_MCX) |
| .addCapability(NetworkCapabilities.NET_CAPABILITY_PRIORITIZE_LATENCY) |
| .addCapability(NetworkCapabilities.NET_CAPABILITY_PRIORITIZE_BANDWIDTH) |
| .addEnterpriseId(NetworkCapabilities.NET_ENTERPRISE_ID_1) |
| .addEnterpriseId(NetworkCapabilities.NET_ENTERPRISE_ID_2) |
| .addEnterpriseId(NetworkCapabilities.NET_ENTERPRISE_ID_3) |
| .addEnterpriseId(NetworkCapabilities.NET_ENTERPRISE_ID_4) |
| .addEnterpriseId(NetworkCapabilities.NET_ENTERPRISE_ID_5) |
| .setNetworkSpecifier(new MatchAllNetworkSpecifier()); |
| |
| NetworkFactory networkFactory = new PhoneSwitcherNetworkRequestListener(looper, context, |
| builder.build(), this); |
| // we want to see all requests |
| networkFactory.registerIgnoringScore(); |
| |
| updateHalCommandToUse(); |
| |
| logl("PhoneSwitcher started"); |
| } |
| |
| private final BroadcastReceiver mDefaultDataChangedReceiver = new BroadcastReceiver() { |
| @Override |
| public void onReceive(Context context, Intent intent) { |
| Message msg = PhoneSwitcher.this.obtainMessage(EVENT_PRIMARY_DATA_SUB_CHANGED); |
| msg.sendToTarget(); |
| } |
| }; |
| |
| private BroadcastReceiver mSimStateIntentReceiver = new BroadcastReceiver() { |
| @Override |
| public void onReceive(Context context, Intent intent) { |
| String action = intent.getAction(); |
| if (action.equals(TelephonyManager.ACTION_SIM_APPLICATION_STATE_CHANGED)) { |
| int state = intent.getIntExtra(TelephonyManager.EXTRA_SIM_STATE, |
| TelephonyManager.SIM_STATE_UNKNOWN); |
| int slotIndex = intent.getIntExtra(SubscriptionManager.EXTRA_SLOT_INDEX, |
| SubscriptionManager.INVALID_SIM_SLOT_INDEX); |
| logl("mSimStateIntentReceiver: slotIndex = " + slotIndex + " state = " + state); |
| obtainMessage(EVENT_PROCESS_SIM_STATE_CHANGE, slotIndex, state).sendToTarget(); |
| } |
| } |
| }; |
| |
| private boolean isSimApplicationReady(int slotIndex) { |
| if (!SubscriptionManager.isValidSlotIndex(slotIndex)) { |
| return false; |
| } |
| |
| SubscriptionInfo info; |
| if (PhoneFactory.isSubscriptionManagerServiceEnabled()) { |
| info = mSubscriptionManagerService |
| .getActiveSubscriptionInfoForSimSlotIndex(slotIndex, |
| mContext.getOpPackageName(), mContext.getAttributionTag()); |
| } else { |
| info = mSubscriptionController |
| .getActiveSubscriptionInfoForSimSlotIndex(slotIndex, |
| mContext.getOpPackageName(), null); |
| } |
| boolean uiccAppsEnabled = info != null && info.areUiccApplicationsEnabled(); |
| |
| IccCard iccCard = PhoneFactory.getPhone(slotIndex).getIccCard(); |
| if (!iccCard.isEmptyProfile() && uiccAppsEnabled) { |
| logl("isSimApplicationReady: SIM is ready for slotIndex: " + slotIndex); |
| return true; |
| } else { |
| return false; |
| } |
| } |
| |
| private final SubscriptionManager.OnSubscriptionsChangedListener mSubscriptionsChangedListener = |
| new SubscriptionManager.OnSubscriptionsChangedListener() { |
| @Override |
| public void onSubscriptionsChanged() { |
| Message msg = PhoneSwitcher.this.obtainMessage(EVENT_SUBSCRIPTION_CHANGED); |
| msg.sendToTarget(); |
| } |
| }; |
| |
| @Override |
| public void handleMessage(Message msg) { |
| switch (msg.what) { |
| case EVENT_SUBSCRIPTION_CHANGED: { |
| onEvaluate(REQUESTS_UNCHANGED, "subChanged"); |
| break; |
| } |
| case EVENT_SERVICE_STATE_CHANGED: { |
| AsyncResult ar = (AsyncResult) msg.obj; |
| final int phoneId = (int) ar.userObj; |
| onServiceStateChanged(phoneId); |
| break; |
| } |
| case EVENT_MEETS_AUTO_DATA_SWITCH_STATE: { |
| final int targetSubId = msg.arg1; |
| final boolean needValidation = (boolean) msg.obj; |
| validate(targetSubId, needValidation, |
| DataSwitch.Reason.DATA_SWITCH_REASON_AUTO, null); |
| break; |
| } |
| case EVENT_PRIMARY_DATA_SUB_CHANGED: { |
| evaluateIfImmediateDataSwitchIsNeeded("primary data sub changed", |
| DataSwitch.Reason.DATA_SWITCH_REASON_MANUAL); |
| break; |
| } |
| case EVENT_REQUEST_NETWORK: { |
| onRequestNetwork((NetworkRequest)msg.obj); |
| break; |
| } |
| case EVENT_RELEASE_NETWORK: { |
| onReleaseNetwork((NetworkRequest)msg.obj); |
| break; |
| } |
| case EVENT_EMERGENCY_TOGGLE: { |
| boolean isInEcm = isInEmergencyCallbackMode(); |
| if (mEmergencyOverride != null) { |
| logl("Emergency override - ecbm status = " + isInEcm); |
| if (isInEcm) { |
| // The device has gone into ECBM. Wait until it's out. |
| removeMessages(EVENT_REMOVE_DDS_EMERGENCY_OVERRIDE); |
| mEmergencyOverride.mRequiresEcmFinish = true; |
| } else if (mEmergencyOverride.mRequiresEcmFinish) { |
| // we have exited ECM! Start the timer to exit DDS override. |
| Message msg2 = obtainMessage(EVENT_REMOVE_DDS_EMERGENCY_OVERRIDE); |
| sendMessageDelayed(msg2, mEmergencyOverride.mGnssOverrideTimeMs); |
| } |
| } |
| onEvaluate(REQUESTS_CHANGED, "emergencyToggle"); |
| break; |
| } |
| case EVENT_EVALUATE_AUTO_SWITCH: |
| evaluateIfAutoSwitchIsNeeded(); |
| break; |
| case EVENT_RADIO_CAPABILITY_CHANGED: { |
| final int phoneId = msg.arg1; |
| sendRilCommands(phoneId); |
| break; |
| } |
| case EVENT_OPPT_DATA_SUB_CHANGED: { |
| int subId = msg.arg1; |
| boolean needValidation = (msg.arg2 == 1); |
| ISetOpportunisticDataCallback callback = |
| (ISetOpportunisticDataCallback) msg.obj; |
| setOpportunisticDataSubscription(subId, needValidation, callback); |
| break; |
| } |
| case EVENT_RADIO_ON: { |
| updateHalCommandToUse(); |
| onEvaluate(REQUESTS_UNCHANGED, EVALUATION_REASON_RADIO_ON); |
| break; |
| } |
| case EVENT_IMS_RADIO_TECH_CHANGED: { |
| // register for radio tech change to listen to radio tech handover in case previous |
| // attempt was not successful |
| registerForImsRadioTechChange(); |
| // if voice call state changes or in voice call didn't change |
| // but RAT changes(e.g. Iwlan -> cross sim), reevaluate for data switch. |
| if (updatesIfPhoneInVoiceCallChanged() || isAnyVoiceCallActiveOnDevice()) { |
| evaluateIfImmediateDataSwitchIsNeeded("Ims radio tech changed", |
| DataSwitch.Reason.DATA_SWITCH_REASON_IN_CALL); |
| } |
| break; |
| } |
| case EVENT_PRECISE_CALL_STATE_CHANGED: { |
| // register for radio tech change to listen to radio tech handover in case previous |
| // attempt was not successful |
| registerForImsRadioTechChange(); |
| |
| // If the phoneId in voice call didn't change, do nothing. |
| if (!updatesIfPhoneInVoiceCallChanged()) { |
| break; |
| } |
| |
| if (!isAnyVoiceCallActiveOnDevice()) { |
| for (int i = 0; i < mActiveModemCount; i++) { |
| if (mCurrentDdsSwitchFailure.get(i).contains( |
| CommandException.Error.OP_NOT_ALLOWED_DURING_VOICE_CALL) |
| && isPhoneIdValidForRetry(i)) { |
| sendRilCommands(i); |
| } |
| } |
| } |
| |
| // Only handle this event if we are currently waiting for the emergency call |
| // associated with the override request to start or end. |
| if (mEmergencyOverride != null && mEmergencyOverride.mPendingOriginatingCall) { |
| removeMessages(EVENT_REMOVE_DDS_EMERGENCY_OVERRIDE); |
| if (mPhoneIdInVoiceCall == SubscriptionManager.INVALID_PHONE_INDEX) { |
| // not in a call anymore. |
| Message msg2 = obtainMessage(EVENT_REMOVE_DDS_EMERGENCY_OVERRIDE); |
| sendMessageDelayed(msg2, mEmergencyOverride.mGnssOverrideTimeMs |
| + ECBM_DEFAULT_DATA_SWITCH_BASE_TIME_MS); |
| // Do not extend the emergency override by waiting for other calls to end. |
| // If it needs to be extended, a new request will come in and replace the |
| // current override. |
| mEmergencyOverride.mPendingOriginatingCall = false; |
| } |
| } |
| // Always update data modem via data during call code path, because |
| // mAutoSelectedDataSubId doesn't know about any data switch due to voice call |
| evaluateIfImmediateDataSwitchIsNeeded("precise call state changed", |
| DataSwitch.Reason.DATA_SWITCH_REASON_IN_CALL); |
| if (!isAnyVoiceCallActiveOnDevice()) { |
| // consider auto switch on hang up all voice call |
| evaluateIfAutoSwitchIsNeeded(); |
| } |
| break; |
| } |
| case EVENT_NETWORK_VALIDATION_DONE: { |
| int subId = msg.arg1; |
| boolean passed = (msg.arg2 == 1); |
| onValidationDone(subId, passed); |
| break; |
| } |
| case EVENT_NETWORK_AVAILABLE: { |
| int subId = msg.arg1; |
| Network network = (Network) msg.obj; |
| onNetworkAvailable(subId, network); |
| break; |
| } |
| case EVENT_MODEM_COMMAND_DONE: { |
| AsyncResult ar = (AsyncResult) msg.obj; |
| onDdsSwitchResponse(ar); |
| break; |
| } |
| case EVENT_MODEM_COMMAND_RETRY: { |
| int phoneId = (int) msg.obj; |
| if (isPhoneIdValidForRetry(phoneId)) { |
| logl("EVENT_MODEM_COMMAND_RETRY: resend modem command on phone " + phoneId); |
| sendRilCommands(phoneId); |
| } else { |
| logl("EVENT_MODEM_COMMAND_RETRY: skip retry as DDS sub changed"); |
| mCurrentDdsSwitchFailure.get(phoneId).clear(); |
| } |
| break; |
| } |
| case EVENT_OVERRIDE_DDS_FOR_EMERGENCY: { |
| EmergencyOverrideRequest req = (EmergencyOverrideRequest) msg.obj; |
| if (mEmergencyOverride != null) { |
| // If an override request comes in for a different phone ID than what is already |
| // being overridden, ignore. We should not try to switch DDS while already |
| // waiting for SUPL. |
| if (mEmergencyOverride.mPhoneId != req.mPhoneId) { |
| logl("emergency override requested for phone id " + req.mPhoneId + " when " |
| + "there is already an override in place for phone id " |
| + mEmergencyOverride.mPhoneId + ". Ignoring."); |
| if (req.isCallbackAvailable()) { |
| // Send failed result |
| req.mOverrideCompleteFuture.complete(false); |
| } |
| break; |
| } else { |
| if (mEmergencyOverride.isCallbackAvailable()) { |
| // Unblock any waiting overrides if a new request comes in before the |
| // previous one is processed. |
| mEmergencyOverride.mOverrideCompleteFuture.complete(false); |
| } |
| } |
| mEmergencyOverride = req; |
| } else { |
| mEmergencyOverride = req; |
| } |
| |
| logl("new emergency override - " + mEmergencyOverride); |
| // a new request has been created, remove any previous override complete scheduled. |
| removeMessages(EVENT_REMOVE_DDS_EMERGENCY_OVERRIDE); |
| Message msg2 = obtainMessage(EVENT_REMOVE_DDS_EMERGENCY_OVERRIDE); |
| // Make sure that if we never get an incall indication that we remove the override. |
| sendMessageDelayed(msg2, DEFAULT_DATA_OVERRIDE_TIMEOUT_MS); |
| // Wait for call to end and EVENT_PRECISE_CALL_STATE_CHANGED to be called, then |
| // start timer to remove DDS emergency override. |
| if (!onEvaluate(REQUESTS_UNCHANGED, "emer_override_dds")) { |
| // Nothing changed as a result of override, so no modem command was sent. Treat |
| // as success. |
| mEmergencyOverride.sendOverrideCompleteCallbackResultAndClear(true); |
| // Do not clear mEmergencyOverride here, as we still want to keep the override |
| // active for the time specified in case the user tries to switch default data. |
| } |
| break; |
| } |
| case EVENT_REMOVE_DDS_EMERGENCY_OVERRIDE: { |
| logl("Emergency override removed - " + mEmergencyOverride); |
| mEmergencyOverride = null; |
| onEvaluate(REQUESTS_UNCHANGED, "emer_rm_override_dds"); |
| break; |
| } |
| case EVENT_MULTI_SIM_CONFIG_CHANGED: { |
| int activeModemCount = (int) ((AsyncResult) msg.obj).result; |
| onMultiSimConfigChanged(activeModemCount); |
| break; |
| } |
| case EVENT_PROCESS_SIM_STATE_CHANGE: { |
| int slotIndex = (int) msg.arg1; |
| int simState = (int) msg.arg2; |
| |
| if (!SubscriptionManager.isValidSlotIndex(slotIndex)) { |
| logl("EVENT_PROCESS_SIM_STATE_CHANGE: skip processing due to invalid slotId: " |
| + slotIndex); |
| } else if (mCurrentDdsSwitchFailure.get(slotIndex).contains( |
| CommandException.Error.INVALID_SIM_STATE) |
| && (TelephonyManager.SIM_STATE_LOADED == simState) |
| && isSimApplicationReady(slotIndex)) { |
| sendRilCommands(slotIndex); |
| } |
| |
| registerConfigChange(); |
| break; |
| } |
| } |
| } |
| |
| /** |
| * Register for device config change on the primary data phone. |
| */ |
| private void registerConfigChange() { |
| Phone phone = getPhoneBySubId(mPrimaryDataSubId); |
| if (phone != null) { |
| DataConfigManager dataConfig = phone.getDataNetworkController().getDataConfigManager(); |
| dataConfig.registerCallback(mDataConfigManagerCallback); |
| updateConfig(); |
| sendEmptyMessage(EVENT_EVALUATE_AUTO_SWITCH); |
| } |
| } |
| |
| /** |
| * Update data config. |
| */ |
| private void updateConfig() { |
| Phone phone = getPhoneBySubId(mPrimaryDataSubId); |
| if (phone != null) { |
| DataConfigManager dataConfig = phone.getDataNetworkController().getDataConfigManager(); |
| mAutoDataSwitchAvailabilityStabilityTimeThreshold = |
| dataConfig.getAutoDataSwitchAvailabilityStabilityTimeThreshold(); |
| mAutoDataSwitchValidationMaxRetry = |
| dataConfig.getAutoDataSwitchValidationMaxRetry(); |
| } |
| } |
| |
| private synchronized void onMultiSimConfigChanged(int activeModemCount) { |
| // No change. |
| if (mActiveModemCount == activeModemCount) return; |
| int oldActiveModemCount = mActiveModemCount; |
| mActiveModemCount = activeModemCount; |
| |
| mPhoneSubscriptions = copyOf(mPhoneSubscriptions, mActiveModemCount); |
| mPhoneStates = copyOf(mPhoneStates, mActiveModemCount); |
| |
| // Dual SIM -> Single SIM switch. |
| for (int phoneId = oldActiveModemCount - 1; phoneId >= mActiveModemCount; phoneId--) { |
| mCurrentDdsSwitchFailure.remove(phoneId); |
| } |
| |
| // Single SIM -> Dual SIM switch. |
| for (int phoneId = oldActiveModemCount; phoneId < mActiveModemCount; phoneId++) { |
| mPhoneStates[phoneId] = new PhoneState(); |
| Phone phone = PhoneFactory.getPhone(phoneId); |
| if (phone == null) continue; |
| |
| phone.registerForEmergencyCallToggle(this, EVENT_EMERGENCY_TOGGLE, null); |
| // TODO (b/135566422): combine register for both GsmCdmaPhone and ImsPhone. |
| phone.registerForPreciseCallStateChanged(this, EVENT_PRECISE_CALL_STATE_CHANGED, null); |
| if (phone.getImsPhone() != null) { |
| phone.getImsPhone().registerForPreciseCallStateChanged( |
| this, EVENT_PRECISE_CALL_STATE_CHANGED, null); |
| } |
| |
| mDataSettingsManagerCallbacks.computeIfAbsent(phone.getPhoneId(), |
| v -> new DataSettingsManagerCallback(this::post) { |
| @Override |
| public void onDataEnabledChanged(boolean enabled, |
| @TelephonyManager.DataEnabledChangedReason int reason, |
| @NonNull String callingPackage) { |
| PhoneSwitcher.this.onDataEnabledChanged(); |
| } |
| }); |
| phone.getDataSettingsManager().registerCallback( |
| mDataSettingsManagerCallbacks.get(phone.getPhoneId())); |
| phone.getServiceStateTracker().registerForServiceStateChanged(this, |
| EVENT_SERVICE_STATE_CHANGED, phoneId); |
| |
| Set<CommandException.Error> ddsFailure = new HashSet<CommandException.Error>(); |
| mCurrentDdsSwitchFailure.add(ddsFailure); |
| registerForImsRadioTechChange(mContext, phoneId); |
| } |
| } |
| |
| /** |
| * Called when |
| * 1. user changed mobile data settings |
| * 2. OR user changed auto data switch feature |
| */ |
| private void onDataEnabledChanged() { |
| logl("user changed data related settings"); |
| if (isAnyVoiceCallActiveOnDevice()) { |
| // user changed data related settings during call, switch or turn off immediately |
| evaluateIfImmediateDataSwitchIsNeeded( |
| "user changed data settings during call", |
| DataSwitch.Reason.DATA_SWITCH_REASON_IN_CALL); |
| } else { |
| evaluateIfAutoSwitchIsNeeded(); |
| } |
| } |
| |
| private boolean isInEmergencyCallbackMode() { |
| for (Phone p : PhoneFactory.getPhones()) { |
| if (p == null) continue; |
| if (p.isInEcm()) return true; |
| Phone imsPhone = p.getImsPhone(); |
| if (imsPhone != null && imsPhone.isInEcm()) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| private static class PhoneSwitcherNetworkRequestListener extends NetworkFactory { |
| private final PhoneSwitcher mPhoneSwitcher; |
| public PhoneSwitcherNetworkRequestListener (Looper l, Context c, |
| NetworkCapabilities nc, PhoneSwitcher ps) { |
| super(l, c, "PhoneSwitcherNetworkRequstListener", nc); |
| mPhoneSwitcher = ps; |
| } |
| |
| @Override |
| protected void needNetworkFor(NetworkRequest networkRequest) { |
| if (VDBG) log("needNetworkFor " + networkRequest); |
| Message msg = mPhoneSwitcher.obtainMessage(EVENT_REQUEST_NETWORK); |
| msg.obj = networkRequest; |
| msg.sendToTarget(); |
| } |
| |
| @Override |
| protected void releaseNetworkFor(NetworkRequest networkRequest) { |
| if (VDBG) log("releaseNetworkFor " + networkRequest); |
| Message msg = mPhoneSwitcher.obtainMessage(EVENT_RELEASE_NETWORK); |
| msg.obj = networkRequest; |
| msg.sendToTarget(); |
| } |
| } |
| |
| private void onRequestNetwork(NetworkRequest networkRequest) { |
| TelephonyNetworkRequest telephonyNetworkRequest = new TelephonyNetworkRequest( |
| networkRequest, PhoneFactory.getDefaultPhone()); |
| if (!mNetworkRequestList.contains(telephonyNetworkRequest)) { |
| mNetworkRequestList.add(telephonyNetworkRequest); |
| onEvaluate(REQUESTS_CHANGED, "netRequest"); |
| } |
| } |
| |
| private void onReleaseNetwork(NetworkRequest networkRequest) { |
| TelephonyNetworkRequest telephonyNetworkRequest = new TelephonyNetworkRequest( |
| networkRequest, PhoneFactory.getDefaultPhone()); |
| if (mNetworkRequestList.remove(telephonyNetworkRequest)) { |
| onEvaluate(REQUESTS_CHANGED, "netReleased"); |
| collectReleaseNetworkMetrics(networkRequest); |
| } |
| } |
| |
| private void registerDefaultNetworkChangeCallback(int expectedSubId, int reason) { |
| mDefaultNetworkCallback.mExpectedSubId = expectedSubId; |
| mDefaultNetworkCallback.mSwitchReason = reason; |
| } |
| |
| private void collectRequestNetworkMetrics(NetworkRequest networkRequest) { |
| // Request network for MMS will temporary disable the network on default data subscription, |
| // this only happen on multi-sim device. |
| if (mActiveModemCount > 1 && networkRequest.hasCapability( |
| NetworkCapabilities.NET_CAPABILITY_MMS)) { |
| OnDemandDataSwitch onDemandDataSwitch = new OnDemandDataSwitch(); |
| onDemandDataSwitch.apn = TelephonyEvent.ApnType.APN_TYPE_MMS; |
| onDemandDataSwitch.state = TelephonyEvent.EventState.EVENT_STATE_START; |
| TelephonyMetrics.getInstance().writeOnDemandDataSwitch(onDemandDataSwitch); |
| } |
| } |
| |
| private void collectReleaseNetworkMetrics(NetworkRequest networkRequest) { |
| // Release network for MMS will recover the network on default data subscription, this only |
| // happen on multi-sim device. |
| if (mActiveModemCount > 1 && networkRequest.hasCapability( |
| NetworkCapabilities.NET_CAPABILITY_MMS)) { |
| OnDemandDataSwitch onDemandDataSwitch = new OnDemandDataSwitch(); |
| onDemandDataSwitch.apn = TelephonyEvent.ApnType.APN_TYPE_MMS; |
| onDemandDataSwitch.state = TelephonyEvent.EventState.EVENT_STATE_END; |
| TelephonyMetrics.getInstance().writeOnDemandDataSwitch(onDemandDataSwitch); |
| } |
| } |
| |
| /** |
| * Called when service state changed. |
| */ |
| private void onServiceStateChanged(int phoneId) { |
| Phone phone = findPhoneById(phoneId); |
| if (phone != null) { |
| int newRegState = phone.getServiceState() |
| .getNetworkRegistrationInfo( |
| NetworkRegistrationInfo.DOMAIN_PS, |
| AccessNetworkConstants.TRANSPORT_TYPE_WWAN) |
| .getRegistrationState(); |
| if (newRegState != mPhoneStates[phoneId].dataRegState) { |
| mPhoneStates[phoneId].dataRegState = newRegState; |
| logl("onServiceStateChanged: phoneId:" + phoneId + " dataReg-> " |
| + NetworkRegistrationInfo.registrationStateToString(newRegState)); |
| if (!hasMessages(EVENT_EVALUATE_AUTO_SWITCH)) { |
| sendEmptyMessage(EVENT_EVALUATE_AUTO_SWITCH); |
| } |
| } |
| } |
| } |
| |
| /** |
| * Evaluate if auto switch is suitable at the moment. |
| */ |
| private void evaluateIfAutoSwitchIsNeeded() { |
| // auto data switch feature is disabled from server |
| if (mAutoDataSwitchAvailabilityStabilityTimeThreshold < 0) return; |
| // check is valid DSDS |
| if (PhoneFactory.isSubscriptionManagerServiceEnabled()) { |
| if (!isActiveSubId(mPrimaryDataSubId) || mSubscriptionManagerService |
| .getActiveSubIdList(true).length <= 1) { |
| return; |
| } |
| } else { |
| if (!isActiveSubId(mPrimaryDataSubId) |
| || mSubscriptionController.getActiveSubIdList(true).length <= 1) { |
| return; |
| } |
| } |
| |
| Phone primaryDataPhone = getPhoneBySubId(mPrimaryDataSubId); |
| if (primaryDataPhone == null) { |
| loge("evaluateIfAutoSwitchIsNeeded: cannot find primary data phone. subId=" |
| + mPrimaryDataSubId); |
| return; |
| } |
| |
| int primaryPhoneId = primaryDataPhone.getPhoneId(); |
| log("evaluateIfAutoSwitchIsNeeded: primaryPhoneId: " + primaryPhoneId |
| + " preferredPhoneId: " + mPreferredDataPhoneId); |
| Phone secondaryDataPhone; |
| |
| if (mPreferredDataPhoneId == primaryPhoneId) { |
| // on primary data sub |
| |
| int candidateSubId = getAutoSwitchTargetSubIdIfExists(); |
| if (candidateSubId != INVALID_SUBSCRIPTION_ID) { |
| startAutoDataSwitchStabilityCheck(candidateSubId, true); |
| } else { |
| cancelPendingAutoDataSwitch(); |
| } |
| } else if ((secondaryDataPhone = findPhoneById(mPreferredDataPhoneId)) != null) { |
| // on secondary data sub |
| |
| if (!primaryDataPhone.isUserDataEnabled() |
| || !secondaryDataPhone.isDataAllowed()) { |
| // immediately switch back if user setting changes |
| mAutoSelectedDataSubId = DEFAULT_SUBSCRIPTION_ID; |
| evaluateIfImmediateDataSwitchIsNeeded("User disabled data settings", |
| DataSwitch.Reason.DATA_SWITCH_REASON_MANUAL); |
| return; |
| } |
| |
| NetworkCapabilities defaultNetworkCapabilities = mConnectivityManager |
| .getNetworkCapabilities(mConnectivityManager.getActiveNetwork()); |
| if (defaultNetworkCapabilities != null && !defaultNetworkCapabilities |
| .hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) { |
| log("evaluateIfAutoSwitchIsNeeded: " |
| + "Default network is active on non-cellular transport"); |
| startAutoDataSwitchStabilityCheck(DEFAULT_SUBSCRIPTION_ID, false); |
| return; |
| } |
| |
| if (mPhoneStates[secondaryDataPhone.getPhoneId()].dataRegState |
| != NetworkRegistrationInfo.REGISTRATION_STATE_HOME) { |
| // secondary phone lost its HOME availability |
| startAutoDataSwitchStabilityCheck(DEFAULT_SUBSCRIPTION_ID, false); |
| return; |
| } |
| |
| if (isInService(mPhoneStates[primaryPhoneId])) { |
| // primary becomes available |
| startAutoDataSwitchStabilityCheck(DEFAULT_SUBSCRIPTION_ID, true); |
| return; |
| } |
| |
| // cancel any previous attempts of switching back to primary |
| cancelPendingAutoDataSwitch(); |
| } |
| } |
| |
| /** |
| * @param phoneState The phone state to check |
| * @return {@code true} if the phone state is considered in service. |
| */ |
| private boolean isInService(@NonNull PhoneState phoneState) { |
| return phoneState.dataRegState == NetworkRegistrationInfo.REGISTRATION_STATE_HOME |
| || phoneState.dataRegState == NetworkRegistrationInfo.REGISTRATION_STATE_ROAMING; |
| } |
| |
| /** |
| * Called when the current environment suits auto data switch. |
| * Start pre-switch validation if the current environment suits auto data switch for |
| * {@link #mAutoDataSwitchAvailabilityStabilityTimeThreshold} MS. |
| * @param targetSubId the target sub Id. |
| * @param needValidation {@code true} if validation is needed. |
| */ |
| private void startAutoDataSwitchStabilityCheck(int targetSubId, boolean needValidation) { |
| log("startAutoDataSwitchStabilityCheck: targetSubId=" + targetSubId |
| + " needValidation=" + needValidation); |
| if (!hasMessages(EVENT_MEETS_AUTO_DATA_SWITCH_STATE, needValidation)) { |
| sendMessageDelayed(obtainMessage(EVENT_MEETS_AUTO_DATA_SWITCH_STATE, targetSubId, |
| 0/*placeholder*/, |
| needValidation), |
| mAutoDataSwitchAvailabilityStabilityTimeThreshold); |
| } |
| } |
| |
| /** |
| * Cancel any auto switch attempts when the current environment is not suitable for auto switch. |
| */ |
| private void cancelPendingAutoDataSwitch() { |
| mAutoSwitchRetryFailedCount = 0; |
| removeMessages(EVENT_MEETS_AUTO_DATA_SWITCH_STATE); |
| if (mValidator.isValidating()) { |
| mValidator.stopValidation(); |
| |
| removeMessages(EVENT_NETWORK_VALIDATION_DONE); |
| removeMessages(EVENT_NETWORK_AVAILABLE); |
| mPendingSwitchSubId = INVALID_SUBSCRIPTION_ID; |
| mPendingSwitchNeedValidation = false; |
| } |
| } |
| |
| /** |
| * Called when consider switching from primary default data sub to another data sub. |
| * @return the target subId if a suitable candidate is found, otherwise return |
| * {@link SubscriptionManager#INVALID_SUBSCRIPTION_ID} |
| */ |
| private int getAutoSwitchTargetSubIdIfExists() { |
| Phone primaryDataPhone = getPhoneBySubId(mPrimaryDataSubId); |
| if (primaryDataPhone == null) { |
| log("getAutoSwitchTargetSubId: no sim loaded"); |
| return INVALID_SUBSCRIPTION_ID; |
| } |
| |
| int primaryPhoneId = primaryDataPhone.getPhoneId(); |
| |
| if (!primaryDataPhone.isUserDataEnabled()) { |
| log("getAutoSwitchTargetSubId: user disabled data"); |
| return INVALID_SUBSCRIPTION_ID; |
| } |
| |
| NetworkCapabilities defaultNetworkCapabilities = mConnectivityManager |
| .getNetworkCapabilities(mConnectivityManager.getActiveNetwork()); |
| if (defaultNetworkCapabilities != null && !defaultNetworkCapabilities |
| .hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) { |
| // Exists other active default transport |
| log("getAutoSwitchTargetSubId: Default network is active on non-cellular transport"); |
| return INVALID_SUBSCRIPTION_ID; |
| } |
| |
| // check whether primary and secondary signal status worth switching |
| if (isInService(mPhoneStates[primaryPhoneId])) { |
| log("getAutoSwitchTargetSubId: primary is in service"); |
| return INVALID_SUBSCRIPTION_ID; |
| } |
| for (int phoneId = 0; phoneId < mPhoneStates.length; phoneId++) { |
| if (phoneId != primaryPhoneId) { |
| // the alternative phone must have HOME availability |
| if (mPhoneStates[phoneId].dataRegState |
| == NetworkRegistrationInfo.REGISTRATION_STATE_HOME) { |
| log("getAutoSwitchTargetSubId: found phone " + phoneId + " in HOME service"); |
| Phone secondaryDataPhone = findPhoneById(phoneId); |
| if (secondaryDataPhone != null && // check auto switch feature enabled |
| secondaryDataPhone.isDataAllowed()) { |
| return secondaryDataPhone.getSubId(); |
| } |
| } |
| } |
| } |
| return INVALID_SUBSCRIPTION_ID; |
| } |
| |
| private TelephonyManager getTm() { |
| return (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE); |
| } |
| |
| protected static final boolean REQUESTS_CHANGED = true; |
| protected static final boolean REQUESTS_UNCHANGED = false; |
| /** |
| * Re-evaluate things. Do nothing if nothing's changed. |
| * |
| * Otherwise, go through the requests in priority order adding their phone until we've added up |
| * to the max allowed. Then go through shutting down phones that aren't in the active phone |
| * list. Finally, activate all phones in the active phone list. |
| * |
| * @return {@code True} if the default data subscription need to be changed. |
| */ |
| protected boolean onEvaluate(boolean requestsChanged, String reason) { |
| StringBuilder sb = new StringBuilder(reason); |
| |
| // If we use HAL_COMMAND_PREFERRED_DATA, |
| boolean diffDetected = mHalCommandToUse != HAL_COMMAND_PREFERRED_DATA && requestsChanged; |
| |
| // Check if user setting of default non-opportunistic data sub is changed. |
| int primaryDataSubId; |
| if (PhoneFactory.isSubscriptionManagerServiceEnabled()) { |
| primaryDataSubId = mSubscriptionManagerService.getDefaultDataSubId(); |
| } else { |
| primaryDataSubId = mSubscriptionController.getDefaultDataSubId(); |
| } |
| if (primaryDataSubId != mPrimaryDataSubId) { |
| sb.append(" mPrimaryDataSubId ").append(mPrimaryDataSubId).append("->") |
| .append(primaryDataSubId); |
| mPrimaryDataSubId = primaryDataSubId; |
| mLastSwitchPreferredDataReason = DataSwitch.Reason.DATA_SWITCH_REASON_MANUAL; |
| } |
| |
| // Check to see if there is any active subscription on any phone |
| boolean hasAnyActiveSubscription = false; |
| |
| // Check if phoneId to subId mapping is changed. |
| for (int i = 0; i < mActiveModemCount; i++) { |
| int sub = SubscriptionManager.getSubscriptionId(i); |
| |
| if (SubscriptionManager.isValidSubscriptionId(sub)) hasAnyActiveSubscription = true; |
| |
| if (sub != mPhoneSubscriptions[i]) { |
| sb.append(" phone[").append(i).append("] ").append(mPhoneSubscriptions[i]); |
| sb.append("->").append(sub); |
| if (mAutoSelectedDataSubId == mPhoneSubscriptions[i]) { |
| mAutoSelectedDataSubId = DEFAULT_SUBSCRIPTION_ID; |
| } |
| mPhoneSubscriptions[i] = sub; |
| diffDetected = true; |
| } |
| } |
| |
| if (!hasAnyActiveSubscription) { |
| transitionToEmergencyPhone(); |
| } else { |
| if (VDBG) log("Found an active subscription"); |
| } |
| |
| // Check if phoneId for preferred data is changed. |
| int oldPreferredDataPhoneId = mPreferredDataPhoneId; |
| |
| // Check if subId for preferred data is changed. |
| int oldPreferredDataSubId = mPreferredDataSubId.get(); |
| |
| // When there are no subscriptions, the preferred data phone ID is invalid, but we want |
| // to keep a valid phoneId for Emergency, so skip logic that updates for preferred data |
| // phone ID. Ideally there should be a single set of checks that evaluate the correct |
| // phoneId on a service-by-service basis (EIMS being one), but for now... just bypass |
| // this logic in the no-SIM case. |
| if (hasAnyActiveSubscription) updatePreferredDataPhoneId(); |
| |
| if (oldPreferredDataPhoneId != mPreferredDataPhoneId) { |
| sb.append(" preferred data phoneId ").append(oldPreferredDataPhoneId) |
| .append("->").append(mPreferredDataPhoneId); |
| diffDetected = true; |
| } else if (oldPreferredDataSubId != mPreferredDataSubId.get()) { |
| logl("SIM refresh, notify dds change"); |
| // Inform connectivity about the active data phone |
| notifyPreferredDataSubIdChanged(); |
| } |
| |
| // Always force DDS when radio on. This is to handle the corner cases that modem and android |
| // DDS are out of sync after APM, AP should force DDS when radio on. long term solution |
| // should be having API to query preferred data modem to detect the out-of-sync scenarios. |
| if (diffDetected || EVALUATION_REASON_RADIO_ON.equals(reason)) { |
| logl("evaluating due to " + sb); |
| if (mHalCommandToUse == HAL_COMMAND_PREFERRED_DATA) { |
| // With HAL_COMMAND_PREFERRED_DATA, all phones are assumed to allow PS attach. |
| // So marking all phone as active, and the phone with mPreferredDataPhoneId |
| // will send radioConfig command. |
| for (int phoneId = 0; phoneId < mActiveModemCount; phoneId++) { |
| mPhoneStates[phoneId].active = true; |
| } |
| sendRilCommands(mPreferredDataPhoneId); |
| } else { |
| List<Integer> newActivePhones = new ArrayList<Integer>(); |
| |
| /** |
| * If all phones can have PS attached, activate all. |
| * Otherwise, choose to activate phones according to requests. And |
| * if list is not full, add mPreferredDataPhoneId. |
| */ |
| if (mMaxDataAttachModemCount == mActiveModemCount) { |
| for (int i = 0; i < mMaxDataAttachModemCount; i++) { |
| newActivePhones.add(i); |
| } |
| } else { |
| // First try to activate phone in voice call. |
| if (mPhoneIdInVoiceCall != SubscriptionManager.INVALID_PHONE_INDEX) { |
| newActivePhones.add(mPhoneIdInVoiceCall); |
| } |
| |
| if (newActivePhones.size() < mMaxDataAttachModemCount) { |
| for (TelephonyNetworkRequest networkRequest : mNetworkRequestList) { |
| int phoneIdForRequest = phoneIdForRequest(networkRequest); |
| if (phoneIdForRequest == INVALID_PHONE_INDEX) continue; |
| if (newActivePhones.contains(phoneIdForRequest)) continue; |
| newActivePhones.add(phoneIdForRequest); |
| if (newActivePhones.size() >= mMaxDataAttachModemCount) break; |
| } |
| } |
| |
| if (newActivePhones.size() < mMaxDataAttachModemCount |
| && !newActivePhones.contains(mPreferredDataPhoneId) |
| && SubscriptionManager.isUsableSubIdValue(mPreferredDataPhoneId)) { |
| newActivePhones.add(mPreferredDataPhoneId); |
| } |
| } |
| |
| if (VDBG) { |
| log("mPrimaryDataSubId = " + mPrimaryDataSubId); |
| log("mAutoSelectedDataSubId = " + mAutoSelectedDataSubId); |
| for (int i = 0; i < mActiveModemCount; i++) { |
| log(" phone[" + i + "] using sub[" + mPhoneSubscriptions[i] + "]"); |
| } |
| log(" newActivePhones:"); |
| for (Integer i : newActivePhones) log(" " + i); |
| } |
| |
| for (int phoneId = 0; phoneId < mActiveModemCount; phoneId++) { |
| if (!newActivePhones.contains(phoneId)) { |
| deactivate(phoneId); |
| } |
| } |
| |
| // only activate phones up to the limit |
| for (int phoneId : newActivePhones) { |
| activate(phoneId); |
| } |
| } |
| } |
| return diffDetected; |
| } |
| |
| protected static class PhoneState { |
| public volatile boolean active = false; |
| public @NetworkRegistrationInfo.RegistrationState int dataRegState = |
| NetworkRegistrationInfo.REGISTRATION_STATE_NOT_REGISTERED_OR_SEARCHING; |
| public long lastRequested = 0; |
| } |
| |
| @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) |
| protected void activate(int phoneId) { |
| switchPhone(phoneId, true); |
| } |
| |
| @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) |
| protected void deactivate(int phoneId) { |
| switchPhone(phoneId, false); |
| } |
| |
| private void switchPhone(int phoneId, boolean active) { |
| PhoneState state = mPhoneStates[phoneId]; |
| if (state.active == active) return; |
| state.active = active; |
| logl((active ? "activate " : "deactivate ") + phoneId); |
| state.lastRequested = System.currentTimeMillis(); |
| sendRilCommands(phoneId); |
| } |
| |
| /** |
| * Used when the modem may have been rebooted and we |
| * want to resend setDataAllowed or setPreferredDataSubscriptionId |
| */ |
| public void onRadioCapChanged(int phoneId) { |
| if (!SubscriptionManager.isValidPhoneId(phoneId)) return; |
| Message msg = obtainMessage(EVENT_RADIO_CAPABILITY_CHANGED); |
| msg.arg1 = phoneId; |
| msg.sendToTarget(); |
| } |
| |
| /** |
| * Switch the Default data for the context of an outgoing emergency call. |
| * |
| * In some cases, we need to try to switch the Default Data subscription before placing the |
| * emergency call on DSDS devices. This includes the following situation: |
| * - The modem does not support processing GNSS SUPL requests on the non-default data |
| * subscription. For some carriers that do not provide a control plane fallback mechanism, the |
| * SUPL request will be dropped and we will not be able to get the user's location for the |
| * emergency call. In this case, we need to swap default data temporarily. |
| * @param phoneId The phone to use to evaluate whether or not the default data should be moved |
| * to this subscription. |
| * @param overrideTimeSec The amount of time to override the default data setting for after the |
| * emergency call ends. |
| * @param dataSwitchResult A {@link CompletableFuture} to be called with a {@link Boolean} |
| * result when the default data switch has either completed (true) or |
| * failed (false). |
| */ |
| public void overrideDefaultDataForEmergency(int phoneId, int overrideTimeSec, |
| CompletableFuture<Boolean> dataSwitchResult) { |
| if (!SubscriptionManager.isValidPhoneId(phoneId)) return; |
| Message msg = obtainMessage(EVENT_OVERRIDE_DDS_FOR_EMERGENCY); |
| EmergencyOverrideRequest request = new EmergencyOverrideRequest(); |
| request.mPhoneId = phoneId; |
| request.mGnssOverrideTimeMs = overrideTimeSec * 1000; |
| request.mOverrideCompleteFuture = dataSwitchResult; |
| msg.obj = request; |
| msg.sendToTarget(); |
| } |
| |
| protected void sendRilCommands(int phoneId) { |
| if (!SubscriptionManager.isValidPhoneId(phoneId)) { |
| logl("sendRilCommands: skip dds switch due to invalid phoneId=" + phoneId); |
| return; |
| } |
| |
| Message message = Message.obtain(this, EVENT_MODEM_COMMAND_DONE, phoneId); |
| if (mHalCommandToUse == HAL_COMMAND_ALLOW_DATA || mHalCommandToUse == HAL_COMMAND_UNKNOWN) { |
| // Skip ALLOW_DATA for single SIM device |
| if (mActiveModemCount > 1) { |
| PhoneFactory.getPhone(phoneId).mCi.setDataAllowed(isPhoneActive(phoneId), message); |
| } |
| } else if (phoneId == mPreferredDataPhoneId) { |
| // Only setPreferredDataModem if the phoneId equals to current mPreferredDataPhoneId |
| logl("sendRilCommands: setPreferredDataModem - phoneId: " + phoneId); |
| mRadioConfig.setPreferredDataModem(mPreferredDataPhoneId, message); |
| } |
| } |
| |
| private void onPhoneCapabilityChangedInternal(PhoneCapability capability) { |
| int newMaxDataAttachModemCount = TelephonyManager.getDefault() |
| .getNumberOfModemsWithSimultaneousDataConnections(); |
| if (mMaxDataAttachModemCount != newMaxDataAttachModemCount) { |
| mMaxDataAttachModemCount = newMaxDataAttachModemCount; |
| logl("Max active phones changed to " + mMaxDataAttachModemCount); |
| onEvaluate(REQUESTS_UNCHANGED, "phoneCfgChanged"); |
| } |
| } |
| |
| private int phoneIdForRequest(TelephonyNetworkRequest networkRequest) { |
| NetworkRequest netRequest = networkRequest.getNativeNetworkRequest(); |
| int subId = getSubIdFromNetworkSpecifier(netRequest.getNetworkSpecifier()); |
| |
| if (subId == DEFAULT_SUBSCRIPTION_ID) return mPreferredDataPhoneId; |
| if (subId == INVALID_SUBSCRIPTION_ID) return INVALID_PHONE_INDEX; |
| |
| int preferredDataSubId = (mPreferredDataPhoneId >= 0 |
| && mPreferredDataPhoneId < mActiveModemCount) |
| ? mPhoneSubscriptions[mPreferredDataPhoneId] : INVALID_SUBSCRIPTION_ID; |
| |
| // Currently we assume multi-SIM devices will only support one Internet PDN connection. So |
| // if Internet PDN is established on the non-preferred phone, it will interrupt |
| // Internet connection on the preferred phone. So we only accept Internet request with |
| // preferred data subscription or no specified subscription. |
| // One exception is, if it's restricted request (doesn't have NET_CAPABILITY_NOT_RESTRICTED) |
| // it will be accepted, which is used temporary data usage from system. |
| if (netRequest.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) |
| && netRequest.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED) |
| && subId != preferredDataSubId && subId != mValidator.getSubIdInValidation()) { |
| // Returning INVALID_PHONE_INDEX will result in netRequest not being handled. |
| return INVALID_PHONE_INDEX; |
| } |
| |
| // Try to find matching phone ID. If it doesn't exist, we'll end up returning INVALID. |
| int phoneId = INVALID_PHONE_INDEX; |
| for (int i = 0; i < mActiveModemCount; i++) { |
| if (mPhoneSubscriptions[i] == subId) { |
| phoneId = i; |
| break; |
| } |
| } |
| return phoneId; |
| } |
| |
| protected int getSubIdFromNetworkSpecifier(NetworkSpecifier specifier) { |
| if (specifier == null) { |
| return DEFAULT_SUBSCRIPTION_ID; |
| } |
| if (specifier instanceof TelephonyNetworkSpecifier) { |
| return ((TelephonyNetworkSpecifier) specifier).getSubscriptionId(); |
| } |
| return INVALID_SUBSCRIPTION_ID; |
| } |
| |
| private boolean isActiveSubId(int subId) { |
| if (PhoneFactory.isSubscriptionManagerServiceEnabled()) { |
| SubscriptionInfoInternal subInfo = mSubscriptionManagerService |
| .getSubscriptionInfoInternal(subId); |
| return subInfo != null && subInfo.isActive(); |
| } else { |
| return mSubscriptionController.isActiveSubId(subId); |
| } |
| } |
| |
| // This updates mPreferredDataPhoneId which decides which phone should handle default network |
| // requests. |
| protected void updatePreferredDataPhoneId() { |
| if (mEmergencyOverride != null && findPhoneById(mEmergencyOverride.mPhoneId) != null) { |
| // Override DDS for emergency even if user data is not enabled, since it is an |
| // emergency. |
| // TODO: Provide a notification to the user that metered data is currently being |
| // used during this period. |
| logl("updatePreferredDataPhoneId: preferred data overridden for emergency." |
| + " phoneId = " + mEmergencyOverride.mPhoneId); |
| mPreferredDataPhoneId = mEmergencyOverride.mPhoneId; |
| mLastSwitchPreferredDataReason = DataSwitch.Reason.DATA_SWITCH_REASON_UNKNOWN; |
| } else { |
| int imsRegTech = mImsRegTechProvider.get(mContext, mPhoneIdInVoiceCall); |
| if (isAnyVoiceCallActiveOnDevice() && imsRegTech != REGISTRATION_TECH_IWLAN) { |
| if (imsRegTech != REGISTRATION_TECH_CROSS_SIM) { |
| mPreferredDataPhoneId = shouldSwitchDataDueToInCall() |
| ? mPhoneIdInVoiceCall : getFallbackDataPhoneIdForInternetRequests(); |
| } else { |
| logl("IMS call on cross-SIM, skip switching data to phone " |
| + mPhoneIdInVoiceCall); |
| } |
| } else { |
| mPreferredDataPhoneId = getFallbackDataPhoneIdForInternetRequests(); |
| } |
| } |
| |
| mPreferredDataSubId.set(SubscriptionManager.getSubscriptionId(mPreferredDataPhoneId)); |
| } |
| |
| /** |
| * @return the default data phone Id (or auto selected phone Id in auto data switch/CBRS case) |
| */ |
| private int getFallbackDataPhoneIdForInternetRequests() { |
| int fallbackSubId = isActiveSubId(mAutoSelectedDataSubId) |
| ? mAutoSelectedDataSubId : mPrimaryDataSubId; |
| |
| if (SubscriptionManager.isUsableSubIdValue(fallbackSubId)) { |
| for (int phoneId = 0; phoneId < mActiveModemCount; phoneId++) { |
| if (mPhoneSubscriptions[phoneId] == fallbackSubId) { |
| return phoneId; |
| } |
| } |
| } |
| return SubscriptionManager.INVALID_PHONE_INDEX; |
| } |
| |
| /** |
| * If a phone is in call and user enabled its mobile data and auto data switch feature, we |
| * should switch internet connection to it because the other modem will lose data connection |
| * anyway. |
| * @return {@code true} if should switch data to the phone in voice call |
| */ |
| private boolean shouldSwitchDataDueToInCall() { |
| Phone voicePhone = findPhoneById(mPhoneIdInVoiceCall); |
| Phone defaultDataPhone = getPhoneBySubId(mPrimaryDataSubId); |
| return defaultDataPhone != null // check user enabled data |
| && defaultDataPhone.isUserDataEnabled() |
| && voicePhone != null // check user enabled voice during call feature |
| && voicePhone.isDataAllowed(); |
| } |
| |
| protected void transitionToEmergencyPhone() { |
| if (mActiveModemCount <= 0) { |
| logl("No phones: unable to reset preferred phone for emergency"); |
| return; |
| } |
| |
| if (mPreferredDataPhoneId != DEFAULT_EMERGENCY_PHONE_ID) { |
| logl("No active subscriptions: resetting preferred phone to 0 for emergency"); |
| mPreferredDataPhoneId = DEFAULT_EMERGENCY_PHONE_ID; |
| } |
| |
| if (mPreferredDataSubId.get() != INVALID_SUBSCRIPTION_ID) { |
| mPreferredDataSubId.set(INVALID_SUBSCRIPTION_ID); |
| notifyPreferredDataSubIdChanged(); |
| } |
| } |
| |
| private Phone getPhoneBySubId(int subId) { |
| if (PhoneFactory.isSubscriptionManagerServiceEnabled()) { |
| return findPhoneById(mSubscriptionManagerService.getPhoneId(subId)); |
| } else { |
| return findPhoneById(mSubscriptionController.getPhoneId(subId)); |
| } |
| } |
| |
| private Phone findPhoneById(final int phoneId) { |
| if (!SubscriptionManager.isValidPhoneId(phoneId)) { |
| return null; |
| } |
| return PhoneFactory.getPhone(phoneId); |
| } |
| |
| public synchronized boolean shouldApplyNetworkRequest( |
| TelephonyNetworkRequest networkRequest, int phoneId) { |
| if (!SubscriptionManager.isValidPhoneId(phoneId)) return false; |
| |
| int subId = SubscriptionManager.getSubscriptionId(phoneId); |
| |
| // In any case, if phone state is inactive, don't apply the network request. |
| if (!isPhoneActive(phoneId) || (subId == SubscriptionManager.INVALID_SUBSCRIPTION_ID |
| && !isEmergencyNetworkRequest(networkRequest))) { |
| return false; |
| } |
| |
| NetworkRequest netRequest = networkRequest.getNativeNetworkRequest(); |
| subId = getSubIdFromNetworkSpecifier(netRequest.getNetworkSpecifier()); |
| |
| //if this phone is an emergency networkRequest |
| //and subId is not specified that is invalid or default |
| if (isAnyVoiceCallActiveOnDevice() && isEmergencyNetworkRequest(networkRequest) |
| && (subId == DEFAULT_SUBSCRIPTION_ID || subId == INVALID_SUBSCRIPTION_ID)) { |
| return phoneId == mPhoneIdInVoiceCall; |
| } |
| |
| int phoneIdToHandle = phoneIdForRequest(networkRequest); |
| return phoneId == phoneIdToHandle; |
| } |
| |
| boolean isEmergencyNetworkRequest(TelephonyNetworkRequest networkRequest) { |
| return networkRequest.hasCapability(NetworkCapabilities.NET_CAPABILITY_EIMS); |
| } |
| |
| @VisibleForTesting |
| protected boolean isPhoneActive(int phoneId) { |
| if (phoneId >= mActiveModemCount) |
| return false; |
| return mPhoneStates[phoneId].active; |
| } |
| |
| /** |
| * If preferred phone changes, or phone activation status changes, registrants |
| * will be notified. |
| */ |
| public void registerForActivePhoneSwitch(Handler h, int what, Object o) { |
| Registrant r = new Registrant(h, what, o); |
| mActivePhoneRegistrants.add(r); |
| r.notifyRegistrant(); |
| } |
| |
| public void unregisterForActivePhoneSwitch(Handler h) { |
| mActivePhoneRegistrants.remove(h); |
| } |
| |
| /** |
| * Set opportunistic data subscription. It's an indication to switch Internet data to this |
| * subscription. It has to be an active subscription, and PhoneSwitcher will try to validate |
| * it first if needed. If subId is DEFAULT_SUBSCRIPTION_ID, it means we are un-setting |
| * opportunistic data sub and switch data back to primary sub. |
| * |
| * @param subId the opportunistic data subscription to switch to. pass DEFAULT_SUBSCRIPTION_ID |
| * if un-setting it. |
| * @param needValidation whether Telephony will wait until the network is validated by |
| * connectivity service before switching data to it. More details see |
| * {@link NetworkCapabilities#NET_CAPABILITY_VALIDATED}. |
| * @param callback Callback will be triggered once it succeeds or failed. |
| * Pass null if don't care about the result. |
| */ |
| private void setOpportunisticDataSubscription(int subId, boolean needValidation, |
| ISetOpportunisticDataCallback callback) { |
| validate(subId, needValidation, |
| DataSwitch.Reason.DATA_SWITCH_REASON_CBRS, callback); |
| } |
| |
| /** |
| * Try setup a new internet connection on the subId that's pending validation. If the validation |
| * succeeds, this subId will be evaluated for being the preferred data subId; If fails, nothing |
| * happens. |
| * Callback will be updated with the validation result. |
| * |
| * @param subId Sub Id that's pending switch, awaiting validation. |
| * @param needValidation {@code false} if switch to the subId even if validation fails. |
| * @param switchReason The switch reason for this validation |
| * @param callback Optional - specific for external opportunistic sub validation request. |
| */ |
| private void validate(int subId, boolean needValidation, int switchReason, |
| @Nullable ISetOpportunisticDataCallback callback) { |
| logl("Validate subId " + subId + " due to " + switchReasonToString(switchReason) |
| + " needValidation=" + needValidation); |
| int subIdToValidate = (subId == SubscriptionManager.DEFAULT_SUBSCRIPTION_ID) |
| ? mPrimaryDataSubId : subId; |
| if (!isActiveSubId(subIdToValidate)) { |
| logl("Can't switch data to inactive subId " + subIdToValidate); |
| if (subId == SubscriptionManager.DEFAULT_SUBSCRIPTION_ID) { |
| // the default data sub is not selected yet, store the intent of switching to |
| // default subId once it becomes available. |
| mAutoSelectedDataSubId = SubscriptionManager.DEFAULT_SUBSCRIPTION_ID; |
| } |
| sendSetOpptCallbackHelper(callback, SET_OPPORTUNISTIC_SUB_INACTIVE_SUBSCRIPTION); |
| return; |
| } |
| |
| if (mValidator.isValidating()) { |
| mValidator.stopValidation(); |
| sendSetOpptCallbackHelper(mSetOpptSubCallback, SET_OPPORTUNISTIC_SUB_VALIDATION_FAILED); |
| mSetOpptSubCallback = null; |
| } |
| |
| // Remove EVENT_NETWORK_VALIDATION_DONE. Don't handle validation result of previous subId |
| // if queued. |
| removeMessages(EVENT_NETWORK_VALIDATION_DONE); |
| removeMessages(EVENT_NETWORK_AVAILABLE); |
| |
| mPendingSwitchSubId = INVALID_SUBSCRIPTION_ID; |
| |
| if (subIdToValidate == mPreferredDataSubId.get()) { |
| sendSetOpptCallbackHelper(callback, SET_OPPORTUNISTIC_SUB_SUCCESS); |
| return; |
| } |
| |
| mLastSwitchPreferredDataReason = switchReason; |
| logDataSwitchEvent(subIdToValidate, |
| TelephonyEvent.EventState.EVENT_STATE_START, |
| switchReason); |
| registerDefaultNetworkChangeCallback(subIdToValidate, |
| switchReason); |
| |
| // If validation feature is not supported, set it directly. Otherwise, |
| // start validation on the subscription first. |
| if (!mValidator.isValidationFeatureSupported()) { |
| setAutoSelectedDataSubIdInternal(subIdToValidate); |
| sendSetOpptCallbackHelper(callback, SET_OPPORTUNISTIC_SUB_SUCCESS); |
| return; |
| } |
| |
| // Even if needValidation is false, we still send request to validator. The reason is we |
| // want to delay data switch until network is available on the target sub, to have a |
| // smoothest transition possible. |
| // In this case, even if data connection eventually failed in 2 seconds, we still |
| // confirm the switch, to maximally respect the request. |
| mPendingSwitchSubId = subIdToValidate; |
| mPendingSwitchNeedValidation = needValidation; |
| mSetOpptSubCallback = callback; |
| long validationTimeout = getValidationTimeout(subIdToValidate, needValidation); |
| mValidator.validate(subIdToValidate, validationTimeout, false, mValidationCallback); |
| } |
| |
| private long getValidationTimeout(int subId, boolean needValidation) { |
| if (!needValidation) return DEFAULT_VALIDATION_EXPIRATION_TIME; |
| |
| long validationTimeout = DEFAULT_VALIDATION_EXPIRATION_TIME; |
| CarrierConfigManager configManager = (CarrierConfigManager) |
| mContext.getSystemService(Context.CARRIER_CONFIG_SERVICE); |
| if (configManager != null) { |
| PersistableBundle b = configManager.getConfigForSubId(subId); |
| if (b != null) { |
| validationTimeout = b.getLong(KEY_DATA_SWITCH_VALIDATION_TIMEOUT_LONG); |
| } |
| } |
| return validationTimeout; |
| } |
| |
| private void sendSetOpptCallbackHelper(ISetOpportunisticDataCallback callback, int result) { |
| if (callback == null) return; |
| try { |
| callback.onComplete(result); |
| } catch (RemoteException exception) { |
| logl("RemoteException " + exception); |
| } |
| } |
| |
| /** |
| * Evaluate whether the specified sub Id can be set to be the preferred data sub Id. |
| * |
| * @param subId The subId that we tried to validate: could possibly be unvalidated if validation |
| * feature is not supported. |
| */ |
| private void setAutoSelectedDataSubIdInternal(int subId) { |
| if (mAutoSelectedDataSubId != subId) { |
| mAutoSelectedDataSubId = subId; |
| onEvaluate(REQUESTS_UNCHANGED, switchReasonToString(mLastSwitchPreferredDataReason)); |
| } |
| } |
| |
| private void confirmSwitch(int subId, boolean confirm) { |
| logl("confirmSwitch: subId " + subId + (confirm ? " confirmed." : " cancelled.")); |
| int resultForCallBack; |
| if (!isActiveSubId(subId)) { |
| logl("confirmSwitch: subId " + subId + " is no longer active"); |
| resultForCallBack = SET_OPPORTUNISTIC_SUB_INACTIVE_SUBSCRIPTION; |
| mAutoSwitchRetryFailedCount = 0; |
| } else if (!confirm) { |
| resultForCallBack = SET_OPPORTUNISTIC_SUB_VALIDATION_FAILED; |
| |
| // retry for auto data switch validation failure |
| if (mLastSwitchPreferredDataReason == DataSwitch.Reason.DATA_SWITCH_REASON_AUTO) { |
| scheduleAutoSwitchRetryEvaluation(); |
| mAutoSwitchRetryFailedCount++; |
| } |
| } else { |
| if (subId == mPrimaryDataSubId) { |
| setAutoSelectedDataSubIdInternal(DEFAULT_SUBSCRIPTION_ID); |
| } else { |
| setAutoSelectedDataSubIdInternal(subId); |
| } |
| resultForCallBack = SET_OPPORTUNISTIC_SUB_SUCCESS; |
| mAutoSwitchRetryFailedCount = 0; |
| } |
| |
| // Trigger callback if needed |
| sendSetOpptCallbackHelper(mSetOpptSubCallback, resultForCallBack); |
| mSetOpptSubCallback = null; |
| mPendingSwitchSubId = INVALID_SUBSCRIPTION_ID; |
| } |
| |
| /** |
| * Schedule auto data switch evaluation retry if haven't reached the max retry count. |
| */ |
| private void scheduleAutoSwitchRetryEvaluation() { |
| if (mAutoSwitchRetryFailedCount < mAutoDataSwitchValidationMaxRetry) { |
| if (!hasMessages(EVENT_EVALUATE_AUTO_SWITCH)) { |
| sendMessageDelayed(obtainMessage(EVENT_EVALUATE_AUTO_SWITCH), |
| mAutoDataSwitchAvailabilityStabilityTimeThreshold |
| << mAutoSwitchRetryFailedCount); |
| } |
| } else { |
| logl("scheduleAutoSwitchEvaluation: reached max auto switch retry count " |
| + mAutoDataSwitchValidationMaxRetry); |
| mAutoSwitchRetryFailedCount = 0; |
| } |
| } |
| |
| private void onNetworkAvailable(int subId, Network network) { |
| log("onNetworkAvailable: on subId " + subId); |
| // Do nothing unless pending switch matches target subId and it doesn't require |
| // validation pass. |
| if (mPendingSwitchSubId == INVALID_SUBSCRIPTION_ID || mPendingSwitchSubId != subId |
| || mPendingSwitchNeedValidation) { |
| return; |
| } |
| confirmSwitch(subId, true); |
| } |
| |
| private void onValidationDone(int subId, boolean passed) { |
| logl("onValidationDone: " + (passed ? "passed" : "failed") + " on subId " + subId); |
| if (mPendingSwitchSubId == INVALID_SUBSCRIPTION_ID || mPendingSwitchSubId != subId) return; |
| |
| // If validation failed and mPendingSwitch.mNeedValidation is false, we still confirm |
| // the switch. |
| confirmSwitch(subId, passed || !mPendingSwitchNeedValidation); |
| } |
| |
| /** |
| * Notify PhoneSwitcher to try to switch data to an opportunistic subscription. |
| * |
| * Set opportunistic data subscription. It's an indication to switch Internet data to this |
| * subscription. It has to be an active subscription, and PhoneSwitcher will try to validate |
| * it first if needed. If subId is DEFAULT_SUBSCRIPTION_ID, it means we are un-setting |
| * opportunistic data sub and switch data back to primary sub. |
| * |
| * @param subId the opportunistic data subscription to switch to. pass DEFAULT_SUBSCRIPTION_ID |
| * if un-setting it. |
| * @param needValidation whether Telephony will wait until the network is validated by |
| * connectivity service before switching data to it. More details see |
| * {@link NetworkCapabilities#NET_CAPABILITY_VALIDATED}. |
| * @param callback Callback will be triggered once it succeeds or failed. |
| * Pass null if don't care about the result. |
| */ |
| public void trySetOpportunisticDataSubscription(int subId, boolean needValidation, |
| ISetOpportunisticDataCallback callback) { |
| logl("Try set opportunistic data subscription to subId " + subId |
| + (needValidation ? " with " : " without ") + "validation"); |
| PhoneSwitcher.this.obtainMessage(EVENT_OPPT_DATA_SUB_CHANGED, |
| subId, needValidation ? 1 : 0, callback).sendToTarget(); |
| } |
| |
| protected boolean isPhoneInVoiceCall(Phone phone) { |
| if (phone == null) { |
| return false; |
| } |
| |
| // A phone in voice call might trigger data being switched to it. |
| return (!phone.getBackgroundCall().isIdle() |
| || !phone.getForegroundCall().isIdle()); |
| } |
| |
| private void updateHalCommandToUse() { |
| mHalCommandToUse = mRadioConfig.isSetPreferredDataCommandSupported() |
| ? HAL_COMMAND_PREFERRED_DATA : HAL_COMMAND_ALLOW_DATA; |
| } |
| |
| public int getPreferredDataPhoneId() { |
| return mPreferredDataPhoneId; |
| } |
| |
| /** |
| * Log debug messages and also log into the local log. |
| * @param l debug messages |
| */ |
| @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) |
| protected void logl(String l) { |
| log(l); |
| mLocalLog.log(l); |
| } |
| |
| /** |
| * Log debug messages. |
| * @param s debug messages |
| */ |
| private void log(@NonNull String s) { |
| Rlog.d(LOG_TAG, s); |
| } |
| |
| /** |
| * Log debug error messages. |
| * @param s debug messages |
| */ |
| private void loge(@NonNull String s) { |
| Rlog.e(LOG_TAG, s); |
| } |
| |
| |
| /** |
| * Convert data switch reason into string. |
| * |
| * @param reason The switch reason. |
| * @return The switch reason in string format. |
| */ |
| private static @NonNull String switchReasonToString(int reason) { |
| switch(reason) { |
| case TelephonyEvent.DataSwitch.Reason.DATA_SWITCH_REASON_UNKNOWN: |
| return "UNKNOWN"; |
| case TelephonyEvent.DataSwitch.Reason.DATA_SWITCH_REASON_MANUAL: |
| return "MANUAL"; |
| case TelephonyEvent.DataSwitch.Reason.DATA_SWITCH_REASON_IN_CALL: |
| return "IN_CALL"; |
| case TelephonyEvent.DataSwitch.Reason.DATA_SWITCH_REASON_CBRS: |
| return "CBRS"; |
| case TelephonyEvent.DataSwitch.Reason.DATA_SWITCH_REASON_AUTO: |
| return "AUTO"; |
| default: return "UNKNOWN(" + reason + ")"; |
| } |
| } |
| |
| /** |
| * Concert switching state to string |
| * |
| * @param state The switching state. |
| * @return The switching state in string format. |
| */ |
| private static @NonNull String switchStateToString(int state) { |
| switch(state) { |
| case TelephonyEvent.EventState.EVENT_STATE_UNKNOWN: |
| return "UNKNOWN"; |
| case TelephonyEvent.EventState.EVENT_STATE_START: |
| return "START"; |
| case TelephonyEvent.EventState.EVENT_STATE_END: |
| return "END"; |
| default: return "UNKNOWN(" + state + ")"; |
| } |
| } |
| |
| /** |
| * Log data switch event |
| * |
| * @param subId Subscription index. |
| * @param state The switching state. |
| * @param reason The switching reason. |
| */ |
| private void logDataSwitchEvent(int subId, int state, int reason) { |
| logl("Data switch event. subId=" + subId + ", state=" + switchStateToString(state) |
| + ", reason=" + switchReasonToString(reason)); |
| DataSwitch dataSwitch = new DataSwitch(); |
| dataSwitch.state = state; |
| dataSwitch.reason = reason; |
| TelephonyMetrics.getInstance().writeDataSwitch(subId, dataSwitch); |
| } |
| |
| /** |
| * See {@link PhoneStateListener#LISTEN_ACTIVE_DATA_SUBSCRIPTION_ID_CHANGE}. |
| */ |
| protected void notifyPreferredDataSubIdChanged() { |
| TelephonyRegistryManager telephonyRegistryManager = (TelephonyRegistryManager) mContext |
| .getSystemService(Context.TELEPHONY_REGISTRY_SERVICE); |
| logl("notifyPreferredDataSubIdChanged to " + mPreferredDataSubId.get()); |
| telephonyRegistryManager.notifyActiveDataSubIdChanged(mPreferredDataSubId.get()); |
| } |
| |
| /** |
| * @return The active data subscription id |
| */ |
| public int getActiveDataSubId() { |
| return mPreferredDataSubId.get(); |
| } |
| |
| /** |
| * @return The auto selected data subscription id. |
| */ |
| public int getAutoSelectedDataSubId() { |
| return mAutoSelectedDataSubId; |
| } |
| |
| // TODO (b/148396668): add an internal callback method to monitor phone capability change, |
| // and hook this call to that callback. |
| private void onPhoneCapabilityChanged(PhoneCapability capability) { |
| onPhoneCapabilityChangedInternal(capability); |
| } |
| |
| public void dump(FileDescriptor fd, PrintWriter writer, String[] args) { |
| final IndentingPrintWriter pw = new IndentingPrintWriter(writer, " "); |
| pw.println("PhoneSwitcher:"); |
| pw.increaseIndent(); |
| Calendar c = Calendar.getInstance(); |
| for (int i = 0; i < mActiveModemCount; i++) { |
| PhoneState ps = mPhoneStates[i]; |
| c.setTimeInMillis(ps.lastRequested); |
| pw.println("PhoneId(" + i + ") active=" + ps.active + ", dataRegState=" |
| + NetworkRegistrationInfo.registrationStateToString(ps.dataRegState) |
| + ", lastRequest=" |
| + (ps.lastRequested == 0 ? "never" : |
| String.format("%tm-%td %tH:%tM:%tS.%tL", c, c, c, c, c, c))); |
| } |
| pw.println("mPreferredDataPhoneId=" + mPreferredDataPhoneId); |
| pw.println("mPreferredDataSubId=" + mPreferredDataSubId.get()); |
| if (PhoneFactory.isSubscriptionManagerServiceEnabled()) { |
| pw.println("DefaultDataSubId=" + mSubscriptionManagerService.getDefaultDataSubId()); |
| pw.println("DefaultDataPhoneId=" + mSubscriptionManagerService.getPhoneId( |
| mSubscriptionManagerService.getDefaultDataSubId())); |
| } else { |
| pw.println("DefaultDataSubId=" + mSubscriptionController.getDefaultDataSubId()); |
| pw.println("DefaultDataPhoneId=" + mSubscriptionController.getPhoneId( |
| mSubscriptionController.getDefaultDataSubId())); |
| } |
| pw.println("mPrimaryDataSubId=" + mPrimaryDataSubId); |
| pw.println("mAutoSelectedDataSubId=" + mAutoSelectedDataSubId); |
| pw.println("mIsRegisteredForImsRadioTechChange=" + mIsRegisteredForImsRadioTechChange); |
| pw.println("mPendingSwitchNeedValidation=" + mPendingSwitchNeedValidation); |
| pw.println("mMaxDataAttachModemCount=" + mMaxDataAttachModemCount); |
| pw.println("mActiveModemCount=" + mActiveModemCount); |
| pw.println("mPhoneIdInVoiceCall=" + mPhoneIdInVoiceCall); |
| pw.println("mCurrentDdsSwitchFailure=" + mCurrentDdsSwitchFailure); |
| pw.println("mAutoDataSwitchAvailabilityStabilityTimeThreshold=" |
| + mAutoDataSwitchAvailabilityStabilityTimeThreshold); |
| pw.println("mAutoDataSwitchValidationMaxRetry=" + mAutoDataSwitchValidationMaxRetry); |
| pw.println("mLastSwitchPreferredDataReason=" |
| + switchReasonToString(mLastSwitchPreferredDataReason)); |
| pw.println("mDisplayedAutoSwitchNotification=" + mDisplayedAutoSwitchNotification); |
| pw.println("Local logs:"); |
| pw.increaseIndent(); |
| mLocalLog.dump(fd, pw, args); |
| pw.decreaseIndent(); |
| pw.decreaseIndent(); |
| } |
| |
| private boolean isAnyVoiceCallActiveOnDevice() { |
| boolean ret = mPhoneIdInVoiceCall != SubscriptionManager.INVALID_PHONE_INDEX; |
| if (VDBG) log("isAnyVoiceCallActiveOnDevice: " + ret); |
| return ret; |
| } |
| |
| private void onDdsSwitchResponse(AsyncResult ar) { |
| boolean commandSuccess = ar != null && ar.exception == null; |
| int phoneId = (int) ar.userObj; |
| if (mEmergencyOverride != null) { |
| logl("Emergency override result sent = " + commandSuccess); |
| mEmergencyOverride.sendOverrideCompleteCallbackResultAndClear(commandSuccess); |
| // Do not retry , as we do not allow changes in onEvaluate during an emergency |
| // call. When the call ends, we will start the countdown to remove the override. |
| } else if (!commandSuccess) { |
| logl("onDdsSwitchResponse: DDS switch failed. with exception " + ar.exception); |
| if (ar.exception instanceof CommandException) { |
| CommandException.Error error = ((CommandException) |
| (ar.exception)).getCommandError(); |
| mCurrentDdsSwitchFailure.get(phoneId).add(error); |
| if (error == CommandException.Error.OP_NOT_ALLOWED_DURING_VOICE_CALL) { |
| logl("onDdsSwitchResponse: Wait for call end indication"); |
| return; |
| } else if (error == CommandException.Error.INVALID_SIM_STATE) { |
| /* If there is a attach failure due to sim not ready then |
| hold the retry until sim gets ready */ |
| logl("onDdsSwitchResponse: Wait for SIM to get READY"); |
| return; |
| } |
| } |
| logl("onDdsSwitchResponse: Scheduling DDS switch retry"); |
| sendMessageDelayed(Message.obtain(this, EVENT_MODEM_COMMAND_RETRY, |
| phoneId), MODEM_COMMAND_RETRY_PERIOD_MS); |
| return; |
| } |
| if (commandSuccess) logl("onDdsSwitchResponse: DDS switch success on phoneId = " + phoneId); |
| mCurrentDdsSwitchFailure.get(phoneId).clear(); |
| // Notify all registrants |
| mActivePhoneRegistrants.notifyRegistrants(); |
| notifyPreferredDataSubIdChanged(); |
| displayAutoDataSwitchNotification(); |
| } |
| |
| /** |
| * Display a notification the first time auto data switch occurs. |
| */ |
| private void displayAutoDataSwitchNotification() { |
| NotificationManager notificationManager = (NotificationManager) |
| mContext.getSystemService(Context.NOTIFICATION_SERVICE); |
| |
| if (mDisplayedAutoSwitchNotification) { |
| // cancel posted notification if any exist |
| log("displayAutoDataSwitchNotification: canceling any notifications for subId " |
| + mAutoSelectedDataSubId); |
| notificationManager.cancel(AUTO_DATA_SWITCH_NOTIFICATION_TAG, |
| AUTO_DATA_SWITCH_NOTIFICATION_ID); |
| return; |
| } |
| // proceed only the first time auto data switch occurs, which includes data during call |
| if (mLastSwitchPreferredDataReason != DataSwitch.Reason.DATA_SWITCH_REASON_AUTO) { |
| log("displayAutoDataSwitchNotification: Ignore DDS switch due to " |
| + switchReasonToString(mLastSwitchPreferredDataReason)); |
| return; |
| } |
| SubscriptionInfo subInfo; |
| if (PhoneFactory.isSubscriptionManagerServiceEnabled()) { |
| subInfo = mSubscriptionManagerService.getSubscriptionInfo(mAutoSelectedDataSubId); |
| } else { |
| subInfo = mSubscriptionController.getSubscriptionInfo(mAutoSelectedDataSubId); |
| } |
| if (subInfo == null || subInfo.isOpportunistic()) { |
| loge("displayAutoDataSwitchNotification: mAutoSelectedDataSubId=" |
| + mAutoSelectedDataSubId + " unexpected subInfo " + subInfo); |
| return; |
| } |
| logl("displayAutoDataSwitchNotification: display for subId=" + mAutoSelectedDataSubId); |
| // "Mobile network settings" screen / dialog |
| Intent intent = new Intent(Settings.ACTION_NETWORK_OPERATOR_SETTINGS); |
| final Bundle fragmentArgs = new Bundle(); |
| // Special contract for Settings to highlight permission row |
| fragmentArgs.putString(SETTINGS_EXTRA_FRAGMENT_ARG_KEY, AUTO_DATA_SWITCH_SETTING_R_ID); |
| intent.putExtra(Settings.EXTRA_SUB_ID, mAutoSelectedDataSubId); |
| intent.putExtra(SETTINGS_EXTRA_SHOW_FRAGMENT_ARGUMENTS, fragmentArgs); |
| PendingIntent contentIntent = PendingIntent.getActivity( |
| mContext, mAutoSelectedDataSubId, intent, PendingIntent.FLAG_IMMUTABLE); |
| |
| CharSequence activeCarrierName = subInfo.getDisplayName(); |
| CharSequence contentTitle = mContext.getString( |
| com.android.internal.R.string.auto_data_switch_title, activeCarrierName); |
| CharSequence contentText = mContext.getText( |
| com.android.internal.R.string.auto_data_switch_content); |
| |
| final Notification notif = new Notification.Builder(mContext) |
| .setContentTitle(contentTitle) |
| .setContentText(contentText) |
| .setSmallIcon(android.R.drawable.stat_sys_warning) |
| .setColor(mContext.getResources().getColor( |
| com.android.internal.R.color.system_notification_accent_color)) |
| .setChannelId(NotificationChannelController.CHANNEL_ID_MOBILE_DATA_STATUS) |
| .setContentIntent(contentIntent) |
| .setStyle(new Notification.BigTextStyle().bigText(contentText)) |
| .build(); |
| notificationManager.notify(AUTO_DATA_SWITCH_NOTIFICATION_TAG, |
| AUTO_DATA_SWITCH_NOTIFICATION_ID, notif); |
| mDisplayedAutoSwitchNotification = true; |
| } |
| |
| private boolean isPhoneIdValidForRetry(int phoneId) { |
| int ddsPhoneId; |
| if (PhoneFactory.isSubscriptionManagerServiceEnabled()) { |
| ddsPhoneId = mSubscriptionManagerService.getPhoneId( |
| mSubscriptionManagerService.getDefaultDataSubId()); |
| } else { |
| ddsPhoneId = mSubscriptionController.getPhoneId( |
| mSubscriptionController.getDefaultDataSubId()); |
| } |
| if (ddsPhoneId != INVALID_PHONE_INDEX && ddsPhoneId == phoneId) { |
| return true; |
| } else { |
| if (mNetworkRequestList.isEmpty()) return false; |
| for (TelephonyNetworkRequest networkRequest : mNetworkRequestList) { |
| if (phoneIdForRequest(networkRequest) == phoneId) { |
| return true; |
| } |
| } |
| } |
| return false; |
| } |
| } |