/* * 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.IntDef; import android.content.Context; import android.content.pm.PackageManager; import android.os.RemoteException; import android.telephony.ims.ImsReasonInfo; import android.telephony.ims.ImsService; import android.telephony.ims.feature.ImsFeature; import com.android.ims.internal.IImsServiceFeatureCallback; import com.android.internal.annotations.VisibleForTesting; import com.android.telephony.Rlog; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.List; import java.util.concurrent.Executor; /** * Helper class for managing a connection to the ImsFeature manager. */ public class FeatureConnector { private static final String TAG = "FeatureConnector"; private static final boolean DBG = false; /** * This Connection has become unavailable due to the ImsService being disconnected due to * an event such as SIM Swap, carrier configuration change, etc... * * {@link Listener#connectionReady} will be called when a new Manager is available. */ public static final int UNAVAILABLE_REASON_DISCONNECTED = 0; /** * This Connection has become unavailable due to the ImsService moving to the NOT_READY state. * * {@link Listener#connectionReady} will be called when the manager moves back to ready. */ public static final int UNAVAILABLE_REASON_NOT_READY = 1; /** * IMS is not supported on this device. This should be considered a permanent error and * a Manager will never become available. */ public static final int UNAVAILABLE_REASON_IMS_UNSUPPORTED = 2; /** * The server of this information has crashed or otherwise generated an error that will require * a retry to connect. This is rare, however in this case, {@link #disconnect()} and * {@link #connect()} will need to be called again to recreate the connection with the server. *

* Only applicable if this is used outside of the server's own process. */ public static final int UNAVAILABLE_REASON_SERVER_UNAVAILABLE = 3; /** * @hide */ @Retention(RetentionPolicy.SOURCE) @IntDef(prefix = "UNAVAILABLE_REASON_", value = { UNAVAILABLE_REASON_DISCONNECTED, UNAVAILABLE_REASON_NOT_READY, UNAVAILABLE_REASON_IMS_UNSUPPORTED, UNAVAILABLE_REASON_SERVER_UNAVAILABLE }) public @interface UnavailableReason {} /** * Factory used to create a new instance of the manager that this FeatureConnector is waiting * to connect the FeatureConnection to. * @param The Manager that this FeatureConnector has been created for. */ public interface ManagerFactory { /** * Create a manager instance, which will connect to the FeatureConnection. */ U createManager(Context context, int phoneId); } /** * Listener interface used by Listeners of FeatureConnector that are waiting for a Manager * interface for a specific ImsFeature. * @param The Manager that the listener is listening for. */ public interface Listener { /** * ImsFeature manager is connected to the underlying IMS implementation. */ void connectionReady(U manager, int subId) throws ImsException; /** * The underlying IMS implementation is unavailable and can not be used to communicate. */ void connectionUnavailable(@UnavailableReason int reason); } private final IImsServiceFeatureCallback mCallback = new IImsServiceFeatureCallback.Stub() { @Override public void imsFeatureCreated(ImsFeatureContainer c, int subId) { log("imsFeatureCreated: " + c + ", subId: " + subId); synchronized (mLock) { mManager.associate(c, subId); mManager.updateFeatureCapabilities(c.getCapabilities()); mDisconnectedReason = null; } // Notifies executor, so notify outside of lock imsStatusChanged(c.getState(), subId); } @Override public void imsFeatureRemoved(@UnavailableReason int reason) { log("imsFeatureRemoved: reason=" + reason); synchronized (mLock) { // only generate new events if the disconnect event isn't the same as before, except // for UNAVAILABLE_REASON_SERVER_UNAVAILABLE, which indicates a local issue and // each event is actionable. if (mDisconnectedReason != null && (mDisconnectedReason == reason && mDisconnectedReason != UNAVAILABLE_REASON_SERVER_UNAVAILABLE)) { log("imsFeatureRemoved: ignore"); return; } mDisconnectedReason = reason; // Ensure that we set ready state back to false so that we do not miss setting ready // later if the initial state when recreated is READY. mLastReadyState = false; } // Allow the listener to do cleanup while the connection still potentially valid (unless // the process crashed). mExecutor.execute(() -> mListener.connectionUnavailable(reason)); mManager.invalidate(); } @Override public void imsStatusChanged(int status, int subId) { log("imsStatusChanged: status=" + ImsFeature.STATE_LOG_MAP.get(status)); final U manager; final boolean isReady; synchronized (mLock) { if (mDisconnectedReason != null) { log("imsStatusChanged: ignore"); return; } mManager.updateFeatureState(status); manager = mManager; isReady = mReadyFilter.contains(status); boolean didReadyChange = isReady ^ mLastReadyState; mLastReadyState = isReady; if (!didReadyChange) { log("imsStatusChanged: ready didn't change, ignore"); return; } } mExecutor.execute(() -> { try { if (isReady) { notifyReady(manager, subId); } else { notifyNotReady(); } } catch (ImsException e) { if (e.getCode() == ImsReasonInfo.CODE_LOCAL_IMS_NOT_SUPPORTED_ON_DEVICE) { mListener.connectionUnavailable(UNAVAILABLE_REASON_IMS_UNSUPPORTED); } else { notifyNotReady(); } } }); } @Override public void updateCapabilities(long caps) { log("updateCapabilities: capabilities=" + ImsService.getCapabilitiesString(caps)); synchronized (mLock) { if (mDisconnectedReason != null) { log("updateCapabilities: ignore"); return; } mManager.updateFeatureCapabilities(caps); } } }; private final int mPhoneId; private final Context mContext; private final ManagerFactory mFactory; private final Listener mListener; private final Executor mExecutor; private final Object mLock = new Object(); private final String mLogPrefix; // A List of integers, each corresponding to an ImsFeature.ImsState, that the FeatureConnector // will use to call Listener#connectionReady when the ImsFeature that this connector is waiting // for changes into one of the states in this list. private final List mReadyFilter = new ArrayList<>(); private U mManager; // Start in disconnected state; private Integer mDisconnectedReason = UNAVAILABLE_REASON_DISCONNECTED; // Stop redundant connectionAvailable if the ready filter contains multiple states. // Also, do not send the first unavailable until after we have moved to available once. private boolean mLastReadyState = false; @VisibleForTesting public FeatureConnector(Context context, int phoneId, ManagerFactory factory, String logPrefix, List readyFilter, Listener listener, Executor executor) { mContext = context; mPhoneId = phoneId; mFactory = factory; mLogPrefix = logPrefix; mReadyFilter.addAll(readyFilter); mListener = listener; mExecutor = executor; } /** * Start the creation of a connection to the underlying ImsService implementation. When the * service is connected, {@link FeatureConnector.Listener#connectionReady} will be * called with an active instance. * * If this device does not support an ImsStack (i.e. doesn't support * {@link PackageManager#FEATURE_TELEPHONY_IMS} feature), this method will do nothing. */ public void connect() { if (DBG) log("connect"); if (!isSupported()) { mExecutor.execute(() -> mListener.connectionUnavailable( UNAVAILABLE_REASON_IMS_UNSUPPORTED)); logw("connect: not supported."); return; } synchronized (mLock) { if (mManager == null) { mManager = mFactory.createManager(mContext, mPhoneId); } } mManager.registerFeatureCallback(mPhoneId, mCallback); } // Check if this ImsFeature is supported or not. private boolean isSupported() { return mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELEPHONY_IMS); } /** * Disconnect from the ImsService Implementation and clean up. When this is complete, * {@link FeatureConnector.Listener#connectionUnavailable(int)} will be called one last time. */ public void disconnect() { if (DBG) log("disconnect"); final U manager; synchronized (mLock) { manager = mManager; } if (manager == null) return; manager.unregisterFeatureCallback(mCallback); try { mCallback.imsFeatureRemoved(UNAVAILABLE_REASON_DISCONNECTED); } catch (RemoteException ignore) {} // local call } // Should be called on executor private void notifyReady(U manager, int subId) throws ImsException { try { if (DBG) log("notifyReady"); mListener.connectionReady(manager, subId); } catch (ImsException e) { if(DBG) log("notifyReady exception: " + e.getMessage()); throw e; } } // Should be called on executor. private void notifyNotReady() { if (DBG) log("notifyNotReady"); mListener.connectionUnavailable(UNAVAILABLE_REASON_NOT_READY); } private void log(String message) { Rlog.d(TAG, "[" + mLogPrefix + ", " + mPhoneId + "] " + message); } private void logw(String message) { Rlog.w(TAG, "[" + mLogPrefix + ", " + mPhoneId + "] " + message); } }