blob: 6b6dc60a47197d3a34ac367827e656fd56801737 [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.content.Context;
import android.content.pm.PackageManager;
import android.os.Handler;
import android.os.Looper;
import com.android.telephony.Rlog;
import android.telephony.ims.ImsReasonInfo;
import android.telephony.ims.feature.ImsFeature;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.telephony.util.HandlerExecutor;
import java.util.concurrent.Executor;
/**
* Helper class for managing a connection to the ImsFeature manager.
*/
public class FeatureConnector<T extends IFeatureConnector> extends Handler {
private static final String TAG = "FeatureConnector";
private static final boolean DBG = true;
// Initial condition for ims connection retry.
private static final int IMS_RETRY_STARTING_TIMEOUT_MS = 500; // ms
// Ceiling bitshift amount for service query timeout, calculated as:
// 2^mImsServiceRetryCount * IMS_RETRY_STARTING_TIMEOUT_MS, where
// mImsServiceRetryCount ∊ [0, CEILING_SERVICE_RETRY_COUNT].
private static final int CEILING_SERVICE_RETRY_COUNT = 6;
public interface Listener<T> {
/**
* Check if ImsFeature supported
*/
boolean isSupported();
/**
* Get ImsFeature manager instance
*/
T getFeatureManager();
/**
* ImsFeature manager is connected to the underlying IMS implementation.
*/
void connectionReady(T manager) throws ImsException;
/**
* The underlying IMS implementation is unavailable and can not be used to communicate.
*/
void connectionUnavailable();
}
public interface RetryTimeout {
int get();
}
protected final int mPhoneId;
protected final Context mContext;
protected final Executor mExecutor;
protected final Object mLock = new Object();
protected final String mLogPrefix;
@VisibleForTesting
public Listener<T> mListener;
// The IMS feature manager which interacts with ImsService
@VisibleForTesting
public T mManager;
protected int mRetryCount = 0;
@VisibleForTesting
public RetryTimeout mRetryTimeout = () -> {
synchronized (mLock) {
int timeout = (1 << mRetryCount) * IMS_RETRY_STARTING_TIMEOUT_MS;
if (mRetryCount <= CEILING_SERVICE_RETRY_COUNT) {
mRetryCount++;
}
return timeout;
}
};
public FeatureConnector(Context context, int phoneId, Listener<T> listener) {
mContext = context;
mPhoneId = phoneId;
mListener = listener;
mExecutor = new HandlerExecutor(this);
mLogPrefix = "?";
}
public FeatureConnector(Context context, int phoneId, Listener<T> listener,
String logPrefix) {
mContext = context;
mPhoneId = phoneId;
mListener = listener;
mExecutor = new HandlerExecutor(this);
mLogPrefix = logPrefix;
}
@VisibleForTesting
public FeatureConnector(Context context, int phoneId, Listener<T> listener,
Executor executor, String logPrefix) {
mContext = context;
mPhoneId = phoneId;
mListener= listener;
mExecutor = executor;
mLogPrefix = logPrefix;
}
@VisibleForTesting
public FeatureConnector(Context context, int phoneId, Listener<T> listener,
Executor executor, Looper looper) {
super(looper);
mContext = context;
mPhoneId = phoneId;
mListener= listener;
mExecutor = executor;
mLogPrefix = "?";
}
/**
* Start the creation of a connection to the underlying ImsService implementation. When the
* service is connected, {@link FeatureConnector.Listener#connectionReady(Object)} 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()) {
logw("connect: not supported.");
return;
}
mRetryCount = 0;
// Send a message to connect to the Ims Service and open a connection through
// getImsService().
post(mGetServiceRunnable);
}
// Check if this ImsFeature is supported or not.
private boolean isSupported() {
return mListener.isSupported();
}
/**
* Disconnect from the ImsService Implementation and clean up. When this is complete,
* {@link FeatureConnector.Listener#connectionUnavailable()} will be called one last time.
*/
public void disconnect() {
if (DBG) log("disconnect");
removeCallbacks(mGetServiceRunnable);
synchronized (mLock) {
if (mManager != null) {
mManager.removeNotifyStatusChangedCallback(mNotifyStatusChangedCallback);
}
}
notifyNotReady();
}
private final Runnable mGetServiceRunnable = () -> {
try {
createImsService();
} catch (ImsException e) {
int errorCode = e.getCode();
if (DBG) logw("Create IMS service error: " + errorCode);
if (ImsReasonInfo.CODE_LOCAL_IMS_NOT_SUPPORTED_ON_DEVICE != errorCode) {
// Retry when error is not IMS_NOT_SUPPORTED_ON_DEVICE
retryGetImsService();
}
}
};
@VisibleForTesting
public void createImsService() throws ImsException {
synchronized (mLock) {
if (DBG) log("createImsService");
mManager = mListener.getFeatureManager();
// Adding to set, will be safe adding multiple times. If the ImsService is not
// active yet, this method will throw an ImsException.
mManager.addNotifyStatusChangedCallbackIfAvailable(mNotifyStatusChangedCallback);
}
// Wait for ImsService.STATE_READY to start listening for calls.
// Call the callback right away for compatibility with older devices that do not use
// states.
mNotifyStatusChangedCallback.notifyStateChanged();
}
/**
* Remove callback and re-running mGetServiceRunnable
*/
public void retryGetImsService() {
if (mManager != null) {
// remove callback so we do not receive updates from old ImsServiceProxy when
// switching between ImsServices.
mManager.removeNotifyStatusChangedCallback(mNotifyStatusChangedCallback);
//Leave mImsManager as null, then CallStateException will be thrown when dialing
mManager = null;
}
// Exponential backoff during retry, limited to 32 seconds.
removeCallbacks(mGetServiceRunnable);
int timeout = mRetryTimeout.get();
postDelayed(mGetServiceRunnable, timeout);
if (DBG) log("retryGetImsService: unavailable, retrying in " + timeout + " seconds");
}
// Callback fires when IMS Feature changes state
public FeatureConnection.IFeatureUpdate mNotifyStatusChangedCallback =
new FeatureConnection.IFeatureUpdate() {
@Override
public void notifyStateChanged() {
mExecutor.execute(() -> {
try {
int status = ImsFeature.STATE_UNAVAILABLE;
synchronized (mLock) {
if (mManager != null) {
status = mManager.getImsServiceState();
}
}
switch (status) {
case ImsFeature.STATE_READY: {
notifyReady();
break;
}
case ImsFeature.STATE_INITIALIZING:
// fall through
case ImsFeature.STATE_UNAVAILABLE: {
notifyNotReady();
break;
}
default: {
logw("Unexpected State! " + status);
}
}
} catch (ImsException e) {
// Could not get the ImsService, retry!
notifyNotReady();
retryGetImsService();
}
});
}
@Override
public void notifyUnavailable() {
mExecutor.execute(() -> {
notifyNotReady();
retryGetImsService();
});
}
};
private void notifyReady() throws ImsException {
T manager;
synchronized (mLock) {
manager = mManager;
}
try {
if (DBG) log("notifyReady");
mListener.connectionReady(manager);
}
catch (ImsException e) {
logw("notifyReady exception: " + e.getMessage());
throw e;
}
// Only reset retry count if connectionReady does not generate an ImsException/
synchronized (mLock) {
mRetryCount = 0;
}
}
protected void notifyNotReady() {
if (DBG) log("notifyNotReady");
mListener.connectionUnavailable();
}
private final void log(String message) {
Rlog.d(TAG, "[" + mLogPrefix + ", " + mPhoneId + "] " + message);
}
private final void logw(String message) {
Rlog.w(TAG, "[" + mLogPrefix + ", " + mPhoneId + "] " + message);
}
}