/* * 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