blob: 8059ec07b22a993911e8b51d0e9261d2dc6184f5 [file] [log] [blame]
/*
* 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.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.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.PersistableBundle;
import android.os.RemoteException;
import android.os.UserHandle;
import android.telephony.CarrierConfigManager;
import android.telephony.SubscriptionManager;
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.stub.ImsFeatureConfiguration;
import android.text.TextUtils;
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 java.util.ArrayList;
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.stream.Collectors;
import java.util.stream.Stream;
/**
* Creates a list of ImsServices that are available to bind to based on the Device configuration
* overlay value "config_ims_package" and Carrier Configuration value
* "config_ims_package_override_string".
* These ImsServices are then bound to in the following order:
*
* 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";
public static final String METADATA_EMERGENCY_MMTEL_FEATURE =
"android.telephony.ims.EMERGENCY_MMTEL_FEATURE";
public static final String METADATA_MMTEL_FEATURE = "android.telephony.ims.MMTEL_FEATURE";
public static final String METADATA_RCS_FEATURE = "android.telephony.ims.RCS_FEATURE";
// 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 query to request ImsService features has completed or the ImsService has updated features.
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;
// Delay between dynamic ImsService queries.
private static final int DELAY_DYNAMIC_QUERY_MS = 5000;
/**
* 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;
private final int mNumSlots;
public ImsServiceInfo(int numSlots) {
mNumSlots = numSlots;
mSupportedFeatures = new HashSet<>();
}
void addFeatureForAllSlots(int feature) {
for (int i = 0; i < mNumSlots; i++) {
mSupportedFeatures.add(new ImsFeatureConfiguration.FeatureSlotPair(i, feature));
}
}
void replaceFeatures(Set<ImsFeatureConfiguration.FeatureSlotPair> newFeatures) {
mSupportedFeatures.clear();
mSupportedFeatures.addAll(newFeatures);
}
@VisibleForTesting
public HashSet<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() {
StringBuilder res = new StringBuilder();
res.append("[ImsServiceInfo] name=");
res.append(name);
res.append(", supportedFeatures=[ ");
for (ImsFeatureConfiguration.FeatureSlotPair feature : mSupportedFeatures) {
res.append("(");
res.append(feature.slotId);
res.append(",");
res.append(feature.featureType);
res.append(") ");
}
return res.toString();
}
}
// 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 SIM change for invalid slot id.");
return;
}
Log.i(TAG, "Received Carrier Config Changed for SlotId: " + slotId);
mHandler.obtainMessage(HANDLER_CONFIG_CHANGED, slotId).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);
}
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 ImsServiceControllerFactory mImsServiceControllerFactoryStaticBindingCompat =
new ImsServiceControllerFactory() {
@Override
public String getServiceInterface() {
// The static method of binding does not use service interfaces.
return null;
}
@Override
public ImsServiceController create(Context context, ComponentName componentName,
ImsServiceController.ImsServiceControllerCallbacks callbacks) {
return new ImsServiceControllerStaticCompat(context, componentName, callbacks);
}
};
private ImsDynamicQueryManagerFactory mDynamicQueryManagerFactory =
ImsServiceFeatureQueryManager::new;
private final CarrierConfigManager mCarrierConfigManager;
private final Context mContext;
// Locks mBoundImsServicesByFeature only. Be careful to avoid deadlocks from
// ImsServiceController callbacks.
private final Object mBoundServicesLock = new Object();
private final int mNumSlots;
private final boolean mIsDynamicBinding;
// Package name of the default device service.
private String mDeviceService;
// Synchronize all messages 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_CONFIG_CHANGED: {
int slotId = (Integer) msg.obj;
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: {
int slotId = msg.arg1;
// arg2 will be equal to 1 if it is a carrier service.
boolean isCarrierImsService = (msg.arg2 == 1);
String packageName = (String) msg.obj;
if (isCarrierImsService) {
Log.i(TAG, "overriding carrier ImsService - slot=" + slotId + " packageName="
+ packageName);
maybeRebindService(slotId, packageName);
} else {
Log.i(TAG, "overriding device ImsService - packageName=" + packageName);
if (packageName == null || packageName.isEmpty()) {
unbindImsService(getImsServiceInfoFromCache(mDeviceService));
}
mDeviceService = packageName;
bindImsService(getImsServiceInfoFromCache(packageName));
}
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 + "features:"
+ printFeatures(features));
handleFeaturesChanged(name, features);
}
@Override
public void onError(ComponentName name) {
Log.w(TAG, "onError: " + name + "returned with an error result");
scheduleQueryForFeatures(name, DELAY_DYNAMIC_QUERY_MS);
}
};
// Array index corresponds to slot Id associated with the service package name.
private String[] mCarrierServices;
// List index corresponds to Slot Id, Maps ImsFeature.FEATURE->bound ImsServiceController
// Locked on mBoundServicesLock
private List<SparseArray<ImsServiceController>> mBoundImsServicesByFeature;
// not locked, only accessed on a handler thread.
private Map<ComponentName, ImsServiceInfo> mInstalledServicesCache = new HashMap<>();
// not locked, only accessed on a handler thread.
private Map<ComponentName, ImsServiceController> mActiveControllers = new HashMap<>();
// Only used as the Component name for legacy ImsServices that did not use dynamic binding.
private final ComponentName mStaticComponent;
private ImsServiceFeatureQueryManager mFeatureQueryManager;
public ImsResolver(Context context, String defaultImsPackageName, int numSlots,
boolean isDynamicBinding) {
mContext = context;
mDeviceService = defaultImsPackageName;
mNumSlots = numSlots;
mIsDynamicBinding = isDynamicBinding;
mStaticComponent = new ComponentName(mContext, ImsResolver.class);
if (!mIsDynamicBinding) {
Log.i(TAG, "ImsResolver initialized with static binding.");
mDeviceService = mStaticComponent.getPackageName();
}
mCarrierConfigManager = (CarrierConfigManager) mContext.getSystemService(
Context.CARRIER_CONFIG_SERVICE);
mCarrierServices = new String[numSlots];
mBoundImsServicesByFeature = Stream.generate(SparseArray<ImsServiceController>::new)
.limit(mNumSlots).collect(Collectors.toList());
// Only register for Package/CarrierConfig updates if dynamic binding.
if(mIsDynamicBinding) {
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");
context.registerReceiverAsUser(mAppChangedReceiver, UserHandle.ALL, appChangedFilter,
null,
null);
context.registerReceiver(mConfigChangedReceiver, new IntentFilter(
CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED));
}
}
@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 first populate the cache and possibly bind to
* ImsServices.
*/
public void initPopulateCacheAndStartBind() {
Log.i(TAG, "Initializing cache and binding.");
mFeatureQueryManager = mDynamicQueryManagerFactory.create(mContext, mDynamicQueryListener);
// Populates the CarrierConfig override package names for each slot
mHandler.obtainMessage(HANDLER_CONFIG_CHANGED,
SubscriptionManager.INVALID_SIM_SLOT_INDEX).sendToTarget();
// Starts first bind to the system.
mHandler.obtainMessage(HANDLER_ADD_PACKAGE, null).sendToTarget();
}
/**
* 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;
}
// Used for testing only.
public boolean overrideImsServiceConfiguration(int slotId, boolean isCarrierService,
String packageName) {
if (slotId < 0 || slotId >= mNumSlots) {
Log.w(TAG, "overrideImsServiceConfiguration: invalid slotId!");
return false;
}
if (packageName == null) {
Log.w(TAG, "overrideImsServiceConfiguration: null packageName!");
return false;
}
// encode boolean to int for Message.
int carrierService = isCarrierService ? 1 : 0;
Message.obtain(mHandler, HANDLER_OVERRIDE_IMS_SERVICE_CONFIG, slotId, carrierService,
packageName).sendToTarget();
return true;
}
// used for testing only.
public String getImsServiceConfiguration(int slotId, boolean isCarrierService) {
if (slotId < 0 || slotId >= mNumSlots) {
Log.w(TAG, "getImsServiceConfiguration: invalid slotId!");
return "";
}
return isCarrierService ? mCarrierServices[slotId] : mDeviceService;
}
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.add(slotId, services);
}
Log.i(TAG, "ImsServiceController added on slot: " + slotId + " with feature: "
+ feature + " using package: " + controller.getComponentName());
services.put(feature, controller);
}
}
private ImsServiceController removeImsController(int slotId, int feature) {
if (slotId < 0 || slotId >= mNumSlots || 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) {
Log.i(TAG, "ImsServiceController removed on slot: " + slotId + " with feature: "
+ 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);
List<ImsServiceInfo> newlyAddedInfos = new ArrayList<>();
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) {
// update features in the cache
Log.i(TAG, "Updating features in cached ImsService: " + info.name);
Log.d(TAG, "Updating features - Old features: " + match + " new features: "
+ info);
match.replaceFeatures(info.getSupportedFeatures());
updateImsServiceFeatures(info);
} else {
// start a query to get ImsService features
scheduleQueryForFeatures(info);
}
} else {
Log.i(TAG, "Adding newly added ImsService to cache: " + info.name);
mInstalledServicesCache.put(info.name, info);
if (info.featureFromMetadata) {
newlyAddedInfos.add(info);
} else {
// newly added ImsServiceInfo that has not had features queried yet. Start async
// bind and query features.
scheduleQueryForFeatures(info);
}
}
}
// Loop through the newly created ServiceInfos in a separate loop to make sure the cache
// is fully updated.
for (ImsServiceInfo info : newlyAddedInfos) {
if (isActiveCarrierService(info)) {
// New ImsService is registered to active carrier services and must be newly
// bound.
bindImsService(info);
// Update existing device service features
updateImsServiceFeatures(getImsServiceInfoFromCache(mDeviceService));
} else if (isDeviceService(info)) {
// New ImsService is registered as device default and must be newly bound.
bindImsService(info);
}
}
}
// Remove the ImsService from the cache. At this point, the ImsService will have already been
// killed.
// Called from the handler ONLY
private boolean maybeRemovedImsService(String packageName) {
ImsServiceInfo match = getInfoByPackageName(mInstalledServicesCache, packageName);
if (match != null) {
mInstalledServicesCache.remove(match.name);
Log.i(TAG, "Removing ImsService: " + match.name);
unbindImsService(match);
updateImsServiceFeatures(getImsServiceInfoFromCache(mDeviceService));
return true;
}
return false;
}
// Returns true if the CarrierConfig that has been loaded includes this ImsServiceInfo
// package name.
// Called from Handler ONLY
private boolean isActiveCarrierService(ImsServiceInfo info) {
for (int i = 0; i < mNumSlots; i++) {
if (TextUtils.equals(mCarrierServices[i], info.name.getPackageName())) {
return true;
}
}
return false;
}
private boolean isDeviceService(ImsServiceInfo info) {
return TextUtils.equals(mDeviceService, info.name.getPackageName());
}
private int getSlotForActiveCarrierService(ImsServiceInfo info) {
for (int i = 0; i < mNumSlots; i++) {
if (TextUtils.equals(mCarrierServices[i], info.name.getPackageName())) {
return i;
}
}
return SubscriptionManager.INVALID_SIM_SLOT_INDEX;
}
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);
}
// Creates new features in active ImsServices and removes obsolete cached features. If
// cachedInfo == null, then newInfo is assumed to be a new ImsService and will have all features
// created.
private void updateImsServiceFeatures(ImsServiceInfo newInfo) {
if (newInfo == null) {
return;
}
ImsServiceController controller = getControllerByServiceInfo(mActiveControllers, newInfo);
// Will return zero if these features are overridden or it should not currently have any
// features because it is not carrier/device.
HashSet<ImsFeatureConfiguration.FeatureSlotPair> features =
calculateFeaturesToCreate(newInfo);
if (shouldFeaturesCauseBind(features)) {
try {
if (controller != null) {
Log.i(TAG, "Updating features for ImsService: "
+ controller.getComponentName());
Log.d(TAG, "Updating Features - New Features: " + features);
controller.changeImsServiceFeatures(features);
} else {
Log.i(TAG, "updateImsServiceFeatures: unbound with active features, rebinding");
bindImsServiceWithFeatures(newInfo, features);
}
// If the carrier service features have changed, the device features will also
// need to be recalculated.
if (isActiveCarrierService(newInfo)
// Prevent infinite recursion from bad behavior
&& !TextUtils.equals(newInfo.name.getPackageName(), mDeviceService)) {
Log.i(TAG, "Updating device default");
updateImsServiceFeatures(getImsServiceInfoFromCache(mDeviceService));
}
} catch (RemoteException e) {
Log.e(TAG, "updateImsServiceFeatures: Remote Exception: " + e.getMessage());
}
// Don't stay bound if the ImsService is providing no features.
} else if (controller != null) {
Log.i(TAG, "Unbinding: features = 0 for ImsService: " + controller.getComponentName());
unbindImsService(newInfo);
}
}
// Bind to an ImsService and wait for the service to be connected to create ImsFeatures.
private void bindImsService(ImsServiceInfo info) {
if (info == null) {
return;
}
HashSet<ImsFeatureConfiguration.FeatureSlotPair> features = calculateFeaturesToCreate(info);
bindImsServiceWithFeatures(info, features);
}
private void bindImsServiceWithFeatures(ImsServiceInfo info,
HashSet<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, 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);
}
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());
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<>();
// Check if the info is a carrier service
int slotId = getSlotForActiveCarrierService(info);
if (slotId != SubscriptionManager.INVALID_SIM_SLOT_INDEX) {
imsFeaturesBySlot.addAll(info.getSupportedFeatures().stream()
// Match slotId with feature slotId.
.filter(feature -> slotId == feature.slotId)
.collect(Collectors.toList()));
} else if (isDeviceService(info)) {
// For all slots that are not currently using a carrier ImsService, enable all features
// for the device default.
for (int i = 0; i < mNumSlots; i++) {
final int currSlotId = i;
ImsServiceInfo carrierImsInfo = getImsServiceInfoFromCache(mCarrierServices[i]);
if (carrierImsInfo == null) {
// No Carrier override, add all features for this slot
imsFeaturesBySlot.addAll(info.getSupportedFeatures().stream()
.filter(feature -> currSlotId == feature.slotId)
.collect(Collectors.toList()));
} else {
// Add all features to the device service that are not currently covered by
// the carrier ImsService.
HashSet<ImsFeatureConfiguration.FeatureSlotPair> deviceFeatures =
new HashSet<>(info.getSupportedFeatures());
deviceFeatures.removeAll(carrierImsInfo.getSupportedFeatures());
// only add features for current slot
imsFeaturesBySlot.addAll(deviceFeatures.stream()
.filter(feature -> currSlotId == feature.slotId).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());
handleFeaturesChanged(controller.getComponentName(), config.getServiceFeatures());
}
/**
* 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(
HashSet<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 if currently installed ImsServices were changed or if
// the SIM card has changed.
// Called from the handler ONLY
private void maybeRebindService(int slotId, String newPackageName) {
if (slotId <= SubscriptionManager.INVALID_SIM_SLOT_INDEX) {
// not specified, replace package on all slots.
for (int i = 0; i < mNumSlots; i++) {
updateBoundCarrierServices(i, newPackageName);
}
} else {
updateBoundCarrierServices(slotId, newPackageName);
}
}
private void carrierConfigChanged(int slotId) {
int subId = mSubscriptionManagerProxy.getSubId(slotId);
PersistableBundle config = mCarrierConfigManager.getConfigForSubId(subId);
if (config != null) {
String newPackageName = config.getString(
CarrierConfigManager.KEY_CONFIG_IMS_PACKAGE_OVERRIDE_STRING, null);
maybeRebindService(slotId, newPackageName);
} else {
Log.w(TAG, "carrierConfigChanged: CarrierConfig is null!");
}
}
private void updateBoundCarrierServices(int slotId, String newPackageName) {
if (slotId > SubscriptionManager.INVALID_SIM_SLOT_INDEX && slotId < mNumSlots) {
String oldPackageName = mCarrierServices[slotId];
mCarrierServices[slotId] = newPackageName;
if (!TextUtils.equals(newPackageName, oldPackageName)) {
Log.i(TAG, "Carrier Config updated, binding new ImsService");
// Unbind old ImsService, not needed anymore
// ImsService is retrieved from the cache. If the cache hasn't been populated yet,
// the calls to unbind/bind will fail (intended during initial start up).
unbindImsService(getImsServiceInfoFromCache(oldPackageName));
ImsServiceInfo newInfo = getImsServiceInfoFromCache(newPackageName);
// if there is no carrier ImsService, newInfo is null. This we still want to update
// bindings for device ImsService to pick up the missing features.
if (newInfo == null || newInfo.featureFromMetadata) {
bindImsService(newInfo);
// Recalculate the device ImsService features to reflect changes.
updateImsServiceFeatures(getImsServiceInfoFromCache(mDeviceService));
} else {
// ImsServiceInfo that has not had features queried yet. Start async
// bind and query features.
scheduleQueryForFeatures(newInfo);
}
}
}
}
/**
* Schedules a query for dynamic ImsService features.
*/
private void scheduleQueryForFeatures(ImsServiceInfo service, int delayMs) {
// if not current device/carrier service, don't perform query. If this changes, this method
// will be called again.
if (!isDeviceService(service) && getSlotForActiveCarrierService(service)
== SubscriptionManager.INVALID_SIM_SLOT_INDEX) {
Log.i(TAG, "scheduleQueryForFeatures: skipping query for ImsService that is not"
+ " set as carrier/device ImsService.");
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();
}
// Starts a dynamic query. Called from handler ONLY.
private void startDynamicQuery(ImsServiceInfo service) {
boolean queryStarted = mFeatureQueryManager.startQuery(service.name,
service.controllerFactory.getServiceInterface());
if (!queryStarted) {
Log.w(TAG, "startDynamicQuery: service could not connect. Retrying after delay.");
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, "handleFeaturesChanged: Couldn't find cached info for name: "
+ name);
return;
}
// Add features to service
service.replaceFeatures(features);
if (isActiveCarrierService(service)) {
// New ImsService is registered to active carrier services and must be newly
// bound.
bindImsService(service);
// Update existing device service features
updateImsServiceFeatures(getImsServiceInfoFromCache(mDeviceService));
} else if (isDeviceService(service)) {
// New ImsService is registered as device default and must be newly bound.
bindImsService(service);
}
}
/**
* @return true if the ImsResolver is in the process of resolving a dynamic query and should not
* be considered available, false if the ImsResolver is idle.
*/
public boolean isResolvingBinding() {
return mHandler.hasMessages(HANDLER_START_DYNAMIC_FEATURE_QUERY)
// We haven't processed this message yet, so it is still resolving.
|| mHandler.hasMessages(HANDLER_DYNAMIC_FEATURE_CHANGE)
|| mFeatureQueryManager.isQueryInProgress();
}
private 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(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<>();
if (!mIsDynamicBinding) {
// always return the same ImsService info.
infos.addAll(getStaticImsService());
} else {
// 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> getStaticImsService() {
List<ImsServiceInfo> infos = new ArrayList<>();
ImsServiceInfo info = new ImsServiceInfo(mNumSlots);
info.name = mStaticComponent;
info.controllerFactory = mImsServiceControllerFactoryStaticBindingCompat;
info.addFeatureForAllSlots(ImsFeature.FEATURE_EMERGENCY_MMTEL);
info.addFeatureForAllSlots(ImsFeature.FEATURE_MMTEL);
infos.add(info);
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,
mContext.getUserId())) {
ServiceInfo serviceInfo = entry.serviceInfo;
if (serviceInfo != null) {
ImsServiceInfo info = new ImsServiceInfo(mNumSlots);
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_EMERGENCY_MMTEL_FEATURE,
false)) {
info.addFeatureForAllSlots(ImsFeature.FEATURE_EMERGENCY_MMTEL);
}
if (serviceInfo.metaData.getBoolean(METADATA_MMTEL_FEATURE, false)) {
info.addFeatureForAllSlots(ImsFeature.FEATURE_MMTEL);
}
if (serviceInfo.metaData.getBoolean(METADATA_RCS_FEATURE, false)) {
info.addFeatureForAllSlots(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.
if (TextUtils.equals(serviceInfo.permission,
Manifest.permission.BIND_IMS_SERVICE)) {
infos.add(info);
} else {
Log.w(TAG, "ImsService is not protected with BIND_IMS_SERVICE permission: "
+ info.name);
}
}
}
return infos;
}
}