/* * 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.ims; import android.annotation.Nullable; import android.content.Context; import android.net.Uri; import android.os.IBinder; import android.os.Message; import android.os.RemoteException; import android.telephony.Rlog; import android.telephony.TelephonyManager; import android.telephony.ims.ImsCallProfile; import android.telephony.ims.ImsReasonInfo; import android.telephony.ims.aidl.IImsConfig; import android.telephony.ims.aidl.IImsMmTelFeature; import android.telephony.ims.aidl.IImsRegistration; import android.telephony.ims.aidl.IImsRegistrationCallback; import android.telephony.ims.aidl.IImsSmsListener; import android.telephony.ims.feature.CapabilityChangeRequest; import android.telephony.ims.feature.ImsFeature; import android.telephony.ims.feature.MmTelFeature; import android.telephony.ims.stub.ImsRegistrationImplBase; import android.telephony.ims.stub.ImsSmsImplBase; import android.util.Log; import com.android.ims.internal.IImsCallSession; import com.android.ims.internal.IImsEcbm; import com.android.ims.internal.IImsMultiEndpoint; import com.android.ims.internal.IImsServiceFeatureCallback; import com.android.ims.internal.IImsUt; import java.util.HashSet; import java.util.Set; /** * A container of the IImsServiceController binder, which implements all of the ImsFeatures that * the platform currently supports: MMTel and RCS. * @hide */ public class MmTelFeatureConnection { protected static final String TAG = "MmTelFeatureConnection"; protected final int mSlotId; protected IBinder mBinder; private Context mContext; private volatile boolean mIsAvailable = false; // ImsFeature Status from the ImsService. Cached. private Integer mFeatureStateCached = null; private IFeatureUpdate mStatusCallback; private final Object mLock = new Object(); // Updated by IImsServiceFeatureCallback when FEATURE_EMERGENCY_MMTEL is sent. private boolean mSupportsEmergencyCalling = false; // Cache the Registration and Config interfaces as long as the MmTel feature is connected. If // it becomes disconnected, invalidate. private IImsRegistration mRegistrationBinder; private IImsConfig mConfigBinder; private abstract class CallbackAdapterManager { private static final String TAG = "CallbackAdapterManager"; protected final Set mLocalCallbacks = new HashSet<>(); private boolean mHasConnected = false; public void addCallback(T localCallback) throws RemoteException { // We only one one binding to the ImsService per process. // Store any more locally. synchronized (mLock) { if(!mHasConnected) { // throws a RemoteException if a connection can not be established. if(createConnection()) { mHasConnected = true; } else { throw new RemoteException("Can not create connection!"); } } Log.i(TAG, "Local callback added: " + localCallback); mLocalCallbacks.add(localCallback); } } public void removeCallback(T localCallback) { // We only maintain one binding to the ImsService per process. synchronized (mLock) { Log.i(TAG, "Local callback removed: " + localCallback); mLocalCallbacks.remove(localCallback); // If we have removed all local callbacks, remove callback to ImsService. if(mHasConnected) { if (mLocalCallbacks.isEmpty()) { removeConnection(); mHasConnected = false; } } } } public void close() { synchronized (mLock) { if (mHasConnected) { removeConnection(); // Still mark the connection as disconnected, even if this fails. mHasConnected = false; } Log.i(TAG, "Closing connection and clearing callbacks"); mLocalCallbacks.clear(); } } abstract boolean createConnection() throws RemoteException; abstract void removeConnection(); } private ImsRegistrationCallbackAdapter mRegistrationCallbackManager = new ImsRegistrationCallbackAdapter(); private class ImsRegistrationCallbackAdapter extends CallbackAdapterManager { private final RegistrationCallbackAdapter mRegistrationCallbackAdapter = new RegistrationCallbackAdapter(); private class RegistrationCallbackAdapter extends IImsRegistrationCallback.Stub { @Override public void onRegistered(int imsRadioTech) { Log.i(TAG, "onRegistered ::"); synchronized (mLock) { mLocalCallbacks.forEach(l -> l.onRegistered(imsRadioTech)); } } @Override public void onRegistering(int imsRadioTech) { Log.i(TAG, "onRegistering ::"); synchronized (mLock) { mLocalCallbacks.forEach(l -> l.onRegistering(imsRadioTech)); } } @Override public void onDeregistered(ImsReasonInfo imsReasonInfo) { Log.i(TAG, "onDeregistered ::"); synchronized (mLock) { mLocalCallbacks.forEach(l -> l.onDeregistered(imsReasonInfo)); } } @Override public void onTechnologyChangeFailed(int targetRadioTech, ImsReasonInfo imsReasonInfo) { Log.i(TAG, "onTechnologyChangeFailed :: targetAccessTech=" + targetRadioTech + ", imsReasonInfo=" + imsReasonInfo); synchronized (mLock) { mLocalCallbacks.forEach(l -> l.onTechnologyChangeFailed(targetRadioTech, imsReasonInfo)); } } @Override public void onSubscriberAssociatedUriChanged(Uri[] uris) { Log.i(TAG, "onSubscriberAssociatedUriChanged"); synchronized (mLock) { mLocalCallbacks.forEach(l -> l.onSubscriberAssociatedUriChanged(uris)); } } } @Override boolean createConnection() throws RemoteException { IImsRegistration imsRegistration = getRegistration(); if (imsRegistration != null) { getRegistration().addRegistrationCallback(mRegistrationCallbackAdapter); return true; } else { Log.e(TAG, "ImsRegistration is null"); return false; } } @Override void removeConnection() { IImsRegistration imsRegistration = getRegistration(); if (imsRegistration != null) { try { getRegistration().removeRegistrationCallback(mRegistrationCallbackAdapter); } catch (RemoteException e) { Log.w(TAG, "removeConnection: couldn't remove registration callback"); } } else { Log.e(TAG, "ImsRegistration is null"); } } } private final CapabilityCallbackManager mCapabilityCallbackManager = new CapabilityCallbackManager(); private class CapabilityCallbackManager extends CallbackAdapterManager { private final CapabilityCallbackAdapter mCallbackAdapter = new CapabilityCallbackAdapter(); private class CapabilityCallbackAdapter extends ImsFeature.CapabilityCallback { // Called when the Capabilities Status on this connection have changed. @Override public void onCapabilitiesStatusChanged(ImsFeature.Capabilities config) { synchronized (mLock) { mLocalCallbacks.forEach( callback -> callback.onCapabilitiesStatusChanged(config)); } } } @Override boolean createConnection() throws RemoteException { IImsMmTelFeature binder = getServiceInterface(mBinder); if (binder != null) { binder.addCapabilityCallback(mCallbackAdapter); return true; } else { Log.w(TAG, "create: Couldn't get IImsMmTelFeature binder"); return false; } } @Override void removeConnection() { IImsMmTelFeature binder = getServiceInterface(mBinder); if (binder != null) { try { binder.removeCapabilityCallback(mCallbackAdapter); } catch (RemoteException e) { Log.w(TAG, "remove: IImsMmTelFeature binder is dead"); } } else { Log.w(TAG, "remove: Couldn't get IImsMmTelFeature binder"); } } } public static MmTelFeatureConnection create(Context context , int slotId) { MmTelFeatureConnection serviceProxy = new MmTelFeatureConnection(context, slotId); TelephonyManager tm = getTelephonyManager(context); if (tm == null) { Rlog.w(TAG, "create: TelephonyManager is null!"); // Binder can be unset in this case because it will be torn down/recreated as part of // a retry mechanism until the serviceProxy binder is set successfully. return serviceProxy; } IImsMmTelFeature binder = tm.getImsMmTelFeatureAndListen(slotId, serviceProxy.getListener()); if (binder != null) { serviceProxy.setBinder(binder.asBinder()); // Trigger the cache to be updated for feature status. serviceProxy.getFeatureState(); } else { Rlog.w(TAG, "create: binder is null! Slot Id: " + slotId); } return serviceProxy; } public static TelephonyManager getTelephonyManager(Context context) { return (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); } public interface IFeatureUpdate { /** * Called when the ImsFeature has changed its state. Use * {@link ImsFeature#getFeatureState()} to get the new state. */ void notifyStateChanged(); /** * Called when the ImsFeature has become unavailable due to the binder switching or app * crashing. A new ImsServiceProxy should be requested for that feature. */ void notifyUnavailable(); } private final IImsServiceFeatureCallback mListenerBinder = new IImsServiceFeatureCallback.Stub() { @Override public void imsFeatureCreated(int slotId, int feature) throws RemoteException { // The feature has been enabled. This happens when the feature is first created and may // happen when the feature is re-enabled. synchronized (mLock) { if(mSlotId != slotId) { return; } switch (feature) { case ImsFeature.FEATURE_MMTEL: { if (!mIsAvailable) { Log.i(TAG, "MmTel enabled on slotId: " + slotId); mIsAvailable = true; } break; } case ImsFeature.FEATURE_EMERGENCY_MMTEL: { mSupportsEmergencyCalling = true; Log.i(TAG, "Emergency calling enabled on slotId: " + slotId); break; } } } } @Override public void imsFeatureRemoved(int slotId, int feature) throws RemoteException { synchronized (mLock) { if(mSlotId != slotId) { return; } switch (feature) { case ImsFeature.FEATURE_MMTEL: { if (mIsAvailable) { Log.i(TAG, "MmTel disabled on slotId: " + slotId); mIsAvailable = false; mmTelFeatureRemoved(); if (mStatusCallback != null) { mStatusCallback.notifyUnavailable(); } } break; } case ImsFeature.FEATURE_EMERGENCY_MMTEL : { mSupportsEmergencyCalling = false; Log.i(TAG, "Emergency calling disabled on slotId: " + slotId); break; } } } } @Override public void imsStatusChanged(int slotId, int feature, int status) throws RemoteException { synchronized (mLock) { Log.i(TAG, "imsStatusChanged: slot: " + slotId + " feature: " + feature + " status: " + status); if (mSlotId == slotId && feature == ImsFeature.FEATURE_MMTEL) { mFeatureStateCached = status; if (mStatusCallback != null) { mStatusCallback.notifyStateChanged(); } } } } }; public MmTelFeatureConnection(Context context, int slotId) { mSlotId = slotId; mContext = context; } // Called when the MmTelFeatureConnection has received an unavailable notification. private void mmTelFeatureRemoved() { synchronized (mLock) { // invalidate caches. mRegistrationBinder = null; mConfigBinder = null; } } private @Nullable IImsRegistration getRegistration() { synchronized (mLock) { // null if cache is invalid; if (mRegistrationBinder != null) { return mRegistrationBinder; } } TelephonyManager tm = getTelephonyManager(mContext); // We don't want to synchronize on a binder call to another process. IImsRegistration regBinder = tm != null ? tm.getImsRegistration(mSlotId, ImsFeature.FEATURE_MMTEL) : null; synchronized (mLock) { // mRegistrationBinder may have changed while we tried to get the registration // interface. if (mRegistrationBinder == null) { mRegistrationBinder = regBinder; } } return mRegistrationBinder; } private IImsConfig getConfig() { synchronized (mLock) { // null if cache is invalid; if (mConfigBinder != null) { return mConfigBinder; } } TelephonyManager tm = getTelephonyManager(mContext); IImsConfig configBinder = tm != null ? tm.getImsConfig(mSlotId, ImsFeature.FEATURE_MMTEL) : null; synchronized (mLock) { // mConfigBinder may have changed while we tried to get the config interface. if (mConfigBinder == null) { mConfigBinder = configBinder; } } return mConfigBinder; } public boolean isEmergencyMmTelAvailable() { return mSupportsEmergencyCalling; } public IImsServiceFeatureCallback getListener() { return mListenerBinder; } public void setBinder(IBinder binder) { mBinder = binder; } /** * Opens the connection to the {@link MmTelFeature} and establishes a listener back to the * framework. Calling this method multiple times will reset the listener attached to the * {@link MmTelFeature}. * @param listener A {@link MmTelFeature.Listener} that will be used by the {@link MmTelFeature} * to notify the framework of updates. */ public void openConnection(MmTelFeature.Listener listener) throws RemoteException { synchronized (mLock) { checkServiceIsReady(); getServiceInterface(mBinder).setListener(listener); } } public void closeConnection() { mRegistrationCallbackManager.close(); mCapabilityCallbackManager.close(); try { getServiceInterface(mBinder).setListener(null); } catch (RemoteException e) { Log.w(TAG, "closeConnection: couldn't remove listener!"); } } public void addRegistrationCallback(ImsRegistrationImplBase.Callback callback) throws RemoteException { mRegistrationCallbackManager.addCallback(callback); } public void removeRegistrationCallback(ImsRegistrationImplBase.Callback callback) throws RemoteException { mRegistrationCallbackManager.removeCallback(callback); } public void addCapabilityCallback(ImsFeature.CapabilityCallback callback) throws RemoteException { mCapabilityCallbackManager.addCallback(callback); } public void removeCapabilityCallback(ImsFeature.CapabilityCallback callback) throws RemoteException { mCapabilityCallbackManager.removeCallback(callback); } public void changeEnabledCapabilities(CapabilityChangeRequest request, ImsFeature.CapabilityCallback callback) throws RemoteException { synchronized (mLock) { checkServiceIsReady(); getServiceInterface(mBinder).changeCapabilitiesConfiguration(request, callback); } } public void queryEnabledCapabilities(int capability, int radioTech, ImsFeature.CapabilityCallback callback) throws RemoteException { synchronized (mLock) { checkServiceIsReady(); getServiceInterface(mBinder).queryCapabilityConfiguration(capability, radioTech, callback); } } public MmTelFeature.MmTelCapabilities queryCapabilityStatus() throws RemoteException { synchronized (mLock) { checkServiceIsReady(); return new MmTelFeature.MmTelCapabilities( getServiceInterface(mBinder).queryCapabilityStatus()); } } public ImsCallProfile createCallProfile(int callServiceType, int callType) throws RemoteException { synchronized (mLock) { checkServiceIsReady(); return getServiceInterface(mBinder).createCallProfile(callServiceType, callType); } } public IImsCallSession createCallSession(ImsCallProfile profile) throws RemoteException { synchronized (mLock) { checkServiceIsReady(); return getServiceInterface(mBinder).createCallSession(profile); } } public IImsUt getUtInterface() throws RemoteException { synchronized (mLock) { checkServiceIsReady(); return getServiceInterface(mBinder).getUtInterface(); } } public IImsConfig getConfigInterface() throws RemoteException { return getConfig(); } public @ImsRegistrationImplBase.ImsRegistrationTech int getRegistrationTech() throws RemoteException { IImsRegistration registration = getRegistration(); if (registration != null) { return registration.getRegistrationTechnology(); } else { return ImsRegistrationImplBase.REGISTRATION_TECH_NONE; } } public IImsEcbm getEcbmInterface() throws RemoteException { synchronized (mLock) { checkServiceIsReady(); return getServiceInterface(mBinder).getEcbmInterface(); } } public void setUiTTYMode(int uiTtyMode, Message onComplete) throws RemoteException { synchronized (mLock) { checkServiceIsReady(); getServiceInterface(mBinder).setUiTtyMode(uiTtyMode, onComplete); } } public IImsMultiEndpoint getMultiEndpointInterface() throws RemoteException { synchronized (mLock) { checkServiceIsReady(); return getServiceInterface(mBinder).getMultiEndpointInterface(); } } public void sendSms(int token, int messageRef, String format, String smsc, boolean isRetry, byte[] pdu) throws RemoteException { synchronized (mLock) { checkServiceIsReady(); getServiceInterface(mBinder).sendSms(token, messageRef, format, smsc, isRetry, pdu); } } public void acknowledgeSms(int token, int messageRef, @ImsSmsImplBase.SendStatusResult int result) throws RemoteException { synchronized (mLock) { checkServiceIsReady(); getServiceInterface(mBinder).acknowledgeSms(token, messageRef, result); } } public void acknowledgeSmsReport(int token, int messageRef, @ImsSmsImplBase.StatusReportResult int result) throws RemoteException { synchronized (mLock) { checkServiceIsReady(); getServiceInterface(mBinder).acknowledgeSmsReport(token, messageRef, result); } } public String getSmsFormat() throws RemoteException { synchronized (mLock) { checkServiceIsReady(); return getServiceInterface(mBinder).getSmsFormat(); } } public void onSmsReady() throws RemoteException { synchronized (mLock) { checkServiceIsReady(); getServiceInterface(mBinder).onSmsReady(); } } public void setSmsListener(IImsSmsListener listener) throws RemoteException { synchronized (mLock) { checkServiceIsReady(); getServiceInterface(mBinder).setSmsListener(listener); } } public @MmTelFeature.ProcessCallResult int shouldProcessCall(boolean isEmergency, String[] numbers) throws RemoteException { if (isEmergency && !isEmergencyMmTelAvailable()) { // Don't query the ImsService if emergency calling is not available on the ImsService. Log.i(TAG, "MmTel does not support emergency over IMS, fallback to CS."); return MmTelFeature.PROCESS_CALL_CSFB; } synchronized (mLock) { checkServiceIsReady(); return getServiceInterface(mBinder).shouldProcessCall(numbers); } } /** * @return an integer describing the current Feature Status, defined in * {@link ImsFeature.ImsState}. */ public int getFeatureState() { synchronized (mLock) { if (isBinderAlive() && mFeatureStateCached != null) { return mFeatureStateCached; } } // Don't synchronize on Binder call. Integer status = retrieveFeatureState(); synchronized (mLock) { if (status == null) { return ImsFeature.STATE_UNAVAILABLE; } // Cache only non-null value for feature status. mFeatureStateCached = status; } Log.i(TAG, "getFeatureState - returning " + status); return status; } /** * Internal method used to retrieve the feature status from the corresponding ImsService. */ private Integer retrieveFeatureState() { if (mBinder != null) { try { return getServiceInterface(mBinder).getFeatureState(); } catch (RemoteException e) { // Status check failed, don't update cache } } return null; } /** * @param c Callback that will fire when the feature status has changed. */ public void setStatusCallback(IFeatureUpdate c) { mStatusCallback = c; } /** * @return Returns true if the ImsService is ready to take commands, false otherwise. If this * method returns false, it doesn't mean that the Binder connection is not available (use * {@link #isBinderReady()} to check that), but that the ImsService is not accepting commands * at this time. * * For example, for DSDS devices, only one slot can be {@link ImsFeature#STATE_READY} to take * commands at a time, so the other slot must stay at {@link ImsFeature#STATE_UNAVAILABLE}. */ public boolean isBinderReady() { return isBinderAlive() && getFeatureState() == ImsFeature.STATE_READY; } /** * @return false if the binder connection is no longer alive. */ public boolean isBinderAlive() { return mIsAvailable && mBinder != null && mBinder.isBinderAlive(); } protected void checkServiceIsReady() throws RemoteException { if (!isBinderReady()) { throw new RemoteException("ImsServiceProxy is not ready to accept commands."); } } private IImsMmTelFeature getServiceInterface(IBinder b) { return IImsMmTelFeature.Stub.asInterface(b); } protected void checkBinderConnection() throws RemoteException { if (!isBinderAlive()) { throw new RemoteException("ImsServiceProxy is not available for that feature."); } } }