| /* |
| * Copyright (C) 2017 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.ims; |
| |
| import android.Manifest; |
| import android.annotation.NonNull; |
| import android.annotation.Nullable; |
| import android.content.BroadcastReceiver; |
| import android.content.ComponentName; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.IntentFilter; |
| import android.content.pm.PackageManager; |
| import android.content.pm.ResolveInfo; |
| import android.content.pm.ServiceInfo; |
| import android.os.AsyncResult; |
| import android.os.Handler; |
| import android.os.Looper; |
| import android.os.Message; |
| import android.os.PersistableBundle; |
| import android.os.RemoteException; |
| import android.os.UserHandle; |
| import android.os.UserManager; |
| import android.telephony.CarrierConfigManager; |
| import android.telephony.SubscriptionManager; |
| import android.telephony.TelephonyManager; |
| import android.telephony.ims.ImsService; |
| import android.telephony.ims.aidl.IImsConfig; |
| import android.telephony.ims.aidl.IImsMmTelFeature; |
| import android.telephony.ims.aidl.IImsRcsFeature; |
| import android.telephony.ims.aidl.IImsRegistration; |
| import android.telephony.ims.feature.ImsFeature; |
| import android.telephony.ims.feature.MmTelFeature; |
| import android.telephony.ims.stub.ImsFeatureConfiguration; |
| import android.text.TextUtils; |
| import android.util.ArrayMap; |
| import android.util.LocalLog; |
| import android.util.Log; |
| import android.util.SparseArray; |
| |
| import com.android.ims.internal.IImsServiceFeatureCallback; |
| import com.android.internal.annotations.VisibleForTesting; |
| import com.android.internal.os.SomeArgs; |
| import com.android.internal.telephony.PhoneConfigurationManager; |
| import com.android.internal.util.IndentingPrintWriter; |
| |
| import java.io.FileDescriptor; |
| import java.io.PrintWriter; |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Objects; |
| import java.util.Set; |
| import java.util.concurrent.LinkedBlockingQueue; |
| import java.util.concurrent.TimeUnit; |
| import java.util.stream.Collectors; |
| |
| /** |
| * Creates a list of ImsServices that are available to bind to based on the Device configuration |
| * overlay values "config_ims_rcs_package" and "config_ims_mmtel_package" as well as Carrier |
| * Configuration value "config_ims_rcs_package_override_string" and |
| * "config_ims_mmtel_package_override_string". |
| * These ImsServices are then bound to in the following order for each mmtel and rcs feature: |
| * |
| * 1. Carrier Config defined override value per SIM. |
| * 2. Device overlay default value (including no SIM case). |
| * |
| * ImsManager can then retrieve the binding to the correct ImsService using |
| * {@link #getImsServiceControllerAndListen} on a per-slot and per feature basis. |
| */ |
| |
| public class ImsResolver implements ImsServiceController.ImsServiceControllerCallbacks { |
| |
| private static final String TAG = "ImsResolver"; |
| private static final int GET_IMS_SERVICE_TIMEOUT_MS = 5000; |
| |
| @VisibleForTesting |
| public static final String METADATA_EMERGENCY_MMTEL_FEATURE = |
| "android.telephony.ims.EMERGENCY_MMTEL_FEATURE"; |
| @VisibleForTesting |
| public static final String METADATA_MMTEL_FEATURE = "android.telephony.ims.MMTEL_FEATURE"; |
| @VisibleForTesting |
| public static final String METADATA_RCS_FEATURE = "android.telephony.ims.RCS_FEATURE"; |
| // Overrides the sanity permission check of android.permission.BIND_IMS_SERVICE for any |
| // ImsService that is connecting to the platform. |
| // This should ONLY be used for testing and should not be used in production ImsServices. |
| private static final String METADATA_OVERRIDE_PERM_CHECK = "override_bind_check"; |
| |
| // Based on updates from PackageManager |
| private static final int HANDLER_ADD_PACKAGE = 0; |
| // Based on updates from PackageManager |
| private static final int HANDLER_REMOVE_PACKAGE = 1; |
| // Based on updates from CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED |
| private static final int HANDLER_CONFIG_CHANGED = 2; |
| // A query has been started for an ImsService to relay the features they support. |
| private static final int HANDLER_START_DYNAMIC_FEATURE_QUERY = 3; |
| // A dynamic query to request ImsService features has completed. |
| private static final int HANDLER_DYNAMIC_FEATURE_CHANGE = 4; |
| // Testing: Overrides the current configuration for ImsService binding |
| private static final int HANDLER_OVERRIDE_IMS_SERVICE_CONFIG = 5; |
| // Based on boot complete indication. When this happens, there may be ImsServices that are not |
| // direct boot aware that need to be started. |
| private static final int HANDLER_BOOT_COMPLETE = 6; |
| // Sent when the number of slots has dynamically changed on the device. We will need to |
| // resize available ImsServiceController slots and perform dynamic queries again. |
| private static final int HANDLER_MSIM_CONFIGURATION_CHANGE = 7; |
| |
| // Delay between dynamic ImsService queries. |
| private static final int DELAY_DYNAMIC_QUERY_MS = 5000; |
| |
| private static class OverrideConfig { |
| public final int slotId; |
| public final boolean isCarrierService; |
| public final Map<Integer, String> featureTypeToPackageMap; |
| |
| OverrideConfig(int slotIndex, boolean isCarrier, Map<Integer, String> feature) { |
| slotId = slotIndex; |
| isCarrierService = isCarrier; |
| featureTypeToPackageMap = feature; |
| } |
| } |
| |
| /** |
| * Stores information about an ImsService, including the package name, class name, and features |
| * that the service supports. |
| */ |
| @VisibleForTesting |
| public static class ImsServiceInfo { |
| public ComponentName name; |
| // Determines if features were created from metadata in the manifest or through dynamic |
| // query. |
| public boolean featureFromMetadata = true; |
| public ImsServiceControllerFactory controllerFactory; |
| |
| // Map slotId->Feature |
| private final HashSet<ImsFeatureConfiguration.FeatureSlotPair> mSupportedFeatures; |
| |
| public ImsServiceInfo() { |
| mSupportedFeatures = new HashSet<>(); |
| } |
| |
| void addFeatureForAllSlots(int numSlots, int feature) { |
| for (int i = 0; i < numSlots; i++) { |
| mSupportedFeatures.add(new ImsFeatureConfiguration.FeatureSlotPair(i, feature)); |
| } |
| } |
| |
| void replaceFeatures(Set<ImsFeatureConfiguration.FeatureSlotPair> newFeatures) { |
| mSupportedFeatures.clear(); |
| mSupportedFeatures.addAll(newFeatures); |
| } |
| |
| @VisibleForTesting |
| public Set<ImsFeatureConfiguration.FeatureSlotPair> getSupportedFeatures() { |
| return mSupportedFeatures; |
| } |
| |
| @Override |
| public boolean equals(Object o) { |
| if (this == o) return true; |
| if (o == null || getClass() != o.getClass()) return false; |
| |
| ImsServiceInfo that = (ImsServiceInfo) o; |
| |
| if (name != null ? !name.equals(that.name) : that.name != null) return false; |
| if (!mSupportedFeatures.equals(that.mSupportedFeatures)) { |
| return false; |
| } |
| return controllerFactory != null ? controllerFactory.equals(that.controllerFactory) |
| : that.controllerFactory == null; |
| } |
| |
| @Override |
| public int hashCode() { |
| // We do not include mSupportedFeatures in hashcode because the internal structure |
| // changes after adding. |
| int result = name != null ? name.hashCode() : 0; |
| result = 31 * result + (controllerFactory != null ? controllerFactory.hashCode() : 0); |
| return result; |
| } |
| |
| @Override |
| public String toString() { |
| return "[ImsServiceInfo] name=" |
| + name |
| + ", featureFromMetadata=" |
| + featureFromMetadata |
| + "," |
| + printFeatures(mSupportedFeatures); |
| } |
| } |
| |
| // Receives broadcasts from the system involving changes to the installed applications. If |
| // an ImsService that we are configured to use is installed, we must bind to it. |
| private BroadcastReceiver mAppChangedReceiver = new BroadcastReceiver() { |
| @Override |
| public void onReceive(Context context, Intent intent) { |
| final String action = intent.getAction(); |
| final String packageName = intent.getData().getSchemeSpecificPart(); |
| switch (action) { |
| case Intent.ACTION_PACKAGE_ADDED: |
| // intentional fall-through |
| case Intent.ACTION_PACKAGE_REPLACED: |
| // intentional fall-through |
| case Intent.ACTION_PACKAGE_CHANGED: |
| mHandler.obtainMessage(HANDLER_ADD_PACKAGE, packageName).sendToTarget(); |
| break; |
| case Intent.ACTION_PACKAGE_REMOVED: |
| mHandler.obtainMessage(HANDLER_REMOVE_PACKAGE, packageName).sendToTarget(); |
| break; |
| default: |
| return; |
| } |
| } |
| }; |
| |
| // Receives the broadcast that a new Carrier Config has been loaded in order to possibly |
| // unbind from one service and bind to another. |
| private BroadcastReceiver mConfigChangedReceiver = new BroadcastReceiver() { |
| @Override |
| public void onReceive(Context context, Intent intent) { |
| |
| int slotId = intent.getIntExtra(CarrierConfigManager.EXTRA_SLOT_INDEX, |
| SubscriptionManager.INVALID_SIM_SLOT_INDEX); |
| |
| if (slotId == SubscriptionManager.INVALID_SIM_SLOT_INDEX) { |
| Log.i(TAG, "Received CCC for invalid slot id."); |
| return; |
| } |
| |
| int subId = intent.getIntExtra(CarrierConfigManager.EXTRA_SUBSCRIPTION_INDEX, |
| SubscriptionManager.INVALID_SUBSCRIPTION_ID); |
| int slotSimState = mTelephonyManagerProxy.getSimState(mContext, slotId); |
| if (subId == SubscriptionManager.INVALID_SUBSCRIPTION_ID |
| && slotSimState != TelephonyManager.SIM_STATE_ABSENT) { |
| // We only care about carrier config updates that happen when a slot is known to be |
| // absent or populated and the carrier config has been loaded. |
| Log.i(TAG, "Received CCC for slot " + slotId + " and sim state " |
| + slotSimState + ", ignoring."); |
| return; |
| } |
| |
| Log.i(TAG, "Received Carrier Config Changed for SlotId: " + slotId); |
| |
| mHandler.obtainMessage(HANDLER_CONFIG_CHANGED, slotId).sendToTarget(); |
| } |
| }; |
| |
| // Receives the broadcast that the device has finished booting (and the device is no longer |
| // encrypted). |
| private BroadcastReceiver mBootCompleted = new BroadcastReceiver() { |
| @Override |
| public void onReceive(Context context, Intent intent) { |
| Log.i(TAG, "Received BOOT_COMPLETED"); |
| // Recalculate all cached services to pick up ones that have just been enabled since |
| // boot complete. |
| mHandler.obtainMessage(HANDLER_BOOT_COMPLETE, null).sendToTarget(); |
| } |
| }; |
| |
| /** |
| * Testing interface used to mock SubscriptionManager in testing |
| */ |
| @VisibleForTesting |
| public interface SubscriptionManagerProxy { |
| /** |
| * Mock-able interface for {@link SubscriptionManager#getSubId(int)} used for testing. |
| */ |
| int getSubId(int slotId); |
| /** |
| * Mock-able interface for {@link SubscriptionManager#getSlotIndex(int)} used for testing. |
| */ |
| int getSlotIndex(int subId); |
| } |
| |
| /** |
| * Testing interface used to stub out TelephonyManager dependencies. |
| */ |
| @VisibleForTesting |
| public interface TelephonyManagerProxy { |
| /** |
| * @return the SIM state for the slot ID specified. |
| */ |
| int getSimState(Context context, int slotId); |
| } |
| |
| private TelephonyManagerProxy mTelephonyManagerProxy = new TelephonyManagerProxy() { |
| @Override |
| public int getSimState(Context context, int slotId) { |
| TelephonyManager tm = context.getSystemService(TelephonyManager.class); |
| if (tm == null) { |
| return TelephonyManager.SIM_STATE_UNKNOWN; |
| } |
| return tm.getSimState(slotId); |
| } |
| }; |
| |
| private SubscriptionManagerProxy mSubscriptionManagerProxy = new SubscriptionManagerProxy() { |
| @Override |
| public int getSubId(int slotId) { |
| int[] subIds = SubscriptionManager.getSubId(slotId); |
| if (subIds != null) { |
| // This is done in all other places getSubId is used. |
| return subIds[0]; |
| } |
| return SubscriptionManager.INVALID_SUBSCRIPTION_ID; |
| } |
| |
| @Override |
| public int getSlotIndex(int subId) { |
| return SubscriptionManager.getSlotIndex(subId); |
| } |
| }; |
| |
| /** |
| * Testing interface for injecting mock ImsServiceControllers. |
| */ |
| @VisibleForTesting |
| public interface ImsServiceControllerFactory { |
| /** |
| * @return the Service Interface String used for binding the ImsService. |
| */ |
| String getServiceInterface(); |
| /** |
| * @return the ImsServiceController created using the context and componentName supplied. |
| */ |
| ImsServiceController create(Context context, ComponentName componentName, |
| ImsServiceController.ImsServiceControllerCallbacks callbacks); |
| } |
| |
| private ImsServiceControllerFactory mImsServiceControllerFactory = |
| new ImsServiceControllerFactory() { |
| |
| @Override |
| public String getServiceInterface() { |
| return ImsService.SERVICE_INTERFACE; |
| } |
| |
| @Override |
| public ImsServiceController create(Context context, ComponentName componentName, |
| ImsServiceController.ImsServiceControllerCallbacks callbacks) { |
| return new ImsServiceController(context, componentName, callbacks); |
| } |
| }; |
| |
| /** |
| * Used for testing. |
| */ |
| @VisibleForTesting |
| public interface ImsDynamicQueryManagerFactory { |
| ImsServiceFeatureQueryManager create(Context context, |
| ImsServiceFeatureQueryManager.Listener listener); |
| } |
| |
| private ImsServiceControllerFactory mImsServiceControllerFactoryCompat = |
| new ImsServiceControllerFactory() { |
| @Override |
| public String getServiceInterface() { |
| return android.telephony.ims.compat.ImsService.SERVICE_INTERFACE; |
| } |
| |
| @Override |
| public ImsServiceController create(Context context, ComponentName componentName, |
| ImsServiceController.ImsServiceControllerCallbacks callbacks) { |
| return new ImsServiceControllerCompat(context, componentName, callbacks); |
| } |
| }; |
| |
| private ImsDynamicQueryManagerFactory mDynamicQueryManagerFactory = |
| ImsServiceFeatureQueryManager::new; |
| |
| private final CarrierConfigManager mCarrierConfigManager; |
| private final Context mContext; |
| // Special context created only for registering receivers for all users using UserHandle.ALL. |
| // The lifetime of a registered receiver is bounded by the lifetime of the context it's |
| // registered through, so we must retain the Context as long as we need the receiver to be |
| // active. |
| private final Context mReceiverContext; |
| // Locks mBoundImsServicesByFeature only. Be careful to avoid deadlocks from |
| // ImsServiceController callbacks. |
| private final Object mBoundServicesLock = new Object(); |
| private int mNumSlots; |
| // Array index corresponds to slot, per slot there is a feature->package name mapping. |
| // should only be accessed from handler |
| private SparseArray<Map<Integer, String>> mCarrierServices; |
| // Package name of the default device services, Maps ImsFeature -> packageName. |
| // should only be accessed from handler |
| private Map<Integer, String> mDeviceServices; |
| // Persistent Logging |
| private final LocalLog mEventLog = new LocalLog(50); |
| |
| private boolean mBootCompletedHandlerRan = false; |
| |
| // Synchronize all events on a handler to ensure that the cache includes the most recent |
| // version of the installed ImsServices. |
| private Handler mHandler = new Handler(Looper.getMainLooper(), (msg) -> { |
| switch (msg.what) { |
| case HANDLER_ADD_PACKAGE: { |
| String packageName = (String) msg.obj; |
| maybeAddedImsService(packageName); |
| break; |
| } |
| case HANDLER_REMOVE_PACKAGE: { |
| String packageName = (String) msg.obj; |
| maybeRemovedImsService(packageName); |
| break; |
| } |
| case HANDLER_BOOT_COMPLETE: { |
| if (!mBootCompletedHandlerRan) { |
| mBootCompletedHandlerRan = true; |
| mEventLog.log("handling BOOT_COMPLETE"); |
| // Re-evaluate bound services for all slots after requerying packagemanager |
| maybeAddedImsService(null /*packageName*/); |
| } |
| break; |
| } |
| case HANDLER_CONFIG_CHANGED: { |
| int slotId = (Integer) msg.obj; |
| // If the msim config has changed and there is a residual event for an invalid slot, |
| // ignore. |
| if (slotId >= mNumSlots) { |
| Log.w(TAG, "HANDLER_CONFIG_CHANGED for invalid slotid=" + slotId); |
| break; |
| } |
| carrierConfigChanged(slotId); |
| break; |
| } |
| case HANDLER_START_DYNAMIC_FEATURE_QUERY: { |
| ImsServiceInfo info = (ImsServiceInfo) msg.obj; |
| startDynamicQuery(info); |
| break; |
| } |
| case HANDLER_DYNAMIC_FEATURE_CHANGE: { |
| SomeArgs args = (SomeArgs) msg.obj; |
| ComponentName name = (ComponentName) args.arg1; |
| Set<ImsFeatureConfiguration.FeatureSlotPair> features = |
| (Set<ImsFeatureConfiguration.FeatureSlotPair>) args.arg2; |
| args.recycle(); |
| dynamicQueryComplete(name, features); |
| break; |
| } |
| case HANDLER_OVERRIDE_IMS_SERVICE_CONFIG: { |
| OverrideConfig config = (OverrideConfig) msg.obj; |
| if (config.isCarrierService) { |
| overrideCarrierService(config.slotId, |
| config.featureTypeToPackageMap); |
| } else { |
| overrideDeviceService(config.featureTypeToPackageMap); |
| } |
| break; |
| } |
| case HANDLER_MSIM_CONFIGURATION_CHANGE: { |
| AsyncResult result = (AsyncResult) msg.obj; |
| handleMsimConfigChange((Integer) result.result); |
| break; |
| } |
| default: |
| return false; |
| } |
| return true; |
| }); |
| |
| // Results from dynamic queries to ImsService regarding the features they support. |
| private ImsServiceFeatureQueryManager.Listener mDynamicQueryListener = |
| new ImsServiceFeatureQueryManager.Listener() { |
| |
| @Override |
| public void onComplete(ComponentName name, |
| Set<ImsFeatureConfiguration.FeatureSlotPair> features) { |
| Log.d(TAG, "onComplete called for name: " + name + printFeatures(features)); |
| handleFeaturesChanged(name, features); |
| } |
| |
| @Override |
| public void onError(ComponentName name) { |
| Log.w(TAG, "onError: " + name + "returned with an error result"); |
| mEventLog.log("onError - dynamic query error for " + name); |
| scheduleQueryForFeatures(name, DELAY_DYNAMIC_QUERY_MS); |
| } |
| |
| @Override |
| public void onPermanentError(ComponentName name) { |
| Log.w(TAG, "onPermanentError: component=" + name); |
| mEventLog.log("onPermanentError - error for " + name); |
| mHandler.obtainMessage(HANDLER_REMOVE_PACKAGE, |
| name.getPackageName()).sendToTarget(); |
| } |
| }; |
| |
| // Used during testing, overrides the carrier services while non-empty. |
| // Array index corresponds to slot, per slot there is a feature->package name mapping. |
| // should only be accessed from handler |
| private SparseArray<SparseArray<String>> mOverrideServices; |
| // Outer array index corresponds to Slot Id, Maps ImsFeature.FEATURE->bound ImsServiceController |
| // Locked on mBoundServicesLock |
| private SparseArray<SparseArray<ImsServiceController>> mBoundImsServicesByFeature; |
| // not locked, only accessed on a handler thread. |
| // Tracks list of all installed ImsServices |
| private Map<ComponentName, ImsServiceInfo> mInstalledServicesCache = new HashMap<>(); |
| // not locked, only accessed on a handler thread. |
| // Active ImsServiceControllers, which are bound to ImsServices. |
| private Map<ComponentName, ImsServiceController> mActiveControllers = new HashMap<>(); |
| private ImsServiceFeatureQueryManager mFeatureQueryManager; |
| |
| public ImsResolver(Context context, String defaultMmTelPackageName, |
| String defaultRcsPackageName, int numSlots) { |
| Log.i(TAG, "device MMTEL package: " + defaultMmTelPackageName + ", device RCS package:" |
| + defaultRcsPackageName); |
| mContext = context; |
| mNumSlots = numSlots; |
| mReceiverContext = context.createContextAsUser(UserHandle.ALL, 0 /*flags*/); |
| |
| mCarrierServices = new SparseArray<>(mNumSlots); |
| mDeviceServices = new ArrayMap<>(); |
| setDeviceConfiguration(defaultMmTelPackageName, ImsFeature.FEATURE_EMERGENCY_MMTEL); |
| setDeviceConfiguration(defaultMmTelPackageName, ImsFeature.FEATURE_MMTEL); |
| setDeviceConfiguration(defaultRcsPackageName, ImsFeature.FEATURE_RCS); |
| mCarrierConfigManager = (CarrierConfigManager) mContext.getSystemService( |
| Context.CARRIER_CONFIG_SERVICE); |
| mOverrideServices = new SparseArray<>(0 /*initial size*/); |
| mBoundImsServicesByFeature = new SparseArray<>(mNumSlots); |
| |
| IntentFilter appChangedFilter = new IntentFilter(); |
| appChangedFilter.addAction(Intent.ACTION_PACKAGE_CHANGED); |
| appChangedFilter.addAction(Intent.ACTION_PACKAGE_REMOVED); |
| appChangedFilter.addAction(Intent.ACTION_PACKAGE_ADDED); |
| appChangedFilter.addDataScheme("package"); |
| mReceiverContext.registerReceiver(mAppChangedReceiver, appChangedFilter); |
| mReceiverContext.registerReceiver(mConfigChangedReceiver, new IntentFilter( |
| CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED)); |
| |
| UserManager userManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE); |
| if (userManager.isUserUnlocked()) { |
| mHandler.obtainMessage(HANDLER_BOOT_COMPLETE, null).sendToTarget(); |
| } else { |
| mReceiverContext.registerReceiver(mBootCompleted, new IntentFilter( |
| Intent.ACTION_BOOT_COMPLETED)); |
| if (userManager.isUserUnlocked()) { |
| mHandler.obtainMessage(HANDLER_BOOT_COMPLETE, null).sendToTarget(); |
| } |
| } |
| } |
| |
| @VisibleForTesting |
| public void setTelephonyManagerProxy(TelephonyManagerProxy proxy) { |
| mTelephonyManagerProxy = proxy; |
| } |
| |
| @VisibleForTesting |
| public void setSubscriptionManagerProxy(SubscriptionManagerProxy proxy) { |
| mSubscriptionManagerProxy = proxy; |
| } |
| |
| @VisibleForTesting |
| public void setImsServiceControllerFactory(ImsServiceControllerFactory factory) { |
| mImsServiceControllerFactory = factory; |
| } |
| |
| @VisibleForTesting |
| public Handler getHandler() { |
| return mHandler; |
| } |
| |
| @VisibleForTesting |
| public void setImsDynamicQueryManagerFactory(ImsDynamicQueryManagerFactory m) { |
| mDynamicQueryManagerFactory = m; |
| } |
| |
| /** |
| * Needs to be called after the constructor to kick off the process of binding to ImsServices. |
| */ |
| public void initialize() { |
| mEventLog.log("Initializing"); |
| Log.i(TAG, "Initializing cache."); |
| PhoneConfigurationManager.registerForMultiSimConfigChange(mHandler, |
| HANDLER_MSIM_CONFIGURATION_CHANGE, null); |
| mFeatureQueryManager = mDynamicQueryManagerFactory.create(mContext, mDynamicQueryListener); |
| |
| // This will get all services with the correct intent filter from PackageManager |
| List<ImsServiceInfo> infos = getImsServiceInfo(null); |
| for (ImsServiceInfo info : infos) { |
| if (!mInstalledServicesCache.containsKey(info.name)) { |
| mInstalledServicesCache.put(info.name, info); |
| } |
| } |
| // Update the package names of the carrier ImsServices if they do not exist already and |
| // possibly bind if carrier configs exist. Otherwise wait for CarrierConfigChanged |
| // indication. |
| bindCarrierServicesIfAvailable(); |
| } |
| |
| /** |
| * Destroys this ImsResolver. Used for tearing down static resources during testing. |
| */ |
| @VisibleForTesting |
| public void destroy() { |
| PhoneConfigurationManager.unregisterForMultiSimConfigChange(mHandler); |
| mHandler.removeCallbacksAndMessages(null); |
| } |
| |
| // Only start the bind if there is an existing Carrier Configuration. Otherwise, wait for |
| // carrier config changed. |
| private void bindCarrierServicesIfAvailable() { |
| boolean hasConfigChanged = false; |
| for (int slotId = 0; slotId < mNumSlots; slotId++) { |
| Map<Integer, String> featureMap = getImsPackageOverrideConfig(slotId); |
| for (int f = ImsFeature.FEATURE_EMERGENCY_MMTEL; f < ImsFeature.FEATURE_MAX; f++) { |
| String newPackageName = featureMap.getOrDefault(f, ""); |
| if (!TextUtils.isEmpty(newPackageName)) { |
| mEventLog.log("bindCarrierServicesIfAvailable - carrier package found: " |
| + newPackageName + " on slot " + slotId); |
| setCarrierConfiguredPackageName(newPackageName, slotId, f); |
| ImsServiceInfo info = getImsServiceInfoFromCache(newPackageName); |
| // We do not want to trigger feature configuration changes unless there is |
| // already a valid carrier config change. |
| if (info != null && info.featureFromMetadata) { |
| hasConfigChanged = true; |
| } else { |
| // Config will change when this query completes |
| scheduleQueryForFeatures(info); |
| } |
| } |
| } |
| } |
| if (hasConfigChanged) calculateFeatureConfigurationChange(); |
| } |
| |
| /** |
| * Notify ImsService to enable IMS for the framework. This will trigger IMS registration and |
| * trigger ImsFeature status updates. |
| */ |
| public void enableIms(int slotId) { |
| SparseArray<ImsServiceController> controllers = getImsServiceControllers(slotId); |
| if (controllers != null) { |
| for (int i = 0; i < controllers.size(); i++) { |
| int key = controllers.keyAt(i); |
| controllers.get(key).enableIms(slotId); |
| } |
| } |
| } |
| |
| /** |
| * Notify ImsService to disable IMS for the framework. This will trigger IMS de-registration and |
| * trigger ImsFeature capability status to become false. |
| */ |
| public void disableIms(int slotId) { |
| SparseArray<ImsServiceController> controllers = getImsServiceControllers(slotId); |
| if (controllers != null) { |
| for (int i = 0; i < controllers.size(); i++) { |
| int key = controllers.keyAt(i); |
| controllers.get(key).disableIms(slotId); |
| } |
| } |
| } |
| |
| /** |
| * Returns the {@link IImsMmTelFeature} that corresponds to the given slot Id or {@link null} if |
| * the service is not available. If an IImsMMTelFeature is available, the |
| * {@link IImsServiceFeatureCallback} callback is registered as a listener for feature updates. |
| * @param slotId The SIM slot that we are requesting the {@link IImsMmTelFeature} for. |
| * @param callback Listener that will send updates to ImsManager when there are updates to |
| * the feature. |
| * @return {@link IImsMmTelFeature} interface or {@link null} if it is unavailable. |
| */ |
| public IImsMmTelFeature getMmTelFeatureAndListen(int slotId, |
| IImsServiceFeatureCallback callback) { |
| ImsServiceController controller = getImsServiceControllerAndListen(slotId, |
| ImsFeature.FEATURE_MMTEL, callback); |
| return (controller != null) ? controller.getMmTelFeature(slotId) : null; |
| } |
| |
| /** |
| * Returns the {@link IImsRcsFeature} that corresponds to the given slot Id for emergency |
| * calling or {@link null} if the service is not available. If an IImsMMTelFeature is |
| * available, the {@link IImsServiceFeatureCallback} callback is registered as a listener for |
| * feature updates. |
| * @param slotId The SIM slot that we are requesting the {@link IImsRcsFeature} for. |
| * @param callback listener that will send updates to ImsManager when there are updates to |
| * the feature. |
| * @return {@link IImsRcsFeature} interface or {@link null} if it is unavailable. |
| */ |
| public IImsRcsFeature getRcsFeatureAndListen(int slotId, IImsServiceFeatureCallback callback) { |
| ImsServiceController controller = getImsServiceControllerAndListen(slotId, |
| ImsFeature.FEATURE_RCS, callback); |
| return (controller != null) ? controller.getRcsFeature(slotId) : null; |
| } |
| |
| /** |
| * Returns the ImsRegistration structure associated with the slotId and feature specified. |
| */ |
| public @Nullable IImsRegistration getImsRegistration(int slotId, int feature) |
| throws RemoteException { |
| ImsServiceController controller = getImsServiceController(slotId, feature); |
| if (controller != null) { |
| return controller.getRegistration(slotId); |
| } |
| return null; |
| } |
| |
| /** |
| * Returns the ImsConfig structure associated with the slotId and feature specified. |
| */ |
| public @Nullable IImsConfig getImsConfig(int slotId, int feature) |
| throws RemoteException { |
| ImsServiceController controller = getImsServiceController(slotId, feature); |
| if (controller != null) { |
| return controller.getConfig(slotId); |
| } |
| return null; |
| } |
| |
| @VisibleForTesting |
| public ImsServiceController getImsServiceController(int slotId, int feature) { |
| if (slotId < 0 || slotId >= mNumSlots) { |
| return null; |
| } |
| ImsServiceController controller; |
| synchronized (mBoundServicesLock) { |
| SparseArray<ImsServiceController> services = mBoundImsServicesByFeature.get(slotId); |
| if (services == null) { |
| return null; |
| } |
| controller = services.get(feature); |
| } |
| return controller; |
| } |
| |
| private SparseArray<ImsServiceController> getImsServiceControllers(int slotId) { |
| if (slotId < 0 || slotId >= mNumSlots) { |
| return null; |
| } |
| synchronized (mBoundServicesLock) { |
| SparseArray<ImsServiceController> services = mBoundImsServicesByFeature.get(slotId); |
| if (services == null) { |
| return null; |
| } |
| return services; |
| } |
| } |
| |
| @VisibleForTesting |
| public ImsServiceController getImsServiceControllerAndListen(int slotId, int feature, |
| IImsServiceFeatureCallback callback) { |
| ImsServiceController controller = getImsServiceController(slotId, feature); |
| |
| if (controller != null) { |
| controller.addImsServiceFeatureCallback(callback); |
| return controller; |
| } |
| return null; |
| } |
| |
| /** |
| * Unregister a previously registered IImsServiceFeatureCallback through |
| * {@link #getImsServiceControllerAndListen(int, int, IImsServiceFeatureCallback)} . |
| * @param slotId The slot id associated with the ImsFeature. |
| * @param feature The {@link ImsFeature.FeatureType} |
| * @param callback The callback to be unregistered. |
| */ |
| public void unregisterImsFeatureCallback(int slotId, int feature, |
| IImsServiceFeatureCallback callback) { |
| ImsServiceController controller = getImsServiceController(slotId, feature); |
| |
| if (controller != null) { |
| controller.removeImsServiceFeatureCallback(callback); |
| } |
| } |
| |
| // Used for testing only. |
| public boolean overrideImsServiceConfiguration(int slotId, boolean isCarrierService, |
| Map<Integer, String> featureConfig) { |
| if (slotId < 0 || slotId >= mNumSlots) { |
| Log.w(TAG, "overrideImsServiceConfiguration: invalid slotId!"); |
| return false; |
| } |
| |
| OverrideConfig overrideConfig = new OverrideConfig(slotId, isCarrierService, featureConfig); |
| Message.obtain(mHandler, HANDLER_OVERRIDE_IMS_SERVICE_CONFIG, overrideConfig) |
| .sendToTarget(); |
| return true; |
| } |
| |
| // not synchronized, access through handler ONLY. |
| private String getDeviceConfiguration(@ImsFeature.FeatureType int featureType) { |
| return mDeviceServices.getOrDefault(featureType, ""); |
| } |
| |
| // not synchronized, access in handler ONLY. |
| private void setDeviceConfiguration(String name, @ImsFeature.FeatureType int featureType) { |
| mDeviceServices.put(featureType, name); |
| } |
| |
| // not synchronized, access in handler ONLY. |
| private void setCarrierConfiguredPackageName(@NonNull String packageName, int slotId, |
| @ImsFeature.FeatureType int featureType) { |
| getCarrierConfiguredPackageNames(slotId).put(featureType, packageName); |
| } |
| |
| // not synchronized, access in handler ONLY. |
| private @NonNull String getCarrierConfiguredPackageName(int slotId, |
| @ImsFeature.FeatureType int featureType) { |
| return getCarrierConfiguredPackageNames(slotId).getOrDefault(featureType, ""); |
| } |
| |
| // not synchronized, access in handler ONLY. |
| private @NonNull Map<Integer, String> getCarrierConfiguredPackageNames(int slotId) { |
| Map<Integer, String> carrierConfig = mCarrierServices.get(slotId); |
| if (carrierConfig == null) { |
| carrierConfig = new ArrayMap<>(); |
| mCarrierServices.put(slotId, carrierConfig); |
| } |
| return carrierConfig; |
| } |
| |
| // not synchronized, access in handler ONLY. |
| private void setOverridePackageName(@Nullable String packageName, int slotId, |
| @ImsFeature.FeatureType int featureType) { |
| getOverridePackageName(slotId).put(featureType, packageName); |
| } |
| |
| // not synchronized, access in handler ONLY. |
| private @Nullable String getOverridePackageName(int slotId, |
| @ImsFeature.FeatureType int featureType) { |
| return getOverridePackageName(slotId).get(featureType); |
| } |
| |
| // not synchronized, access in handler ONLY. |
| private @NonNull SparseArray<String> getOverridePackageName(int slotId) { |
| SparseArray<String> carrierConfig = mOverrideServices.get(slotId); |
| if (carrierConfig == null) { |
| carrierConfig = new SparseArray<>(); |
| mOverrideServices.put(slotId, carrierConfig); |
| } |
| return carrierConfig; |
| } |
| |
| /** |
| * @return true if there is a carrier configuration that exists for the slot & featureType pair |
| * and the cached carrier ImsService associated with the configuration also supports the |
| * requested ImsFeature type. |
| */ |
| // not synchronized, access in handler ONLY. |
| private boolean doesCarrierConfigurationExist(int slotId, |
| @ImsFeature.FeatureType int featureType) { |
| String carrierPackage = getCarrierConfiguredPackageName(slotId, featureType); |
| if (TextUtils.isEmpty(carrierPackage)) { |
| return false; |
| } |
| // Config exists, but the carrier ImsService also needs to support this feature |
| ImsServiceInfo info = getImsServiceInfoFromCache(carrierPackage); |
| return info != null && info.getSupportedFeatures().stream().anyMatch( |
| feature -> feature.slotId == slotId && feature.featureType == featureType); |
| } |
| |
| /** |
| * @return the package name of the ImsService with the requested configuration. |
| */ |
| // used in shell commands queries during testing only. |
| public String getImsServiceConfiguration(int slotId, boolean isCarrierService, |
| @ImsFeature.FeatureType int featureType) { |
| if (slotId < 0 || slotId >= mNumSlots) { |
| Log.w(TAG, "getImsServiceConfiguration: invalid slotId!"); |
| return ""; |
| } |
| |
| LinkedBlockingQueue<String> result = new LinkedBlockingQueue<>(1); |
| // access the configuration on the handler. |
| mHandler.post(() -> result.offer(isCarrierService |
| ? getCarrierConfiguredPackageName(slotId, featureType) : |
| getDeviceConfiguration(featureType))); |
| try { |
| return result.poll(GET_IMS_SERVICE_TIMEOUT_MS, TimeUnit.MILLISECONDS); |
| } catch (InterruptedException e) { |
| Log.w(TAG, "getImsServiceConfiguration: exception=" + e.getMessage()); |
| return null; |
| } |
| } |
| |
| private void putImsController(int slotId, int feature, ImsServiceController controller) { |
| if (slotId < 0 || slotId >= mNumSlots || feature <= ImsFeature.FEATURE_INVALID |
| || feature >= ImsFeature.FEATURE_MAX) { |
| Log.w(TAG, "putImsController received invalid parameters - slot: " + slotId |
| + ", feature: " + feature); |
| return; |
| } |
| synchronized (mBoundServicesLock) { |
| SparseArray<ImsServiceController> services = mBoundImsServicesByFeature.get(slotId); |
| if (services == null) { |
| services = new SparseArray<>(); |
| mBoundImsServicesByFeature.put(slotId, services); |
| } |
| mEventLog.log("putImsController - [" + slotId + ", " |
| + ImsFeature.FEATURE_LOG_MAP.get(feature) + "] -> " + controller); |
| Log.i(TAG, "ImsServiceController added on slot: " + slotId + " with feature: " |
| + ImsFeature.FEATURE_LOG_MAP.get(feature) + " using package: " |
| + controller.getComponentName()); |
| services.put(feature, controller); |
| } |
| } |
| |
| private ImsServiceController removeImsController(int slotId, int feature) { |
| if (slotId < 0 || feature <= ImsFeature.FEATURE_INVALID |
| || feature >= ImsFeature.FEATURE_MAX) { |
| Log.w(TAG, "removeImsController received invalid parameters - slot: " + slotId |
| + ", feature: " + feature); |
| return null; |
| } |
| synchronized (mBoundServicesLock) { |
| SparseArray<ImsServiceController> services = mBoundImsServicesByFeature.get(slotId); |
| if (services == null) { |
| return null; |
| } |
| ImsServiceController c = services.get(feature, null); |
| if (c != null) { |
| mEventLog.log("removeImsController - [" + slotId + ", " |
| + ImsFeature.FEATURE_LOG_MAP.get(feature) + "] -> " + c); |
| Log.i(TAG, "ImsServiceController removed on slot: " + slotId + " with feature: " |
| + ImsFeature.FEATURE_LOG_MAP.get(feature) + " using package: " |
| + c.getComponentName()); |
| services.remove(feature); |
| } |
| return c; |
| } |
| } |
| |
| // Update the current cache with the new ImsService(s) if it has been added or update the |
| // supported IMS features if they have changed. |
| // Called from the handler ONLY |
| private void maybeAddedImsService(String packageName) { |
| Log.d(TAG, "maybeAddedImsService, packageName: " + packageName); |
| List<ImsServiceInfo> infos = getImsServiceInfo(packageName); |
| // Wait until all ImsServiceInfo is cached before calling |
| // calculateFeatureConfigurationChange to reduce churn. |
| boolean requiresCalculation = false; |
| for (ImsServiceInfo info : infos) { |
| // Checking to see if the ComponentName is the same, so we can update the supported |
| // features. Will only be one (if it exists), since it is a set. |
| ImsServiceInfo match = getInfoByComponentName(mInstalledServicesCache, info.name); |
| if (match != null) { |
| // for dynamic query the new "info" will have no supported features yet. Don't wipe |
| // out the cache for the existing features or update yet. Instead start a query |
| // for features dynamically. |
| if (info.featureFromMetadata) { |
| mEventLog.log("maybeAddedImsService - updating features for " + info.name |
| + ": " + printFeatures(match.getSupportedFeatures()) + " -> " |
| + printFeatures(info.getSupportedFeatures())); |
| Log.i(TAG, "Updating features in cached ImsService: " + info.name); |
| Log.d(TAG, "Updating features - Old features: " + match + " new features: " |
| + info); |
| // update features in the cache |
| match.replaceFeatures(info.getSupportedFeatures()); |
| requiresCalculation = true; |
| } else { |
| mEventLog.log("maybeAddedImsService - scheduling query for " + info); |
| // start a query to get ImsService features |
| scheduleQueryForFeatures(info); |
| } |
| } else { |
| Log.i(TAG, "Adding newly added ImsService to cache: " + info.name); |
| mEventLog.log("maybeAddedImsService - adding new ImsService: " + info); |
| mInstalledServicesCache.put(info.name, info); |
| if (info.featureFromMetadata) { |
| requiresCalculation = true; |
| } else { |
| // newly added ImsServiceInfo that has not had features queried yet. Start async |
| // bind and query features. |
| scheduleQueryForFeatures(info); |
| } |
| } |
| } |
| if (requiresCalculation) calculateFeatureConfigurationChange(); |
| } |
| |
| // Remove the ImsService from the cache. This may have been due to the ImsService being removed |
| // from the device or was returning permanent errors when bound. |
| // Called from the handler ONLY |
| private boolean maybeRemovedImsService(String packageName) { |
| ImsServiceInfo match = getInfoByPackageName(mInstalledServicesCache, packageName); |
| if (match != null) { |
| mInstalledServicesCache.remove(match.name); |
| mEventLog.log("maybeRemovedImsService - removing ImsService: " + match); |
| Log.i(TAG, "Removing ImsService: " + match.name); |
| unbindImsService(match); |
| calculateFeatureConfigurationChange(); |
| return true; |
| } |
| return false; |
| } |
| |
| private boolean isDeviceService(ImsServiceInfo info) { |
| if (info == null) return false; |
| return mDeviceServices.containsValue(info.name.getPackageName()); |
| } |
| |
| private List<Integer> getSlotsForActiveCarrierService(ImsServiceInfo info) { |
| if (info == null) return Collections.emptyList(); |
| List<Integer> slots = new ArrayList<>(mNumSlots); |
| for (int i = 0; i < mNumSlots; i++) { |
| if (!TextUtils.isEmpty(getCarrierConfiguredPackageNames(i).values().stream() |
| .filter(e -> e.equals(info.name.getPackageName())).findAny().orElse(""))) { |
| slots.add(i); |
| } |
| } |
| return slots; |
| } |
| |
| private ImsServiceController getControllerByServiceInfo( |
| Map<ComponentName, ImsServiceController> searchMap, ImsServiceInfo matchValue) { |
| return searchMap.values().stream() |
| .filter(c -> Objects.equals(c.getComponentName(), matchValue.name)) |
| .findFirst().orElse(null); |
| } |
| |
| private ImsServiceInfo getInfoByPackageName(Map<ComponentName, ImsServiceInfo> searchMap, |
| String matchValue) { |
| return searchMap.values().stream() |
| .filter((i) -> Objects.equals(i.name.getPackageName(), matchValue)) |
| .findFirst().orElse(null); |
| } |
| |
| private ImsServiceInfo getInfoByComponentName( |
| Map<ComponentName, ImsServiceInfo> searchMap, ComponentName matchValue) { |
| return searchMap.get(matchValue); |
| } |
| |
| private void bindImsServiceWithFeatures(ImsServiceInfo info, |
| Set<ImsFeatureConfiguration.FeatureSlotPair> features) { |
| // Only bind if there are features that will be created by the service. |
| if (shouldFeaturesCauseBind(features)) { |
| // Check to see if an active controller already exists |
| ImsServiceController controller = getControllerByServiceInfo(mActiveControllers, info); |
| if (controller != null) { |
| Log.i(TAG, "ImsService connection exists for " + info.name + ", updating features " |
| + features); |
| try { |
| controller.changeImsServiceFeatures(features); |
| // Features have been set, there was an error adding/removing. When the |
| // controller recovers, it will add/remove again. |
| } catch (RemoteException e) { |
| Log.w(TAG, "bindImsService: error=" + e.getMessage()); |
| } |
| } else { |
| controller = info.controllerFactory.create(mContext, info.name, this); |
| Log.i(TAG, "Binding ImsService: " + controller.getComponentName() |
| + " with features: " + features); |
| controller.bind(features); |
| mEventLog.log("bindImsServiceWithFeatures - create new controller: " |
| + controller); |
| } |
| mActiveControllers.put(info.name, controller); |
| } |
| } |
| |
| // Clean up and unbind from an ImsService |
| private void unbindImsService(ImsServiceInfo info) { |
| if (info == null) { |
| return; |
| } |
| ImsServiceController controller = getControllerByServiceInfo(mActiveControllers, info); |
| if (controller != null) { |
| // Calls imsServiceFeatureRemoved on all features in the controller |
| try { |
| Log.i(TAG, "Unbinding ImsService: " + controller.getComponentName()); |
| mEventLog.log("unbindImsService - unbinding and removing " + controller); |
| controller.unbind(); |
| } catch (RemoteException e) { |
| Log.e(TAG, "unbindImsService: Remote Exception: " + e.getMessage()); |
| } |
| mActiveControllers.remove(info.name); |
| } |
| } |
| |
| // Calculate which features an ImsServiceController will need. If it is the carrier specific |
| // ImsServiceController, it will be granted all of the features it requests on the associated |
| // slot. If it is the device ImsService, it will get all of the features not covered by the |
| // carrier implementation. |
| private HashSet<ImsFeatureConfiguration.FeatureSlotPair> calculateFeaturesToCreate( |
| ImsServiceInfo info) { |
| HashSet<ImsFeatureConfiguration.FeatureSlotPair> imsFeaturesBySlot = new HashSet<>(); |
| List<Integer> slots = getSlotsForActiveCarrierService(info); |
| if (!slots.isEmpty()) { |
| // There is an active carrier config associated with this. Return with the ImsService's |
| // supported features that are also within the carrier configuration |
| imsFeaturesBySlot.addAll(info.getSupportedFeatures().stream() |
| .filter(feature -> info.name.getPackageName().equals( |
| getCarrierConfiguredPackageName(feature.slotId, feature.featureType))) |
| .collect(Collectors.toList())); |
| return imsFeaturesBySlot; |
| } |
| if (isDeviceService(info)) { |
| imsFeaturesBySlot.addAll(info.getSupportedFeatures().stream() |
| // only allow supported features that are also set for this package as the |
| // device configuration. |
| .filter(feature -> info.name.getPackageName().equals( |
| getDeviceConfiguration(feature.featureType))) |
| // filter out any separate carrier configuration, since that feature is handled |
| // by the carrier ImsService. |
| .filter(feature -> !doesCarrierConfigurationExist(feature.slotId, |
| feature.featureType)) |
| .collect(Collectors.toList())); |
| } |
| return imsFeaturesBySlot; |
| } |
| |
| /** |
| * Implementation of |
| * {@link ImsServiceController.ImsServiceControllerCallbacks#imsServiceFeatureCreated}, which |
| * removes the ImsServiceController from the mBoundImsServicesByFeature structure. |
| */ |
| public void imsServiceFeatureCreated(int slotId, int feature, ImsServiceController controller) { |
| putImsController(slotId, feature, controller); |
| } |
| |
| /** |
| * Implementation of |
| * {@link ImsServiceController.ImsServiceControllerCallbacks#imsServiceFeatureRemoved}, which |
| * removes the ImsServiceController from the mBoundImsServicesByFeature structure. |
| */ |
| public void imsServiceFeatureRemoved(int slotId, int feature, ImsServiceController controller) { |
| removeImsController(slotId, feature); |
| } |
| |
| /** |
| * Implementation of |
| * {@link ImsServiceController.ImsServiceControllerCallbacks#imsServiceFeaturesChanged, which |
| * notify the ImsResolver of a change to the supported ImsFeatures of a connected ImsService. |
| */ |
| public void imsServiceFeaturesChanged(ImsFeatureConfiguration config, |
| ImsServiceController controller) { |
| if (controller == null || config == null) { |
| return; |
| } |
| Log.i(TAG, "imsServiceFeaturesChanged: config=" + config.getServiceFeatures() |
| + ", ComponentName=" + controller.getComponentName()); |
| mEventLog.log("imsServiceFeaturesChanged - for " + controller + ", new config " |
| + config.getServiceFeatures()); |
| handleFeaturesChanged(controller.getComponentName(), config.getServiceFeatures()); |
| } |
| |
| @Override |
| public void imsServiceBindPermanentError(ComponentName name) { |
| if (name == null) { |
| return; |
| } |
| Log.w(TAG, "imsServiceBindPermanentError: component=" + name); |
| mEventLog.log("imsServiceBindPermanentError - for " + name); |
| mHandler.obtainMessage(HANDLER_REMOVE_PACKAGE, name.getPackageName()).sendToTarget(); |
| } |
| |
| /** |
| * Determines if the features specified should cause a bind or keep a binding active to an |
| * ImsService. |
| * @return true if MMTEL or RCS features are present, false if they are not or only |
| * EMERGENCY_MMTEL is specified. |
| */ |
| private boolean shouldFeaturesCauseBind(Set<ImsFeatureConfiguration.FeatureSlotPair> features) { |
| long bindableFeatures = features.stream() |
| // remove all emergency features |
| .filter(f -> f.featureType != ImsFeature.FEATURE_EMERGENCY_MMTEL).count(); |
| return bindableFeatures > 0; |
| } |
| |
| // Possibly rebind to another ImsService for testing carrier ImsServices. |
| // Called from the handler ONLY |
| private void overrideCarrierService(int slotId, Map<Integer, String> featureMap) { |
| for (Integer featureType : featureMap.keySet()) { |
| String overridePackageName = featureMap.get(featureType); |
| mEventLog.log("overriding carrier ImsService to " + overridePackageName |
| + " on slot " + slotId + " for feature " |
| + ImsFeature.FEATURE_LOG_MAP.getOrDefault(featureType, "invalid")); |
| setOverridePackageName(overridePackageName, slotId, featureType); |
| } |
| updateBoundServices(slotId, Collections.emptyMap()); |
| } |
| |
| // Possibly rebind to another ImsService for testing carrier ImsServices. |
| // Called from the handler ONLY |
| private void overrideDeviceService(Map<Integer, String> featureMap) { |
| boolean requiresRecalc = false; |
| for (Integer featureType : featureMap.keySet()) { |
| String overridePackageName = featureMap.get(featureType); |
| mEventLog.log("overriding device ImsService to " + overridePackageName + " for feature " |
| + ImsFeature.FEATURE_LOG_MAP.getOrDefault(featureType, "invalid")); |
| String oldPackageName = getDeviceConfiguration(featureType); |
| if (!TextUtils.equals(oldPackageName, overridePackageName)) { |
| Log.i(TAG, "overrideDeviceService - device package changed (override): " |
| + oldPackageName + " -> " + overridePackageName); |
| mEventLog.log("overrideDeviceService - device package changed (override): " |
| + oldPackageName + " -> " + overridePackageName); |
| setDeviceConfiguration(overridePackageName, featureType); |
| ImsServiceInfo info = getImsServiceInfoFromCache(overridePackageName); |
| if (info == null || info.featureFromMetadata) { |
| requiresRecalc = true; |
| } else { |
| // Config will change when this query completes |
| scheduleQueryForFeatures(info); |
| } |
| } |
| } |
| if (requiresRecalc) calculateFeatureConfigurationChange(); |
| } |
| |
| // Called from handler ONLY. |
| private void carrierConfigChanged(int slotId) { |
| updateBoundDeviceServices(); |
| |
| if (slotId <= SubscriptionManager.INVALID_SIM_SLOT_INDEX) { |
| // not specified, update carrier override cache and possibly rebind on all slots. |
| for (int i = 0; i < mNumSlots; i++) { |
| updateBoundServices(i, getImsPackageOverrideConfig(i)); |
| } |
| } |
| updateBoundServices(slotId, getImsPackageOverrideConfig(slotId)); |
| } |
| |
| private void updateBoundDeviceServices() { |
| Log.d(TAG, "updateBoundDeviceServices: called"); |
| ArrayMap<String, ImsServiceInfo> featureDynamicImsPackages = new ArrayMap<>(); |
| for (int f = ImsFeature.FEATURE_EMERGENCY_MMTEL; f < ImsFeature.FEATURE_MAX; f++) { |
| String packageName = getDeviceConfiguration(f); |
| ImsServiceInfo serviceInfo = getImsServiceInfoFromCache(packageName); |
| if (serviceInfo != null && !serviceInfo.featureFromMetadata |
| && !featureDynamicImsPackages.containsKey(packageName)) { |
| featureDynamicImsPackages.put(packageName, serviceInfo); |
| |
| Log.d(TAG, "updateBoundDeviceServices: Schedule query for package=" + packageName); |
| scheduleQueryForFeatures(featureDynamicImsPackages.get(packageName)); |
| } |
| } |
| } |
| |
| private void updateBoundServices(int slotId, Map<Integer, String> featureMap) { |
| if (slotId <= SubscriptionManager.INVALID_SIM_SLOT_INDEX || slotId >= mNumSlots) { |
| return; |
| } |
| boolean hasConfigChanged = false; |
| boolean didQuerySchedule = false; |
| for (int f = ImsFeature.FEATURE_EMERGENCY_MMTEL; f < ImsFeature.FEATURE_MAX; f++) { |
| String overridePackageName = getOverridePackageName(slotId, f); |
| String oldPackageName = getCarrierConfiguredPackageName(slotId, f); |
| String newPackageName = featureMap.getOrDefault(f, ""); |
| if (!TextUtils.isEmpty(overridePackageName)) { |
| // Do not allow carrier config changes to change the override package while it |
| // is in effect. |
| Log.i(TAG, String.format("updateBoundServices: overriding %s with %s for feature" |
| + " %s on slot %d", |
| TextUtils.isEmpty(newPackageName) ? "(none)" : newPackageName, |
| overridePackageName, |
| ImsFeature.FEATURE_LOG_MAP.getOrDefault(f, "invalid"), slotId)); |
| newPackageName = overridePackageName; |
| } |
| |
| setCarrierConfiguredPackageName(newPackageName, slotId, f); |
| // Carrier config may have not changed, but we still want to kick off a recalculation |
| // in case there has been a change to the supported device features. |
| ImsServiceInfo info = getImsServiceInfoFromCache(newPackageName); |
| mEventLog.log("updateBoundServices - carrier package changed: " |
| + oldPackageName + " -> " + newPackageName + " on slot " + slotId |
| + ", hasConfigChanged=" + hasConfigChanged); |
| if (info == null || info.featureFromMetadata) { |
| hasConfigChanged = true; |
| } else { |
| // Config will change when this query completes |
| scheduleQueryForFeatures(info); |
| didQuerySchedule = true; |
| } |
| } |
| if (hasConfigChanged) calculateFeatureConfigurationChange(); |
| |
| if (hasConfigChanged && didQuerySchedule) { |
| mEventLog.log("[warning] updateBoundServices - both hasConfigChange and query " |
| + "scheduled on slot " + slotId); |
| } |
| } |
| |
| private @NonNull Map<Integer, String> getImsPackageOverrideConfig(int slotId) { |
| int subId = mSubscriptionManagerProxy.getSubId(slotId); |
| PersistableBundle config = mCarrierConfigManager.getConfigForSubId(subId); |
| if (config == null) return Collections.emptyMap(); |
| String packageNameMmTel = config.getString( |
| CarrierConfigManager.KEY_CONFIG_IMS_PACKAGE_OVERRIDE_STRING, null); |
| // Set the config equal for the deprecated key. |
| String packageNameRcs = packageNameMmTel; |
| packageNameMmTel = config.getString( |
| CarrierConfigManager.KEY_CONFIG_IMS_MMTEL_PACKAGE_OVERRIDE_STRING, |
| packageNameMmTel); |
| packageNameRcs = config.getString( |
| CarrierConfigManager.KEY_CONFIG_IMS_RCS_PACKAGE_OVERRIDE_STRING, packageNameRcs); |
| Map<Integer, String> result = new ArrayMap<>(); |
| if (!TextUtils.isEmpty(packageNameMmTel)) { |
| result.put(ImsFeature.FEATURE_EMERGENCY_MMTEL, packageNameMmTel); |
| result.put(ImsFeature.FEATURE_MMTEL, packageNameMmTel); |
| } |
| if (!TextUtils.isEmpty(packageNameRcs)) { |
| result.put(ImsFeature.FEATURE_RCS, packageNameRcs); |
| } |
| return result; |
| } |
| |
| /** |
| * Schedules a query for dynamic ImsService features. |
| */ |
| private void scheduleQueryForFeatures(ImsServiceInfo service, int delayMs) { |
| if (service == null) { |
| return; |
| } |
| Message msg = Message.obtain(mHandler, HANDLER_START_DYNAMIC_FEATURE_QUERY, service); |
| if (mHandler.hasMessages(HANDLER_START_DYNAMIC_FEATURE_QUERY, service)) { |
| Log.d(TAG, "scheduleQueryForFeatures: dynamic query for " + service.name |
| + " already scheduled"); |
| return; |
| } |
| Log.d(TAG, "scheduleQueryForFeatures: starting dynamic query for " + service.name |
| + " in " + delayMs + "ms."); |
| mHandler.sendMessageDelayed(msg, delayMs); |
| } |
| |
| private void scheduleQueryForFeatures(ComponentName name, int delayMs) { |
| ImsServiceInfo service = getImsServiceInfoFromCache(name.getPackageName()); |
| if (service == null) { |
| Log.w(TAG, "scheduleQueryForFeatures: Couldn't find cached info for name: " + name); |
| return; |
| } |
| scheduleQueryForFeatures(service, delayMs); |
| } |
| |
| private void scheduleQueryForFeatures(ImsServiceInfo service) { |
| scheduleQueryForFeatures(service, 0); |
| } |
| |
| /** |
| * Schedules the processing of a completed query. |
| */ |
| private void handleFeaturesChanged(ComponentName name, |
| Set<ImsFeatureConfiguration.FeatureSlotPair> features) { |
| SomeArgs args = SomeArgs.obtain(); |
| args.arg1 = name; |
| args.arg2 = features; |
| mHandler.obtainMessage(HANDLER_DYNAMIC_FEATURE_CHANGE, args).sendToTarget(); |
| } |
| |
| private void handleMsimConfigChange(Integer newNumSlots) { |
| int oldLen = mNumSlots; |
| if (oldLen == newNumSlots) { |
| return; |
| } |
| mNumSlots = newNumSlots; |
| Log.i(TAG, "handleMsimConfigChange: oldLen=" + oldLen + ", newLen=" + newNumSlots); |
| mEventLog.log("MSIM config change: " + oldLen + " -> " + newNumSlots); |
| if (newNumSlots < oldLen) { |
| // we need to trim data structures that use slots, however mBoundImsServicesByFeature |
| // will be updated by ImsServiceController changing to remove features on old slots. |
| // start at the index of the new highest slot + 1. |
| for (int oldSlot = newNumSlots; oldSlot < oldLen; oldSlot++) { |
| // First clear old carrier configs |
| Map<Integer, String> carrierConfigs = getCarrierConfiguredPackageNames(oldSlot); |
| for (Integer feature : carrierConfigs.keySet()) { |
| setCarrierConfiguredPackageName("", oldSlot, feature); |
| } |
| // next clear old overrides |
| SparseArray<String> overrideConfigs = getOverridePackageName(oldSlot); |
| for (int i = 0; i < overrideConfigs.size(); i++) { |
| int feature = overrideConfigs.keyAt(i); |
| setOverridePackageName("", oldSlot, feature); |
| } |
| } |
| } |
| // Get the new config for each ImsService. For manifest queries, this will update the |
| // number of slots. |
| // This will get all services with the correct intent filter from PackageManager |
| List<ImsServiceInfo> infos = getImsServiceInfo(null); |
| for (ImsServiceInfo info : infos) { |
| ImsServiceInfo cachedInfo = mInstalledServicesCache.get(info.name); |
| if (cachedInfo != null) { |
| if (info.featureFromMetadata) { |
| cachedInfo.replaceFeatures(info.getSupportedFeatures()); |
| } else { |
| // Remove features that are no longer supported by the device configuration. |
| cachedInfo.getSupportedFeatures() |
| .removeIf(filter -> filter.slotId >= newNumSlots); |
| } |
| } else { |
| // This is unexpected, put the new service on the queue to be added |
| mEventLog.log("handleMsimConfigChange: detected untracked service - " + info); |
| Log.w(TAG, "handleMsimConfigChange: detected untracked package, queueing to add " |
| + info); |
| mHandler.obtainMessage(HANDLER_ADD_PACKAGE, |
| info.name.getPackageName()).sendToTarget(); |
| } |
| } |
| |
| if (newNumSlots < oldLen) { |
| // A CarrierConfigChange will happen for the new slot, so only recalculate if there are |
| // less new slots because we need to remove the old capabilities. |
| calculateFeatureConfigurationChange(); |
| } |
| } |
| |
| // Starts a dynamic query. Called from handler ONLY. |
| private void startDynamicQuery(ImsServiceInfo service) { |
| // if not current device/carrier service, don't perform query. If this changes, this method |
| // will be called again. |
| if (!isDeviceService(service) && getSlotsForActiveCarrierService(service).isEmpty()) { |
| Log.i(TAG, "scheduleQueryForFeatures: skipping query for ImsService that is not" |
| + " set as carrier/device ImsService."); |
| return; |
| } |
| mEventLog.log("startDynamicQuery - starting query for " + service); |
| boolean queryStarted = mFeatureQueryManager.startQuery(service.name, |
| service.controllerFactory.getServiceInterface()); |
| if (!queryStarted) { |
| Log.w(TAG, "startDynamicQuery: service could not connect. Retrying after delay."); |
| mEventLog.log("startDynamicQuery - query failed. Retrying in " |
| + DELAY_DYNAMIC_QUERY_MS + " mS"); |
| scheduleQueryForFeatures(service, DELAY_DYNAMIC_QUERY_MS); |
| } else { |
| Log.d(TAG, "startDynamicQuery: Service queried, waiting for response."); |
| } |
| } |
| |
| // process complete dynamic query. Called from handler ONLY. |
| private void dynamicQueryComplete(ComponentName name, |
| Set<ImsFeatureConfiguration.FeatureSlotPair> features) { |
| ImsServiceInfo service = getImsServiceInfoFromCache(name.getPackageName()); |
| if (service == null) { |
| Log.w(TAG, "dynamicQueryComplete: Couldn't find cached info for name: " |
| + name); |
| return; |
| } |
| mEventLog.log("dynamicQueryComplete: for package " + name + ", features: " |
| + printFeatures(service.getSupportedFeatures()) + " -> " + printFeatures(features)); |
| sanitizeFeatureConfig(features); |
| // Add features to service |
| service.replaceFeatures(features); |
| // Wait until all queries have completed before changing the configuration to reduce churn. |
| if (!mFeatureQueryManager.isQueryInProgress()) { |
| if (mHandler.hasMessages(HANDLER_DYNAMIC_FEATURE_CHANGE)) { |
| mEventLog.log("[warning] dynamicQueryComplete - HANDLER_DYNAMIC_FEATURE_CHANGE " |
| + "pending with calculateFeatureConfigurationChange()"); |
| } |
| calculateFeatureConfigurationChange(); |
| } |
| } |
| |
| /** |
| * Ensure the feature includes MMTEL when it supports EMERGENCY_MMTEL, if not, remove. |
| */ |
| private void sanitizeFeatureConfig(Set<ImsFeatureConfiguration.FeatureSlotPair> features) { |
| Set<ImsFeatureConfiguration.FeatureSlotPair> emergencyMmtelFeatures = features.stream() |
| .filter(feature -> feature.featureType == ImsFeature.FEATURE_EMERGENCY_MMTEL) |
| .collect(Collectors.toSet()); |
| for (ImsFeatureConfiguration.FeatureSlotPair feature : emergencyMmtelFeatures) { |
| if (!features.contains(new ImsFeatureConfiguration.FeatureSlotPair(feature.slotId, |
| ImsFeature.FEATURE_MMTEL))) { |
| features.remove(feature); |
| } |
| } |
| } |
| |
| // Calculate the new configuration for the bound ImsServices. |
| // Should ONLY be called from the handler. |
| private void calculateFeatureConfigurationChange() { |
| for (ImsServiceInfo info : mInstalledServicesCache.values()) { |
| Set<ImsFeatureConfiguration.FeatureSlotPair> features = calculateFeaturesToCreate(info); |
| if (shouldFeaturesCauseBind(features)) { |
| bindImsServiceWithFeatures(info, features); |
| } else { |
| unbindImsService(info); |
| } |
| } |
| } |
| |
| private static String printFeatures(Set<ImsFeatureConfiguration.FeatureSlotPair> features) { |
| StringBuilder featureString = new StringBuilder(); |
| featureString.append(" features: ["); |
| if (features != null) { |
| for (ImsFeatureConfiguration.FeatureSlotPair feature : features) { |
| featureString.append("{"); |
| featureString.append(feature.slotId); |
| featureString.append(","); |
| featureString.append(ImsFeature.FEATURE_LOG_MAP.get(feature.featureType)); |
| featureString.append("}"); |
| } |
| featureString.append("]"); |
| } |
| return featureString.toString(); |
| } |
| |
| /** |
| * Returns the ImsServiceInfo that matches the provided packageName. Visible for testing |
| * the ImsService caching functionality. |
| */ |
| @VisibleForTesting |
| public ImsServiceInfo getImsServiceInfoFromCache(String packageName) { |
| if (TextUtils.isEmpty(packageName)) { |
| return null; |
| } |
| ImsServiceInfo infoFilter = getInfoByPackageName(mInstalledServicesCache, packageName); |
| if (infoFilter != null) { |
| return infoFilter; |
| } else { |
| return null; |
| } |
| } |
| |
| // Return the ImsServiceInfo specified for the package name. If the package name is null, |
| // get all packages that support ImsServices. |
| private List<ImsServiceInfo> getImsServiceInfo(String packageName) { |
| List<ImsServiceInfo> infos = new ArrayList<>(); |
| // Search for Current ImsService implementations |
| infos.addAll(searchForImsServices(packageName, mImsServiceControllerFactory)); |
| // Search for compat ImsService Implementations |
| infos.addAll(searchForImsServices(packageName, mImsServiceControllerFactoryCompat)); |
| return infos; |
| } |
| |
| private List<ImsServiceInfo> searchForImsServices(String packageName, |
| ImsServiceControllerFactory controllerFactory) { |
| List<ImsServiceInfo> infos = new ArrayList<>(); |
| |
| Intent serviceIntent = new Intent(controllerFactory.getServiceInterface()); |
| serviceIntent.setPackage(packageName); |
| |
| PackageManager packageManager = mContext.getPackageManager(); |
| for (ResolveInfo entry : packageManager.queryIntentServicesAsUser( |
| serviceIntent, |
| PackageManager.GET_META_DATA, |
| UserHandle.getUserHandleForUid(UserHandle.myUserId()))) { |
| ServiceInfo serviceInfo = entry.serviceInfo; |
| |
| if (serviceInfo != null) { |
| ImsServiceInfo info = new ImsServiceInfo(); |
| info.name = new ComponentName(serviceInfo.packageName, serviceInfo.name); |
| info.controllerFactory = controllerFactory; |
| |
| // we will allow the manifest method of declaring manifest features in two cases: |
| // 1) it is the device overlay "default" ImsService, where the features do not |
| // change (the new method can still be used if the default does not define manifest |
| // entries). |
| // 2) using the "compat" ImsService, which only supports manifest query. |
| if (isDeviceService(info) |
| || mImsServiceControllerFactoryCompat == controllerFactory) { |
| if (serviceInfo.metaData != null) { |
| if (serviceInfo.metaData.getBoolean(METADATA_MMTEL_FEATURE, false)) { |
| info.addFeatureForAllSlots(mNumSlots, ImsFeature.FEATURE_MMTEL); |
| // only allow FEATURE_EMERGENCY_MMTEL if FEATURE_MMTEL is defined. |
| if (serviceInfo.metaData.getBoolean(METADATA_EMERGENCY_MMTEL_FEATURE, |
| false)) { |
| info.addFeatureForAllSlots(mNumSlots, |
| ImsFeature.FEATURE_EMERGENCY_MMTEL); |
| } |
| } |
| if (serviceInfo.metaData.getBoolean(METADATA_RCS_FEATURE, false)) { |
| info.addFeatureForAllSlots(mNumSlots, ImsFeature.FEATURE_RCS); |
| } |
| } |
| // Only dynamic query if we are not a compat version of ImsService and the |
| // default service. |
| if (mImsServiceControllerFactoryCompat != controllerFactory |
| && info.getSupportedFeatures().isEmpty()) { |
| // metadata empty, try dynamic query instead |
| info.featureFromMetadata = false; |
| } |
| } else { |
| // We are a carrier service and not using the compat version of ImsService. |
| info.featureFromMetadata = false; |
| } |
| Log.i(TAG, "service name: " + info.name + ", manifest query: " |
| + info.featureFromMetadata); |
| // Check manifest permission to be sure that the service declares the correct |
| // permissions. Overridden if the METADATA_OVERRIDE_PERM_CHECK metadata is set to |
| // true. |
| // NOTE: METADATA_OVERRIDE_PERM_CHECK should only be set for testing. |
| if (TextUtils.equals(serviceInfo.permission, Manifest.permission.BIND_IMS_SERVICE) |
| || serviceInfo.metaData.getBoolean(METADATA_OVERRIDE_PERM_CHECK, false)) { |
| infos.add(info); |
| } else { |
| Log.w(TAG, "ImsService is not protected with BIND_IMS_SERVICE permission: " |
| + info.name); |
| } |
| } |
| } |
| return infos; |
| } |
| |
| // Dump is called on the main thread, since ImsResolver Handler is also handled on main thread, |
| // we shouldn't need to worry about concurrent access of private params. |
| public void dump(FileDescriptor fd, PrintWriter printWriter, String[] args) { |
| IndentingPrintWriter pw = new IndentingPrintWriter(printWriter, " "); |
| pw.println("ImsResolver:"); |
| pw.increaseIndent(); |
| pw.println("Configurations:"); |
| pw.increaseIndent(); |
| pw.println("Device:"); |
| pw.increaseIndent(); |
| for (Integer i : mDeviceServices.keySet()) { |
| pw.println(ImsFeature.FEATURE_LOG_MAP.get(i) + " -> " + mDeviceServices.get(i)); |
| } |
| pw.decreaseIndent(); |
| pw.println("Carrier: "); |
| pw.increaseIndent(); |
| for (int i = 0; i < mNumSlots; i++) { |
| for (int j = 0; j < MmTelFeature.FEATURE_MAX; j++) { |
| pw.print("slot="); |
| pw.print(i); |
| pw.print(", feature="); |
| pw.print(ImsFeature.FEATURE_LOG_MAP.getOrDefault(j, "?")); |
| pw.println(": "); |
| pw.increaseIndent(); |
| String name = getCarrierConfiguredPackageName(i, j); |
| pw.println(TextUtils.isEmpty(name) ? "none" : name); |
| pw.decreaseIndent(); |
| } |
| } |
| pw.decreaseIndent(); |
| pw.decreaseIndent(); |
| pw.println("Bound Features:"); |
| pw.increaseIndent(); |
| for (int i = 0; i < mNumSlots; i++) { |
| for (int j = 0; j < MmTelFeature.FEATURE_MAX; j++) { |
| pw.print("slot="); |
| pw.print(i); |
| pw.print(", feature="); |
| pw.print(ImsFeature.FEATURE_LOG_MAP.getOrDefault(j, "?")); |
| pw.println(": "); |
| pw.increaseIndent(); |
| ImsServiceController c = getImsServiceController(i, j); |
| pw.println(c == null ? "none" : c); |
| pw.decreaseIndent(); |
| } |
| } |
| pw.decreaseIndent(); |
| pw.println("Cached ImsServices:"); |
| pw.increaseIndent(); |
| for (ImsServiceInfo i : mInstalledServicesCache.values()) { |
| pw.println(i); |
| } |
| pw.decreaseIndent(); |
| pw.println("Active controllers:"); |
| pw.increaseIndent(); |
| for (ImsServiceController c : mActiveControllers.values()) { |
| pw.println(c); |
| pw.increaseIndent(); |
| c.dump(pw); |
| pw.decreaseIndent(); |
| } |
| pw.decreaseIndent(); |
| pw.println("Event Log:"); |
| pw.increaseIndent(); |
| mEventLog.dump(pw); |
| pw.decreaseIndent(); |
| } |
| } |