| /* |
| * Copyright (C) 2022 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.services.telephony.domainselection; |
| |
| import android.annotation.NonNull; |
| import android.annotation.SuppressLint; |
| import android.content.Context; |
| import android.os.Handler; |
| import android.os.HandlerThread; |
| import android.os.Looper; |
| import android.telephony.BarringInfo; |
| import android.telephony.DisconnectCause; |
| import android.telephony.DomainSelectionService; |
| import android.telephony.ServiceState; |
| import android.telephony.SubscriptionInfo; |
| import android.telephony.SubscriptionManager; |
| import android.telephony.SubscriptionManager.OnSubscriptionsChangedListener; |
| import android.telephony.TelephonyManager; |
| import android.telephony.TransportSelectorCallback; |
| import android.util.IndentingPrintWriter; |
| import android.util.LocalLog; |
| import android.util.Log; |
| import android.util.SparseArray; |
| |
| import com.android.internal.annotations.VisibleForTesting; |
| |
| import java.io.FileDescriptor; |
| import java.io.PrintWriter; |
| import java.util.ArrayList; |
| import java.util.List; |
| import java.util.concurrent.Executor; |
| |
| /** |
| * Implements the telephony domain selection for various telephony features. |
| */ |
| public class TelephonyDomainSelectionService extends DomainSelectionService { |
| /** |
| * Testing interface for injecting mock ImsStateTracker. |
| */ |
| @VisibleForTesting |
| public interface ImsStateTrackerFactory { |
| /** |
| * @return The {@link ImsStateTracker} created for the specified slot. |
| */ |
| ImsStateTracker create(Context context, int slotId, @NonNull Looper looper); |
| } |
| |
| /** |
| * Testing interface for injecting mock DomainSelector. |
| */ |
| @VisibleForTesting |
| public interface DomainSelectorFactory { |
| /** |
| * @return The {@link DomainSelectorBase} created using the specified arguments. |
| */ |
| DomainSelectorBase create(Context context, int slotId, int subId, |
| @SelectorType int selectorType, boolean isEmergency, @NonNull Looper looper, |
| @NonNull ImsStateTracker imsStateTracker, |
| @NonNull DomainSelectorBase.DestroyListener listener, |
| @NonNull CrossSimRedialingController crossSimRedialingController); |
| } |
| |
| private static final class DefaultDomainSelectorFactory implements DomainSelectorFactory { |
| @Override |
| public DomainSelectorBase create(Context context, int slotId, int subId, |
| @SelectorType int selectorType, boolean isEmergency, @NonNull Looper looper, |
| @NonNull ImsStateTracker imsStateTracker, |
| @NonNull DomainSelectorBase.DestroyListener listener, |
| @NonNull CrossSimRedialingController crossSimRedialingController) { |
| DomainSelectorBase selector = null; |
| |
| logi("create-DomainSelector: slotId=" + slotId + ", subId=" + subId |
| + ", selectorType=" + selectorTypeToString(selectorType) |
| + ", emergency=" + isEmergency); |
| |
| switch (selectorType) { |
| case SELECTOR_TYPE_CALLING: |
| if (isEmergency) { |
| selector = new EmergencyCallDomainSelector(context, slotId, subId, looper, |
| imsStateTracker, listener, crossSimRedialingController); |
| } else { |
| selector = new NormalCallDomainSelector(context, slotId, subId, looper, |
| imsStateTracker, listener); |
| } |
| break; |
| case SELECTOR_TYPE_SMS: |
| if (isEmergency) { |
| selector = new EmergencySmsDomainSelector(context, slotId, subId, looper, |
| imsStateTracker, listener); |
| } else { |
| selector = new SmsDomainSelector(context, slotId, subId, looper, |
| imsStateTracker, listener); |
| } |
| break; |
| default: |
| // Not reachable. |
| break; |
| } |
| |
| return selector; |
| } |
| }; |
| |
| /** |
| * A container class to manage the domain selector per a slot and selector type. |
| * If the domain selector is not null and reusable, the same domain selector will be used |
| * for the specific slot. |
| */ |
| private static final class DomainSelectorContainer { |
| private final int mSlotId; |
| private final @SelectorType int mSelectorType; |
| private final boolean mIsEmergency; |
| private final @NonNull DomainSelectorBase mSelector; |
| |
| DomainSelectorContainer(int slotId, @SelectorType int selectorType, boolean isEmergency, |
| @NonNull DomainSelectorBase selector) { |
| mSlotId = slotId; |
| mSelectorType = selectorType; |
| mIsEmergency = isEmergency; |
| mSelector = selector; |
| } |
| |
| public int getSlotId() { |
| return mSlotId; |
| } |
| |
| public @SelectorType int getSelectorType() { |
| return mSelectorType; |
| } |
| |
| public DomainSelectorBase getDomainSelector() { |
| return mSelector; |
| } |
| |
| public boolean isEmergency() { |
| return mIsEmergency; |
| } |
| |
| @Override |
| public String toString() { |
| return new StringBuilder() |
| .append("{ ") |
| .append("slotId=").append(mSlotId) |
| .append(", selectorType=").append(selectorTypeToString(mSelectorType)) |
| .append(", isEmergency=").append(mIsEmergency) |
| .append(", selector=").append(mSelector) |
| .append(" }").toString(); |
| } |
| } |
| |
| private final DomainSelectorBase.DestroyListener mDestroyListener = |
| new DomainSelectorBase.DestroyListener() { |
| @Override |
| public void onDomainSelectorDestroyed(DomainSelectorBase selector) { |
| logd("DomainSelector destroyed: " + selector); |
| removeDomainSelector(selector); |
| } |
| }; |
| |
| /** |
| * A class to listen for the subscription change for starting {@link ImsStateTracker} |
| * to monitor the IMS states. |
| */ |
| private final OnSubscriptionsChangedListener mSubscriptionsChangedListener = |
| new OnSubscriptionsChangedListener() { |
| @Override |
| public void onSubscriptionsChanged() { |
| handleSubscriptionsChanged(); |
| } |
| }; |
| |
| private static final String TAG = TelephonyDomainSelectionService.class.getSimpleName(); |
| |
| // Persistent Logging |
| private static final LocalLog sEventLog = new LocalLog(20); |
| private final Context mContext; |
| // Map of slotId -> ImsStateTracker |
| private final SparseArray<ImsStateTracker> mImsStateTrackers = new SparseArray<>(2); |
| private final List<DomainSelectorContainer> mDomainSelectorContainers = new ArrayList<>(); |
| private final ImsStateTrackerFactory mImsStateTrackerFactory; |
| private final DomainSelectorFactory mDomainSelectorFactory; |
| private Handler mServiceHandler; |
| private CrossSimRedialingController mCrossSimRedialingController; |
| |
| public TelephonyDomainSelectionService(Context context) { |
| this(context, ImsStateTracker::new, new DefaultDomainSelectorFactory()); |
| } |
| |
| @VisibleForTesting |
| public TelephonyDomainSelectionService(Context context, |
| @NonNull ImsStateTrackerFactory imsStateTrackerFactory, |
| @NonNull DomainSelectorFactory domainSelectorFactory) { |
| mContext = context; |
| mImsStateTrackerFactory = imsStateTrackerFactory; |
| mDomainSelectorFactory = domainSelectorFactory; |
| |
| // Create a worker thread for this domain selection service. |
| getExecutor(); |
| |
| TelephonyManager tm = mContext.getSystemService(TelephonyManager.class); |
| int activeModemCount = (tm != null) ? tm.getActiveModemCount() : 1; |
| for (int i = 0; i < activeModemCount; ++i) { |
| mImsStateTrackers.put(i, mImsStateTrackerFactory.create(mContext, i, getLooper())); |
| } |
| |
| SubscriptionManager sm = mContext.getSystemService(SubscriptionManager.class); |
| if (sm != null) { |
| sm.addOnSubscriptionsChangedListener(getExecutor(), mSubscriptionsChangedListener); |
| } else { |
| loge("Adding OnSubscriptionChangedListener failed"); |
| } |
| |
| mCrossSimRedialingController = new CrossSimRedialingController(context, getLooper()); |
| |
| logi("TelephonyDomainSelectionService created"); |
| } |
| |
| @Override |
| public void onDestroy() { |
| logd("onDestroy"); |
| |
| List<DomainSelectorContainer> domainSelectorContainers; |
| |
| synchronized (mDomainSelectorContainers) { |
| domainSelectorContainers = new ArrayList<>(mDomainSelectorContainers); |
| mDomainSelectorContainers.clear(); |
| } |
| |
| for (DomainSelectorContainer dsc : domainSelectorContainers) { |
| DomainSelectorBase selector = dsc.getDomainSelector(); |
| if (selector != null) { |
| selector.destroy(); |
| } |
| } |
| domainSelectorContainers.clear(); |
| |
| synchronized (mImsStateTrackers) { |
| for (int i = 0; i < mImsStateTrackers.size(); ++i) { |
| ImsStateTracker ist = mImsStateTrackers.get(i); |
| if (ist != null) { |
| ist.destroy(); |
| } |
| } |
| mImsStateTrackers.clear(); |
| } |
| |
| SubscriptionManager sm = mContext.getSystemService(SubscriptionManager.class); |
| if (sm != null) { |
| sm.removeOnSubscriptionsChangedListener(mSubscriptionsChangedListener); |
| } |
| |
| if (mCrossSimRedialingController != null) { |
| mCrossSimRedialingController.destroy(); |
| mCrossSimRedialingController = null; |
| } |
| |
| if (mServiceHandler != null) { |
| mServiceHandler.getLooper().quit(); |
| mServiceHandler = null; |
| } |
| } |
| |
| /** |
| * Selects a domain for the given attributes and callback. |
| * |
| * @param attr required to determine the domain. |
| * @param callback the callback instance being registered. |
| */ |
| @Override |
| public void onDomainSelection(@NonNull SelectionAttributes attr, |
| @NonNull TransportSelectorCallback callback) { |
| final int slotId = attr.getSlotId(); |
| final int subId = attr.getSubId(); |
| final int selectorType = attr.getSelectorType(); |
| final boolean isEmergency = attr.isEmergency(); |
| ImsStateTracker ist = getImsStateTracker(slotId); |
| DomainSelectorBase selector = mDomainSelectorFactory.create(mContext, slotId, subId, |
| selectorType, isEmergency, getLooper(), ist, mDestroyListener, |
| mCrossSimRedialingController); |
| |
| if (selector != null) { |
| // Ensures that ImsStateTracker is started before selecting the domain if not started |
| // for the specified subscription index. |
| ist.start(subId); |
| addDomainSelector(slotId, selectorType, isEmergency, selector); |
| } else { |
| loge("No proper domain selector: " + selectorTypeToString(selectorType)); |
| callback.onSelectionTerminated(DisconnectCause.ERROR_UNSPECIFIED); |
| return; |
| } |
| |
| // Notify the caller that the domain selector is created. |
| callback.onCreated(selector); |
| |
| // Performs the domain selection. |
| selector.selectDomain(attr, callback); |
| } |
| |
| /** |
| * Called when the {@link ServiceState} needs to be updated for the specified slot and |
| * subcription index. |
| * |
| * @param slotId for which the service state changed. |
| * @param subId The current subscription for a specified slot. |
| * @param serviceState The {@link ServiceState} to be updated. |
| */ |
| @Override |
| public void onServiceStateUpdated(int slotId, int subId, @NonNull ServiceState serviceState) { |
| ImsStateTracker ist = getImsStateTracker(slotId); |
| if (ist != null) { |
| ist.updateServiceState(serviceState); |
| } |
| } |
| |
| /** |
| * Called when the {@link BarringInfo} needs to be updated for the specified slot and |
| * subscription index. |
| * |
| * @param slotId The slot the BarringInfo is updated for. |
| * @param subId The current subscription for a specified slot. |
| * @param barringInfo The {@link BarringInfo} to be updated. |
| */ |
| @Override |
| public void onBarringInfoUpdated(int slotId, int subId, @NonNull BarringInfo barringInfo) { |
| ImsStateTracker ist = getImsStateTracker(slotId); |
| if (ist != null) { |
| ist.updateBarringInfo(barringInfo); |
| } |
| } |
| |
| /** |
| * Returns an Executor used to execute methods called remotely by the framework. |
| */ |
| @SuppressLint("OnNameExpected") |
| @Override |
| public @NonNull Executor getExecutor() { |
| if (mServiceHandler == null) { |
| HandlerThread handlerThread = new HandlerThread(TAG); |
| handlerThread.start(); |
| mServiceHandler = new Handler(handlerThread.getLooper()); |
| } |
| |
| return mServiceHandler::post; |
| } |
| |
| /** |
| * Returns a Looper instance. |
| */ |
| @VisibleForTesting |
| public Looper getLooper() { |
| getExecutor(); |
| return mServiceHandler.getLooper(); |
| } |
| |
| /** |
| * Handles the subscriptions change. |
| */ |
| private void handleSubscriptionsChanged() { |
| SubscriptionManager sm = mContext.getSystemService(SubscriptionManager.class); |
| List<SubscriptionInfo> subsInfoList = |
| (sm != null) ? sm.getActiveSubscriptionInfoList() : null; |
| |
| if (subsInfoList == null || subsInfoList.isEmpty()) { |
| logd("handleSubscriptionsChanged: No valid SubscriptionInfo"); |
| return; |
| } |
| |
| for (int i = 0; i < subsInfoList.size(); ++i) { |
| SubscriptionInfo subsInfo = subsInfoList.get(i); |
| int slotId = subsInfo.getSimSlotIndex(); |
| |
| if (slotId != SubscriptionManager.INVALID_SIM_SLOT_INDEX) { |
| logd("handleSubscriptionsChanged: slotId=" + slotId); |
| ImsStateTracker ist = getImsStateTracker(slotId); |
| ist.start(subsInfo.getSubscriptionId()); |
| } |
| } |
| } |
| |
| /** |
| * Adds the {@link DomainSelectorBase} to the list of domain selector container. |
| */ |
| private void addDomainSelector(int slotId, @SelectorType int selectorType, |
| boolean isEmergency, @NonNull DomainSelectorBase selector) { |
| synchronized (mDomainSelectorContainers) { |
| // If the domain selector already exists, remove the previous one first. |
| for (int i = 0; i < mDomainSelectorContainers.size(); ++i) { |
| DomainSelectorContainer dsc = mDomainSelectorContainers.get(i); |
| |
| if (dsc.getSlotId() == slotId |
| && dsc.getSelectorType() == selectorType |
| && dsc.isEmergency() == isEmergency) { |
| mDomainSelectorContainers.remove(i); |
| DomainSelectorBase oldSelector = dsc.getDomainSelector(); |
| if (oldSelector != null) { |
| logw("DomainSelector destroyed by new domain selection request: " + dsc); |
| oldSelector.destroy(); |
| } |
| break; |
| } |
| } |
| |
| DomainSelectorContainer dsc = |
| new DomainSelectorContainer(slotId, selectorType, isEmergency, selector); |
| mDomainSelectorContainers.add(dsc); |
| |
| logi("DomainSelector added: " + dsc + ", count=" + mDomainSelectorContainers.size()); |
| } |
| } |
| |
| /** |
| * Removes the domain selector container that matches with the specified |
| * {@link DomainSelectorBase}. |
| */ |
| private void removeDomainSelector(@NonNull DomainSelectorBase selector) { |
| synchronized (mDomainSelectorContainers) { |
| for (int i = 0; i < mDomainSelectorContainers.size(); ++i) { |
| DomainSelectorContainer dsc = mDomainSelectorContainers.get(i); |
| |
| if (dsc.getDomainSelector() == selector) { |
| mDomainSelectorContainers.remove(i); |
| logi("DomainSelector removed: " + dsc |
| + ", count=" + mDomainSelectorContainers.size()); |
| break; |
| } |
| } |
| } |
| } |
| |
| /** |
| * Returns the {@link ImsStateTracker} instance for the specified slot. |
| * If the {@link ImsStateTracker} does not exist for the slot, it creates new instance |
| * and returns. |
| */ |
| private ImsStateTracker getImsStateTracker(int slotId) { |
| synchronized (mImsStateTrackers) { |
| ImsStateTracker ist = mImsStateTrackers.get(slotId); |
| |
| if (ist == null) { |
| ist = mImsStateTrackerFactory.create(mContext, slotId, getLooper()); |
| mImsStateTrackers.put(slotId, ist); |
| } |
| |
| return ist; |
| } |
| } |
| |
| private static String selectorTypeToString(@SelectorType int selectorType) { |
| switch (selectorType) { |
| case SELECTOR_TYPE_CALLING: return "CALLING"; |
| case SELECTOR_TYPE_SMS: return "SMS"; |
| case SELECTOR_TYPE_UT: return "UT"; |
| default: return Integer.toString(selectorType); |
| } |
| } |
| |
| private static void logd(String s) { |
| Log.d(TAG, s); |
| } |
| |
| private static void logi(String s) { |
| Log.i(TAG, s); |
| sEventLog.log(s); |
| } |
| |
| private static void loge(String s) { |
| Log.e(TAG, s); |
| sEventLog.log(s); |
| } |
| |
| private static void logw(String s) { |
| Log.w(TAG, s); |
| sEventLog.log(s); |
| } |
| |
| /** |
| * Dumps this instance into a readable format for dumpsys usage. |
| */ |
| public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { |
| IndentingPrintWriter ipw = new IndentingPrintWriter(pw, " "); |
| ipw.println("TelephonyDomainSelectionService:"); |
| ipw.increaseIndent(); |
| ipw.println("ImsStateTrackers:"); |
| synchronized (mImsStateTrackers) { |
| for (int i = 0; i < mImsStateTrackers.size(); ++i) { |
| ImsStateTracker ist = mImsStateTrackers.valueAt(i); |
| ist.dump(ipw); |
| } |
| } |
| ipw.decreaseIndent(); |
| ipw.increaseIndent(); |
| synchronized (mDomainSelectorContainers) { |
| for (int i = 0; i < mDomainSelectorContainers.size(); ++i) { |
| DomainSelectorContainer dsc = mDomainSelectorContainers.get(i); |
| ipw.println("DomainSelector: " + dsc.toString()); |
| ipw.increaseIndent(); |
| DomainSelectorBase selector = dsc.getDomainSelector(); |
| if (selector != null) { |
| selector.dump(ipw); |
| } |
| ipw.decreaseIndent(); |
| } |
| } |
| ipw.decreaseIndent(); |
| ipw.increaseIndent(); |
| ipw.println("Event Log:"); |
| ipw.increaseIndent(); |
| sEventLog.dump(ipw); |
| ipw.decreaseIndent(); |
| ipw.decreaseIndent(); |
| ipw.println("________________________________"); |
| } |
| } |