blob: 32674d9cdd7a335771b8017d22f8f7db8cfdfe2b [file] [log] [blame]
/*
* 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<U extends FeatureUpdates> {
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.
* <p>
* 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 <U> The Manager that this FeatureConnector has been created for.
*/
public interface ManagerFactory<U extends FeatureUpdates> {
/**
* 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 <U> The Manager that the listener is listening for.
*/
public interface Listener<U extends FeatureUpdates> {
/**
* 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<U> mFactory;
private final Listener<U> 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<Integer> 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<U> factory,
String logPrefix, List<Integer> readyFilter, Listener<U> 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);
}
}