/* * Copyright (c) 2019 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.ims; import android.annotation.NonNull; import android.annotation.Nullable; import android.content.Context; import android.net.Uri; import android.os.IBinder; import android.os.PersistableBundle; import android.os.RemoteException; import android.os.ServiceSpecificException; import android.telephony.BinderCacheManager; import android.telephony.CarrierConfigManager; import android.telephony.SubscriptionManager; import android.telephony.TelephonyFrameworkInitializer; import android.telephony.ims.ImsException; import android.telephony.ims.ImsService; import android.telephony.ims.RcsUceAdapter.StackPublishTriggerType; import android.telephony.ims.RegistrationManager; import android.telephony.ims.SipDetails; import android.telephony.ims.aidl.ICapabilityExchangeEventListener; import android.telephony.ims.aidl.IImsCapabilityCallback; import android.telephony.ims.aidl.IImsConfig; import android.telephony.ims.aidl.IImsRcsController; import android.telephony.ims.aidl.IImsRcsFeature; import android.telephony.ims.aidl.IImsRegistration; import android.telephony.ims.aidl.IImsRegistrationCallback; import android.telephony.ims.aidl.IOptionsRequestCallback; import android.telephony.ims.aidl.IOptionsResponseCallback; import android.telephony.ims.aidl.IPublishResponseCallback; import android.telephony.ims.aidl.ISipTransport; import android.telephony.ims.aidl.ISubscribeResponseCallback; import android.telephony.ims.feature.CapabilityChangeRequest; import android.telephony.ims.feature.ImsFeature; import android.telephony.ims.feature.RcsFeature.RcsImsCapabilities; import android.telephony.ims.stub.ImsRegistrationImplBase; import android.util.Log; import com.android.ims.internal.IImsServiceFeatureCallback; import com.android.internal.annotations.VisibleForTesting; import com.android.telephony.Rlog; import java.util.ArrayList; import java.util.List; import java.util.Set; import java.util.concurrent.CopyOnWriteArraySet; import java.util.concurrent.CountDownLatch; import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; /** * Encapsulates all logic related to the RcsFeature: * - Updating RcsFeature capabilities. * - Registering/Unregistering availability/registration callbacks. * - Querying Registration and Capability information. */ public class RcsFeatureManager implements FeatureUpdates { private static final String TAG = "RcsFeatureManager"; private static boolean DBG = true; private static final int CAPABILITY_OPTIONS = RcsImsCapabilities.CAPABILITY_TYPE_OPTIONS_UCE; private static final int CAPABILITY_PRESENCE = RcsImsCapabilities.CAPABILITY_TYPE_PRESENCE_UCE; /** * The capability exchange event callbacks from the RcsFeature. */ public interface CapabilityExchangeEventCallback { /** * Triggered by RcsFeature to publish the device's capabilities to the network. */ void onRequestPublishCapabilities(@StackPublishTriggerType int publishTriggerType); /** * Notify that the devices is unpublished. */ void onUnpublish(); /** * Notify the framework that the ImsService has refreshed the PUBLISH * internally, which has resulted in a new PUBLISH result. *

* This method must be called to notify the framework of SUCCESS (200 OK) and FAILURE (300+) * codes in order to keep the AOSP stack up to date. */ void onPublishUpdated(SipDetails details); /** * Receive a capabilities request from the remote client. */ void onRemoteCapabilityRequest(Uri contactUri, List remoteCapabilities, IOptionsRequestCallback cb); } /* * Setup the listener to listen to the requests and updates from ImsService. */ private ICapabilityExchangeEventListener mCapabilityEventListener = new ICapabilityExchangeEventListener.Stub() { @Override public void onRequestPublishCapabilities(@StackPublishTriggerType int type) { mCapabilityEventCallback.forEach( callback -> callback.onRequestPublishCapabilities(type)); } @Override public void onUnpublish() { mCapabilityEventCallback.forEach(callback -> callback.onUnpublish()); } @Override public void onPublishUpdated(@NonNull SipDetails details) { mCapabilityEventCallback.forEach( callback ->callback.onPublishUpdated(details)); } @Override public void onRemoteCapabilityRequest(Uri contactUri, List remoteCapabilities, IOptionsRequestCallback cb) { mCapabilityEventCallback.forEach( callback -> callback.onRemoteCapabilityRequest( contactUri, remoteCapabilities, cb)); } }; private final int mSlotId; private final Context mContext; private final Set mCapabilityEventCallback = new CopyOnWriteArraySet<>(); private final BinderCacheManager mBinderCache = new BinderCacheManager<>(RcsFeatureManager::getIImsRcsControllerInterface); @VisibleForTesting public RcsFeatureConnection mRcsFeatureConnection; /** * Use to obtain a FeatureConnector, which will maintain a consistent listener to the * RcsFeature attached to the specified slotId. If the RcsFeature changes (due to things like * SIM swap), a new RcsFeatureManager will be delivered to this Listener. * @param context The Context this connector should use. * @param slotId The slotId associated with the Listener and requested RcsFeature * @param listener The listener, which will be used to generate RcsFeatureManager instances. * @param executor The executor that the Listener callbacks will be called on. * @param logPrefix The prefix used in logging of the FeatureConnector for notable events. * @return A FeatureConnector, which will start delivering RcsFeatureManagers as the underlying * RcsFeature instances become available to the platform. * @see {@link FeatureConnector#connect()}. */ public static FeatureConnector getConnector(Context context, int slotId, FeatureConnector.Listener listener, Executor executor, String logPrefix) { ArrayList filter = new ArrayList<>(); filter.add(ImsFeature.STATE_READY); return new FeatureConnector<>(context, slotId, RcsFeatureManager::new, logPrefix, filter, listener, executor); } /** * Use {@link #getConnector} to get an instance of this class. */ private RcsFeatureManager(Context context, int slotId) { mContext = context; mSlotId = slotId; } /** * Opens a persistent connection to the RcsFeature. This must be called before the RcsFeature * can be used to communicate. */ public void openConnection() throws android.telephony.ims.ImsException { try { mRcsFeatureConnection.setCapabilityExchangeEventListener(mCapabilityEventListener); } catch (RemoteException e){ throw new android.telephony.ims.ImsException("Service is not available.", android.telephony.ims.ImsException.CODE_ERROR_SERVICE_UNAVAILABLE); } } /** * Closes the persistent connection to the RcsFeature. This must be called when this manager * wishes to no longer be used to communicate with the RcsFeature. */ public void releaseConnection() { try { mRcsFeatureConnection.setCapabilityExchangeEventListener(null); } catch (RemoteException e){ // Connection may not be available at this point. } mRcsFeatureConnection.close(); mCapabilityEventCallback.clear(); } /** * Adds a callback for {@link CapabilityExchangeEventCallback}. * Note: These callbacks will be sent on the binder thread used to notify the callback. */ public void addCapabilityEventCallback(CapabilityExchangeEventCallback listener) { mCapabilityEventCallback.add(listener); } /** * Removes an existing {@link CapabilityExchangeEventCallback}. */ public void removeCapabilityEventCallback(CapabilityExchangeEventCallback listener) { mCapabilityEventCallback.remove(listener); } /** * Update the capabilities for this RcsFeature. */ public void updateCapabilities(int newSubId) throws android.telephony.ims.ImsException { boolean optionsSupport = isOptionsSupported(newSubId); boolean presenceSupported = isPresenceSupported(newSubId); logi("Update capabilities for slot " + mSlotId + " and sub " + newSubId + ": options=" + optionsSupport+ ", presence=" + presenceSupported); if (optionsSupport || presenceSupported) { CapabilityChangeRequest request = new CapabilityChangeRequest(); if (optionsSupport) { addRcsUceCapability(request, CAPABILITY_OPTIONS); } if (presenceSupported) { addRcsUceCapability(request, CAPABILITY_PRESENCE); } sendCapabilityChangeRequest(request); } else { disableAllRcsUceCapabilities(); } } /** * Add a {@link RegistrationManager.RegistrationCallback} callback that gets called when IMS * registration has changed for a specific subscription. */ public void registerImsRegistrationCallback(int subId, IImsRegistrationCallback callback) throws android.telephony.ims.ImsException { try { mRcsFeatureConnection.addCallbackForSubscription(subId, callback); } catch (IllegalStateException e) { loge("registerImsRegistrationCallback error: ", e); throw new android.telephony.ims.ImsException("Can not register callback", android.telephony.ims.ImsException.CODE_ERROR_SERVICE_UNAVAILABLE); } } /** * Add a {@link RegistrationManager.RegistrationCallback} callback that gets called when IMS * registration has changed, independent of the subscription it is currently on. */ public void registerImsRegistrationCallback(IImsRegistrationCallback callback) throws android.telephony.ims.ImsException { try { mRcsFeatureConnection.addCallback(callback); } catch (IllegalStateException e) { loge("registerImsRegistrationCallback error: ", e); throw new android.telephony.ims.ImsException("Can not register callback", android.telephony.ims.ImsException.CODE_ERROR_SERVICE_UNAVAILABLE); } } /** * Removes a previously registered {@link RegistrationManager.RegistrationCallback} callback * that is associated with a specific subscription. */ public void unregisterImsRegistrationCallback(int subId, IImsRegistrationCallback callback) { mRcsFeatureConnection.removeCallbackForSubscription(subId, callback); } /** * Removes a previously registered {@link RegistrationManager.RegistrationCallback} callback * that was not associated with a subscription. */ public void unregisterImsRegistrationCallback(IImsRegistrationCallback callback) { mRcsFeatureConnection.removeCallback(callback); } /** * Get the IMS RCS registration technology for this Phone, * defined in {@link ImsRegistrationImplBase}. */ public void getImsRegistrationTech(Consumer callback) { try { int tech = mRcsFeatureConnection.getRegistrationTech(); callback.accept(tech); } catch (RemoteException e) { loge("getImsRegistrationTech error: ", e); callback.accept(ImsRegistrationImplBase.REGISTRATION_TECH_NONE); } } /** * Register an ImsCapabilityCallback with RCS service, which will provide RCS availability * updates. */ public void registerRcsAvailabilityCallback(int subId, IImsCapabilityCallback callback) throws android.telephony.ims.ImsException { try { mRcsFeatureConnection.addCallbackForSubscription(subId, callback); } catch (IllegalStateException e) { loge("registerRcsAvailabilityCallback: ", e); throw new android.telephony.ims.ImsException("Can not register callback", android.telephony.ims.ImsException.CODE_ERROR_SERVICE_UNAVAILABLE); } } /** * Remove an registered ImsCapabilityCallback from RCS service. */ public void unregisterRcsAvailabilityCallback(int subId, IImsCapabilityCallback callback) { mRcsFeatureConnection.removeCallbackForSubscription(subId, callback); } public boolean isImsServiceCapable(@ImsService.ImsServiceCapability long capabilities) throws ImsException { try { return mRcsFeatureConnection.isCapable(capabilities); } catch (RemoteException e) { throw new ImsException(e.getMessage(), ImsException.CODE_ERROR_SERVICE_UNAVAILABLE); } } /** * @return The SipTransport interface if it exists or {@code null} if it does not exist due to * the ImsService not supporting it. */ public ISipTransport getSipTransport() throws ImsException { if (!isImsServiceCapable(ImsService.CAPABILITY_SIP_DELEGATE_CREATION)) { return null; } return mRcsFeatureConnection.getSipTransport(); } public IImsRegistration getImsRegistration() { return mRcsFeatureConnection.getRegistration(); } /** * Query for the specific capability. */ public boolean isCapable( @RcsImsCapabilities.RcsImsCapabilityFlag int capability, @ImsRegistrationImplBase.ImsRegistrationTech int radioTech) throws android.telephony.ims.ImsException { CountDownLatch latch = new CountDownLatch(1); AtomicReference capableRef = new AtomicReference<>(); IImsCapabilityCallback callback = new IImsCapabilityCallback.Stub() { @Override public void onQueryCapabilityConfiguration( int resultCapability, int resultRadioTech, boolean enabled) { if ((capability != resultCapability) || (radioTech != resultRadioTech)) { return; } if (DBG) log("capable result:capability=" + capability + ", enabled=" + enabled); capableRef.set(enabled); latch.countDown(); } @Override public void onCapabilitiesStatusChanged(int config) { // Don't handle it } @Override public void onChangeCapabilityConfigurationError(int capability, int radioTech, int reason) { // Don't handle it } }; try { if (DBG) log("Query capability: " + capability + ", radioTech=" + radioTech); mRcsFeatureConnection.queryCapabilityConfiguration(capability, radioTech, callback); return awaitResult(latch, capableRef); } catch (RemoteException e) { loge("isCapable error: ", e); throw new android.telephony.ims.ImsException("Can not determine capabilities", android.telephony.ims.ImsException.CODE_ERROR_SERVICE_UNAVAILABLE); } } private static T awaitResult(CountDownLatch latch, AtomicReference resultRef) { try { latch.await(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } return resultRef.get(); } /** * Query the availability of an IMS RCS capability. */ public boolean isAvailable(@RcsImsCapabilities.RcsImsCapabilityFlag int capability, @ImsRegistrationImplBase.ImsRegistrationTech int radioTech) throws android.telephony.ims.ImsException { try { if (mRcsFeatureConnection.getRegistrationTech() != radioTech) { return false; } int currentStatus = mRcsFeatureConnection.queryCapabilityStatus(); return new RcsImsCapabilities(currentStatus).isCapable(capability); } catch (RemoteException e) { loge("isAvailable error: ", e); throw new android.telephony.ims.ImsException("Can not determine availability", android.telephony.ims.ImsException.CODE_ERROR_SERVICE_UNAVAILABLE); } } /** * Add UCE capabilities with given type. * @param capability the specific RCS UCE capability wants to enable */ public void addRcsUceCapability(CapabilityChangeRequest request, @RcsImsCapabilities.RcsImsCapabilityFlag int capability) { request.addCapabilitiesToEnableForTech(capability, ImsRegistrationImplBase.REGISTRATION_TECH_NR); request.addCapabilitiesToEnableForTech(capability, ImsRegistrationImplBase.REGISTRATION_TECH_LTE); request.addCapabilitiesToEnableForTech(capability, ImsRegistrationImplBase.REGISTRATION_TECH_IWLAN); } public void requestPublication(String pidfXml, IPublishResponseCallback responseCallback) throws RemoteException { mRcsFeatureConnection.requestPublication(pidfXml, responseCallback); } public void requestCapabilities(List uris, ISubscribeResponseCallback c) throws RemoteException { mRcsFeatureConnection.requestCapabilities(uris, c); } public void sendOptionsCapabilityRequest(Uri contactUri, List myCapabilities, IOptionsResponseCallback callback) throws RemoteException { mRcsFeatureConnection.sendOptionsCapabilityRequest(contactUri, myCapabilities, callback); } /** * Disable all of the UCE capabilities. */ private void disableAllRcsUceCapabilities() throws android.telephony.ims.ImsException { final int techNr = ImsRegistrationImplBase.REGISTRATION_TECH_NR; final int techLte = ImsRegistrationImplBase.REGISTRATION_TECH_LTE; final int techIWlan = ImsRegistrationImplBase.REGISTRATION_TECH_IWLAN; CapabilityChangeRequest request = new CapabilityChangeRequest(); request.addCapabilitiesToDisableForTech(CAPABILITY_OPTIONS, techNr); request.addCapabilitiesToDisableForTech(CAPABILITY_OPTIONS, techLte); request.addCapabilitiesToDisableForTech(CAPABILITY_OPTIONS, techIWlan); request.addCapabilitiesToDisableForTech(CAPABILITY_PRESENCE, techNr); request.addCapabilitiesToDisableForTech(CAPABILITY_PRESENCE, techLte); request.addCapabilitiesToDisableForTech(CAPABILITY_PRESENCE, techIWlan); sendCapabilityChangeRequest(request); } private void sendCapabilityChangeRequest(CapabilityChangeRequest request) throws android.telephony.ims.ImsException { try { if (DBG) log("sendCapabilityChangeRequest: " + request); mRcsFeatureConnection.changeEnabledCapabilities(request, null); } catch (RemoteException e) { throw new android.telephony.ims.ImsException("Can not connect to service", android.telephony.ims.ImsException.CODE_ERROR_SERVICE_UNAVAILABLE); } } private boolean isOptionsSupported(int subId) { return isCapabilityTypeSupported(mContext, subId, CAPABILITY_OPTIONS); } private boolean isPresenceSupported(int subId) { return isCapabilityTypeSupported(mContext, subId, CAPABILITY_PRESENCE); } /* * Check if the given type of capability is supported. */ private static boolean isCapabilityTypeSupported( Context context, int subId, int capabilityType) { if (subId == SubscriptionManager.INVALID_SUBSCRIPTION_ID) { Log.e(TAG, "isCapabilityTypeSupported: Invalid subId=" + subId); return false; } CarrierConfigManager configManager = (CarrierConfigManager) context.getSystemService(Context.CARRIER_CONFIG_SERVICE); if (configManager == null) { Log.e(TAG, "isCapabilityTypeSupported: CarrierConfigManager is null, " + subId); return false; } PersistableBundle b = configManager.getConfigForSubId(subId); if (b == null) { Log.e(TAG, "isCapabilityTypeSupported: PersistableBundle is null, " + subId); return false; } if (capabilityType == CAPABILITY_OPTIONS) { return b.getBoolean(CarrierConfigManager.KEY_USE_RCS_SIP_OPTIONS_BOOL, false); } else if (capabilityType == CAPABILITY_PRESENCE) { return b.getBoolean(CarrierConfigManager.Ims.KEY_ENABLE_PRESENCE_PUBLISH_BOOL, false); } return false; } @Override public void registerFeatureCallback(int slotId, IImsServiceFeatureCallback cb) { IImsRcsController controller = mBinderCache.listenOnBinder(cb, () -> { try { cb.imsFeatureRemoved( FeatureConnector.UNAVAILABLE_REASON_SERVER_UNAVAILABLE); } catch (RemoteException ignore) {} // This is local. }); try { if (controller == null) { Log.e(TAG, "registerRcsFeatureListener: IImsRcsController is null"); cb.imsFeatureRemoved(FeatureConnector.UNAVAILABLE_REASON_SERVER_UNAVAILABLE); return; } controller.registerRcsFeatureCallback(slotId, cb); } catch (ServiceSpecificException e) { try { switch (e.errorCode) { case ImsException.CODE_ERROR_UNSUPPORTED_OPERATION: cb.imsFeatureRemoved(FeatureConnector.UNAVAILABLE_REASON_IMS_UNSUPPORTED); break; default: { cb.imsFeatureRemoved( FeatureConnector.UNAVAILABLE_REASON_SERVER_UNAVAILABLE); } } } catch (RemoteException ignore) {} // Already dead anyway if this happens. } catch (RemoteException e) { try { cb.imsFeatureRemoved(FeatureConnector.UNAVAILABLE_REASON_SERVER_UNAVAILABLE); } catch (RemoteException ignore) {} // Already dead if this happens. } } @Override public void unregisterFeatureCallback(IImsServiceFeatureCallback cb) { try { IImsRcsController imsRcsController = mBinderCache.removeRunnable(cb); if (imsRcsController != null) { imsRcsController.unregisterImsFeatureCallback(cb); } } catch (RemoteException e) { // This means that telephony died, so do not worry about it. Rlog.e(TAG, "unregisterImsFeatureCallback (RCS), RemoteException: " + e.getMessage()); } } private IImsRcsController getIImsRcsController() { return mBinderCache.getBinder(); } private static IImsRcsController getIImsRcsControllerInterface() { IBinder binder = TelephonyFrameworkInitializer .getTelephonyServiceManager() .getTelephonyImsServiceRegisterer() .get(); IImsRcsController c = IImsRcsController.Stub.asInterface(binder); return c; } @Override public void associate(ImsFeatureContainer c, int subId) { IImsRcsFeature f = IImsRcsFeature.Stub.asInterface(c.imsFeature); mRcsFeatureConnection = new RcsFeatureConnection(mContext, mSlotId, subId, f, c.imsConfig, c.imsRegistration, c.sipTransport); } @Override public void invalidate() { mRcsFeatureConnection.onRemovedOrDied(); } @Override public void updateFeatureState(int state) { mRcsFeatureConnection.updateFeatureState(state); } @Override public void updateFeatureCapabilities(long capabilities) { mRcsFeatureConnection.updateFeatureCapabilities(capabilities); } public IImsConfig getConfig() { return mRcsFeatureConnection.getConfig(); } /** * @return the subscription ID associated with this ImsService connection. */ public int getSubId() { return mRcsFeatureConnection.getSubId(); } private void log(String s) { Rlog.d(TAG + " [" + mSlotId + "]", s); } private void logi(String s) { Rlog.i(TAG + " [" + mSlotId + "]", s); } private void loge(String s) { Rlog.e(TAG + " [" + mSlotId + "]", s); } private void loge(String s, Throwable t) { Rlog.e(TAG + " [" + mSlotId + "]", s, t); } }