/* * Copyright (c) 2015, Motorola Mobility LLC * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * - Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * - Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * - Neither the name of Motorola Mobility nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL MOTOROLA MOBILITY LLC BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH * DAMAGE. */ package com.android.service.ims; import android.app.Service; import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.database.ContentObserver; import android.net.ConnectivityManager; import android.os.Handler; import android.os.IBinder; import android.os.Looper; import android.os.RemoteException; import android.os.ServiceManager; import android.provider.Settings; import android.provider.Telephony; import android.telecom.TelecomManager; import android.telephony.AccessNetworkConstants; import android.telephony.SubscriptionManager; import android.telephony.ims.ImsException; import android.telephony.ims.ImsMmTelManager; import android.telephony.ims.ImsReasonInfo; import android.telephony.ims.ProvisioningManager; import android.telephony.ims.RcsContactUceCapability; import android.telephony.ims.RegistrationManager; import android.telephony.ims.feature.MmTelFeature; import com.android.ims.IRcsPresenceListener; import com.android.ims.RcsPresenceInfo; import com.android.ims.ResultCode; import com.android.ims.RcsPresence; import com.android.ims.internal.IRcsPresence; import com.android.ims.internal.IRcsService; import com.android.ims.internal.Logger; import com.android.service.ims.R; import com.android.service.ims.presence.ContactCapabilityResponse; import com.android.service.ims.presence.PresenceBase; import com.android.service.ims.presence.PresencePublication; import com.android.service.ims.presence.PresenceSubscriber; import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; public class RcsService extends Service { private static final int IMS_SERVICE_RETRY_TIMEOUT_MS = 5000; private Logger logger = Logger.getLogger(this.getClass().getName()); private RcsStackAdaptor mRcsStackAdaptor = null; private PresencePublication mPublication = null; private PresenceSubscriber mSubscriber = null; private Handler mRetryHandler; private Runnable mRegisterCallbacks = this::registerImsCallbacksAndSetAssociatedSubscription; private int mNetworkRegistrationType = AccessNetworkConstants.TRANSPORT_TYPE_INVALID; private int mAssociatedSubscription = SubscriptionManager.INVALID_SUBSCRIPTION_ID; private SubscriptionManager.OnSubscriptionsChangedListener mOnSubscriptionsChangedListener = new SubscriptionManager.OnSubscriptionsChangedListener() { @Override public void onSubscriptionsChanged() { registerImsCallbacksAndSetAssociatedSubscription(); } }; private BroadcastReceiver mReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { switch (intent.getAction()) { case TelecomManager.ACTION_TTY_PREFERRED_MODE_CHANGED: { int newPreferredTtyMode = intent.getIntExtra( TelecomManager.EXTRA_TTY_PREFERRED_MODE, TelecomManager.TTY_MODE_OFF); if (mPublication != null) { mPublication.onTtyPreferredModeChanged(newPreferredTtyMode); } break; } case Intent.ACTION_AIRPLANE_MODE_CHANGED: { boolean airplaneMode = intent.getBooleanExtra("state", false); if (mPublication != null) { mPublication.onAirplaneModeChanged(airplaneMode); } break; } case SubscriptionManager.ACTION_DEFAULT_SUBSCRIPTION_CHANGED: { mRetryHandler.removeCallbacks(mRegisterCallbacks); mRetryHandler.post(mRegisterCallbacks); } } } }; private class CapabilityResultListener implements ContactCapabilityResponse { private final IRcsPresenceListener mListener; public CapabilityResultListener(IRcsPresenceListener listener) { mListener = listener; } @Override public void onSuccess(int reqId) { try { mListener.onSuccess(reqId); } catch (RemoteException e) { logger.warn("CapabilityResultListener: onSuccess exception = " + e.getMessage()); } } @Override public void onError(int reqId, int resultCode) { try { mListener.onError(reqId, resultCode); } catch (RemoteException e) { logger.warn("CapabilityResultListener: onError exception = " + e.getMessage()); } } @Override public void onFinish(int reqId) { try { mListener.onFinish(reqId); } catch (RemoteException e) { logger.warn("CapabilityResultListener: onFinish exception = " + e.getMessage()); } } @Override public void onTimeout(int reqId) { try { mListener.onTimeout(reqId); } catch (RemoteException e) { logger.warn("CapabilityResultListener: onTimeout exception = " + e.getMessage()); } } @Override public void onCapabilitiesUpdated(int reqId, List contactCapabilities, boolean updateLastTimestamp) { ArrayList presenceInfoList = contactCapabilities.stream().map( PresenceInfoParser::getRcsPresenceInfo).collect( Collectors.toCollection(ArrayList::new)); logger.debug("capabilities updated:"); for (RcsPresenceInfo info : presenceInfoList) { logger.debug("capabilities updated: info -" + info); } // For some reason it uses an intent to send this info back instead of just using the // active binder... Intent intent = new Intent(RcsPresence.ACTION_PRESENCE_CHANGED); intent.putParcelableArrayListExtra(RcsPresence.EXTRA_PRESENCE_INFO_LIST, presenceInfoList); intent.putExtra("updateLastTimestamp", updateLastTimestamp); launchPersistService(intent); } } private void launchPersistService(Intent intent) { ComponentName component = new ComponentName("com.android.service.ims.presence", "com.android.service.ims.presence.PersistService"); intent.setComponent(component); startService(intent); } @Override public void onCreate() { super.onCreate(); logger.debug("RcsService onCreate"); mRcsStackAdaptor = RcsStackAdaptor.getInstance(this); mPublication = new PresencePublication(mRcsStackAdaptor, this, getResources().getStringArray( R.array.config_volte_provision_error_on_publish_response), getResources().getStringArray( R.array.config_rcs_provision_error_on_publish_response)); mRcsStackAdaptor.getListener().setPresencePublication(mPublication); mSubscriber = new PresenceSubscriber(mRcsStackAdaptor, this, getResources().getStringArray( R.array.config_volte_provision_error_on_subscribe_response), getResources().getStringArray( R.array.config_rcs_provision_error_on_subscribe_response)); mRcsStackAdaptor.getListener().setPresenceSubscriber(mSubscriber); mPublication.setSubscriber(mSubscriber); ConnectivityManager cm = ConnectivityManager.from(this); if (cm != null) { boolean enabled = Settings.Global.getInt(getContentResolver(), Settings.Global.MOBILE_DATA, 1) == 1; logger.debug("Mobile data enabled status: " + (enabled ? "ON" : "OFF")); onMobileDataEnabled(enabled); } // TODO: support MSIM ServiceManager.addService("rcs", mBinder); mObserver = new MobileDataContentObserver(); getContentResolver().registerContentObserver( Settings.Global.getUriFor(Settings.Global.MOBILE_DATA), false, mObserver); mSiminfoSettingObserver = new SimInfoContentObserver(); getContentResolver().registerContentObserver(Telephony.SimInfo.CONTENT_URI, false, mSiminfoSettingObserver); mRetryHandler = new Handler(Looper.getMainLooper()); registerBroadcastReceiver(); registerSubscriptionChangedListener(); } private void registerBroadcastReceiver() { IntentFilter filter = new IntentFilter(TelecomManager.ACTION_TTY_PREFERRED_MODE_CHANGED); filter.addAction(Intent.ACTION_AIRPLANE_MODE_CHANGED); filter.addAction(SubscriptionManager.ACTION_DEFAULT_SUBSCRIPTION_CHANGED); registerReceiver(mReceiver, filter); } private void unregisterBroadcastReceiver() { unregisterReceiver(mReceiver); } private void registerSubscriptionChangedListener() { SubscriptionManager subscriptionManager = getSystemService(SubscriptionManager.class); if (subscriptionManager != null) { // This will call back after the listener is added automatically. subscriptionManager.addOnSubscriptionsChangedListener(mOnSubscriptionsChangedListener); } else { logger.error("SubscriptionManager not available! Retrying..."); // Retry this again after some time. mRetryHandler.postDelayed(this::registerSubscriptionChangedListener, IMS_SERVICE_RETRY_TIMEOUT_MS); } } private void registerImsCallbacksAndSetAssociatedSubscription() { SubscriptionManager sm = getSystemService(SubscriptionManager.class); if (sm == null) { logger.warn("handleSubscriptionsChanged: SubscriptionManager is null!"); return; } int defaultSub = RcsSettingUtils.getDefaultSubscriptionId(this); if (!SubscriptionManager.isValidSubscriptionId(defaultSub)) { mAssociatedSubscription = SubscriptionManager.INVALID_SUBSCRIPTION_ID; handleAssociatedSubscriptionChanged(mAssociatedSubscription); return; } ImsMmTelManager imsManager = ImsMmTelManager.createForSubscriptionId(defaultSub); ProvisioningManager provisioningManager = ProvisioningManager.createForSubscriptionId(defaultSub); try { if (defaultSub == mAssociatedSubscription) { // Don't register duplicate callbacks for the same subscription. return; } if (mAssociatedSubscription != SubscriptionManager.INVALID_SUBSCRIPTION_ID) { // Get rid of any existing registrations. ImsMmTelManager oldImsManager = ImsMmTelManager.createForSubscriptionId( mAssociatedSubscription); ProvisioningManager oldProvisioningManager = ProvisioningManager.createForSubscriptionId(mAssociatedSubscription); oldImsManager.unregisterImsRegistrationCallback(mImsRegistrationCallback); oldImsManager.unregisterMmTelCapabilityCallback(mCapabilityCallback); oldProvisioningManager.unregisterProvisioningChangedCallback( mProvisioningChangedCallback); logger.print("callbacks unregistered for sub " + mAssociatedSubscription); } // move over registrations. imsManager.registerImsRegistrationCallback(getMainExecutor(), mImsRegistrationCallback); imsManager.registerMmTelCapabilityCallback(getMainExecutor(), mCapabilityCallback); provisioningManager.registerProvisioningChangedCallback(getMainExecutor(), mProvisioningChangedCallback); mAssociatedSubscription = defaultSub; logger.print("registerImsCallbacksAndSetAssociatedSubscription: new default=" + defaultSub); logger.print("callbacks registered for sub " + mAssociatedSubscription); handleAssociatedSubscriptionChanged(mAssociatedSubscription); } catch (ImsException e) { logger.info("Couldn't register callbacks for " + defaultSub + ": " + e.getMessage()); if (e.getCode() == ImsException.CODE_ERROR_SERVICE_UNAVAILABLE) { handleAssociatedSubscriptionChanged(SubscriptionManager.INVALID_SUBSCRIPTION_ID); // IMS temporarily unavailable. Retry after a few seconds. mRetryHandler.removeCallbacks(mRegisterCallbacks); mRetryHandler.postDelayed(mRegisterCallbacks, IMS_SERVICE_RETRY_TIMEOUT_MS); } } } private void handleAssociatedSubscriptionChanged(int newSubId) { if (mSubscriber != null) { mSubscriber.handleAssociatedSubscriptionChanged(newSubId); } if (mPublication != null) { mPublication.handleAssociatedSubscriptionChanged(newSubId); } if (mRcsStackAdaptor != null) { mRcsStackAdaptor.handleAssociatedSubscriptionChanged(newSubId); } } @Override public int onStartCommand(Intent intent, int flags, int startId) { logger.debug("RcsService onStartCommand"); return super.onStartCommand(intent, flags, startId); } /** * Cleans up when the service is destroyed */ @Override public void onDestroy() { unregisterBroadcastReceiver(); getContentResolver().unregisterContentObserver(mObserver); getContentResolver().unregisterContentObserver(mSiminfoSettingObserver); getSystemService(SubscriptionManager.class) .removeOnSubscriptionsChangedListener(mOnSubscriptionsChangedListener); mRcsStackAdaptor.finish(); mPublication = null; mSubscriber = null; logger.debug("RcsService onDestroy"); super.onDestroy(); } public PresencePublication getPublication() { return mPublication; } IRcsPresence.Stub mIRcsPresenceImpl = new IRcsPresence.Stub() { /** * Asyncrhonously request the latest capability for a given contact list. * The result will be saved to DB directly if the contactNumber can be found in DB. * And then send intent com.android.ims.presence.CAPABILITY_STATE_CHANGED to notify it. * @param contactsNumber the contact list which will request capability. * Currently only support phone number. * @param listener the listener to get the response. * @return the resultCode which is defined by ResultCode. * @note framework uses only. * @hide */ public int requestCapability(List contactsNumber, IRcsPresenceListener listener){ logger.debug("calling requestCapability"); if(mSubscriber == null){ logger.debug("requestCapability, mPresenceSubscriber == null"); return ResultCode.ERROR_SERVICE_NOT_AVAILABLE; } return mSubscriber.requestCapability(contactsNumber, new CapabilityResultListener(listener)); } /** * Asyncrhonously request the latest presence for a given contact. * The result will be saved to DB directly if it can be found in DB. And then send intent * com.android.ims.presence.AVAILABILITY_STATE_CHANGED to notify it. * @param contactNumber the contact which will request available. * Currently only support phone number. * @param listener the listener to get the response. * @return the resultCode which is defined by ResultCode. * @note framework uses only. * @hide */ public int requestAvailability(String contactNumber, IRcsPresenceListener listener){ if(mSubscriber == null){ logger.error("requestAvailability, mPresenceSubscriber is null"); return ResultCode.ERROR_SERVICE_NOT_AVAILABLE; } // check availability cache (in RAM). return mSubscriber.requestAvailability(contactNumber, new CapabilityResultListener(listener), false); } /** * Same as requestAvailability. but requestAvailability will consider throttle to avoid too * fast call. Which means it will not send the request to network in next 60s for the same * request. * The error code SUBSCRIBE_TOO_FREQUENTLY will be returned under the case. * But for this funcation it will always send the request to network. * * @see IRcsPresenceListener * @see RcsManager.ResultCode * @see ResultCode.SUBSCRIBE_TOO_FREQUENTLY */ public int requestAvailabilityNoThrottle(String contactNumber, IRcsPresenceListener listener) { if(mSubscriber == null){ logger.error("requestAvailabilityNoThrottle, mPresenceSubscriber is null"); return ResultCode.ERROR_SERVICE_NOT_AVAILABLE; } // check availability cache (in RAM). return mSubscriber.requestAvailability(contactNumber, new CapabilityResultListener(listener), true); } public int getPublishState() throws RemoteException { return publisherPublishStateToPublishState(mPublication.getPublishState()); } }; @Override public IBinder onBind(Intent arg0) { return mBinder; } /** * Receives notifications when Mobile data is enabled or disabled. */ private class MobileDataContentObserver extends ContentObserver { public MobileDataContentObserver() { super(new Handler()); } @Override public void onChange(final boolean selfChange) { boolean enabled = Settings.Global.getInt(getContentResolver(), Settings.Global.MOBILE_DATA, 1) == 1; logger.debug("Mobile data enabled status: " + (enabled ? "ON" : "OFF")); onMobileDataEnabled(enabled); } } /** Observer to get notified when Mobile data enabled status changes */ private MobileDataContentObserver mObserver; private void onMobileDataEnabled(final boolean enabled) { logger.debug("Enter onMobileDataEnabled: " + enabled); Thread thread = new Thread(new Runnable() { @Override public void run() { try{ if(mPublication != null){ mPublication.onMobileDataChanged(enabled); return; } }catch(Exception e){ logger.error("Exception onMobileDataEnabled:", e); } } }, "onMobileDataEnabled thread"); thread.start(); } private SimInfoContentObserver mSiminfoSettingObserver; /** * Receives notifications when the TelephonyProvider is changed. */ private class SimInfoContentObserver extends ContentObserver { public SimInfoContentObserver() { super(new Handler()); } @Override public void onChange(final boolean selfChange) { if (mAssociatedSubscription == SubscriptionManager.INVALID_SUBSCRIPTION_ID) { return; } ImsMmTelManager ims = ImsMmTelManager.createForSubscriptionId(mAssociatedSubscription); try { boolean enabled = ims.isVtSettingEnabled(); logger.debug("vt enabled status: " + (enabled ? "ON" : "OFF")); onVtEnabled(enabled); } catch (Exception e) { logger.info("Exception getting VT status for sub:" + mAssociatedSubscription + ", Exception = " + e.getMessage()); } } } private void onVtEnabled(boolean enabled) { if(mPublication != null){ mPublication.onVtEnabled(enabled); } } private final IRcsService.Stub mBinder = new IRcsService.Stub() { /** * return true if the rcs service is ready for use. */ public boolean isRcsServiceAvailable(){ logger.debug("calling isRcsServiceAvailable"); if(mRcsStackAdaptor == null){ return false; } return mRcsStackAdaptor.isImsEnableState(); } /** * Gets the presence interface. * * @see IRcsPresence */ public IRcsPresence getRcsPresenceInterface(){ return mIRcsPresenceImpl; } }; private RegistrationManager.RegistrationCallback mImsRegistrationCallback = new RegistrationManager.RegistrationCallback() { @Override public void onRegistered(int imsTransportType) { logger.debug("onImsConnected imsTransportType=" + imsTransportType); mNetworkRegistrationType = imsTransportType; if(mPublication != null) { mPublication.onImsConnected(); } } @Override public void onUnregistered(ImsReasonInfo info) { logger.debug("onImsDisconnected"); mNetworkRegistrationType = AccessNetworkConstants.TRANSPORT_TYPE_INVALID; if(mPublication != null) { mPublication.onImsDisconnected(); } } }; private ImsMmTelManager.CapabilityCallback mCapabilityCallback = new ImsMmTelManager.CapabilityCallback() { @Override public void onCapabilitiesStatusChanged(MmTelFeature.MmTelCapabilities capabilities) { mPublication.onFeatureCapabilityChanged(mNetworkRegistrationType, capabilities); } }; private ProvisioningManager.Callback mProvisioningChangedCallback = new ProvisioningManager.Callback() { @Override public void onProvisioningIntChanged(int item, int value) { switch (item) { case ProvisioningManager.KEY_EAB_PROVISIONING_STATUS: // intentional fallthrough case ProvisioningManager.KEY_VOLTE_PROVISIONING_STATUS: // intentional fallthrough case ProvisioningManager.KEY_VT_PROVISIONING_STATUS: if (mPublication != null) { mPublication.handleProvisioningChanged(); } } } }; private static int publisherPublishStateToPublishState(int publisherPublishState) { switch(publisherPublishState) { case PresenceBase.PUBLISH_STATE_200_OK: return RcsPresence.PublishState.PUBLISH_STATE_200_OK; case PresenceBase.PUBLISH_STATE_NOT_PUBLISHED: return RcsPresence.PublishState.PUBLISH_STATE_NOT_PUBLISHED; case PresenceBase.PUBLISH_STATE_VOLTE_PROVISION_ERROR: return RcsPresence.PublishState.PUBLISH_STATE_VOLTE_PROVISION_ERROR; case PresenceBase.PUBLISH_STATE_RCS_PROVISION_ERROR: return RcsPresence.PublishState.PUBLISH_STATE_RCS_PROVISION_ERROR; case PresenceBase.PUBLISH_STATE_REQUEST_TIMEOUT: return RcsPresence.PublishState.PUBLISH_STATE_REQUEST_TIMEOUT; case PresenceBase.PUBLISH_STATE_OTHER_ERROR: return RcsPresence.PublishState.PUBLISH_STATE_OTHER_ERROR; } return PresenceBase.PUBLISH_STATE_OTHER_ERROR; } }