blob: 1494738e9629eea1d7c8eb043efe8ac4e66a9dd0 [file] [log] [blame]
/*
* Copyright (C) 2020 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.rcs.uce;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
import android.net.Uri;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.RemoteException;
import android.telephony.ims.RcsContactUceCapability;
import android.telephony.ims.RcsContactUceCapability.CapabilityMechanism;
import android.telephony.ims.RcsUceAdapter;
import android.telephony.ims.RcsUceAdapter.PublishState;
import android.telephony.ims.RcsUceAdapter.StackPublishTriggerType;
import android.telephony.ims.aidl.IOptionsRequestCallback;
import android.telephony.ims.aidl.IRcsUceControllerCallback;
import android.telephony.ims.aidl.IRcsUcePublishStateCallback;
import android.util.IndentingPrintWriter;
import android.util.LocalLog;
import android.util.Log;
import com.android.ims.RcsFeatureManager;
import com.android.ims.rcs.uce.eab.EabCapabilityResult;
import com.android.ims.rcs.uce.eab.EabController;
import com.android.ims.rcs.uce.eab.EabControllerImpl;
import com.android.ims.rcs.uce.options.OptionsController;
import com.android.ims.rcs.uce.options.OptionsControllerImpl;
import com.android.ims.rcs.uce.presence.publish.PublishController;
import com.android.ims.rcs.uce.presence.publish.PublishControllerImpl;
import com.android.ims.rcs.uce.presence.subscribe.SubscribeController;
import com.android.ims.rcs.uce.presence.subscribe.SubscribeControllerImpl;
import com.android.ims.rcs.uce.request.UceRequestManager;
import com.android.ims.rcs.uce.util.UceUtils;
import com.android.internal.annotations.VisibleForTesting;
import java.io.PrintWriter;
import java.time.Duration;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.List;
import java.util.Set;
/**
* The UceController will manage the RCS UCE requests on a per subscription basis. When it receives
* the UCE requests from the RCS applications and from the ImsService, it will coordinate the
* cooperation between the publish/subscribe/options components to complete the requests.
*/
public class UceController {
private static final String LOG_TAG = UceUtils.getLogPrefix() + "UceController";
/**
* The callback interface is called by the internal controllers to receive information from
* others controllers.
*/
public interface UceControllerCallback {
/**
* Retrieve the capabilities associated with the given uris from the cache.
*/
List<EabCapabilityResult> getCapabilitiesFromCache(@NonNull List<Uri> uris);
/**
* Retrieve the contact's capabilities from the availability cache.
*/
EabCapabilityResult getAvailabilityFromCache(@NonNull Uri uri);
/**
* Store the given capabilities to the cache.
*/
void saveCapabilities(List<RcsContactUceCapability> contactCapabilities);
/**
* Retrieve the device's capabilities.
*/
RcsContactUceCapability getDeviceCapabilities(@CapabilityMechanism int mechanism);
/**
* The network reply that the request is forbidden.
* @param isForbidden If UCE requests are forbidden by the network.
* @param errorCode The {@link RcsUceAdapter#ErrorCode} of the forbidden reason.
* @param retryAfterMillis The time to wait for the retry.
*/
void updateRequestForbidden(boolean isForbidden, @Nullable Integer errorCode,
long retryAfterMillis);
/**
* Get the milliseconds need to wait for retry.
* @return The milliseconds need to wait
*/
long getRetryAfterMillis();
/**
* Check if UCE request is forbidden by the network.
* @return true when the UCE is forbidden by the network
*/
boolean isRequestForbiddenByNetwork();
/**
* The method is called when the given contacts' capabilities are expired and need to be
* refreshed.
*/
void refreshCapabilities(@NonNull List<Uri> contactNumbers,
@NonNull IRcsUceControllerCallback callback) throws RemoteException;
}
/**
* Used to inject RequestManger instances for testing.
*/
@VisibleForTesting
public interface RequestManagerFactory {
UceRequestManager createRequestManager(Context context, int subId, Looper looper,
UceControllerCallback callback);
}
private RequestManagerFactory mRequestManagerFactory = (context, subId, looper, callback) ->
new UceRequestManager(context, subId, looper, callback);
/**
* Used to inject Controller instances for testing.
*/
@VisibleForTesting
public interface ControllerFactory {
/**
* @return an {@link EabController} associated with the subscription id specified.
*/
EabController createEabController(Context context, int subId, UceControllerCallback c,
Looper looper);
/**
* @return an {@link PublishController} associated with the subscription id specified.
*/
PublishController createPublishController(Context context, int subId,
UceControllerCallback c, Looper looper);
/**
* @return an {@link SubscribeController} associated with the subscription id specified.
*/
SubscribeController createSubscribeController(Context context, int subId);
/**
* @return an {@link OptionsController} associated with the subscription id specified.
*/
OptionsController createOptionsController(Context context, int subId);
}
private ControllerFactory mControllerFactory = new ControllerFactory() {
@Override
public EabController createEabController(Context context, int subId,
UceControllerCallback c, Looper looper) {
return new EabControllerImpl(context, subId, c, looper);
}
@Override
public PublishController createPublishController(Context context, int subId,
UceControllerCallback c, Looper looper) {
return new PublishControllerImpl(context, subId, c, looper);
}
@Override
public SubscribeController createSubscribeController(Context context, int subId) {
return new SubscribeControllerImpl(context, subId);
}
@Override
public OptionsController createOptionsController(Context context, int subId) {
return new OptionsControllerImpl(context, subId);
}
};
private final int mSubId;
private final Context mContext;
private final LocalLog mLocalLog = new LocalLog(UceUtils.LOG_SIZE);
private volatile boolean mIsRcsConnected;
private volatile boolean mIsDestroyedFlag;
private Looper mLooper;
private RcsFeatureManager mRcsFeatureManager;
private EabController mEabController;
private PublishController mPublishController;
private SubscribeController mSubscribeController;
private OptionsController mOptionsController;
private UceRequestManager mRequestManager;
// The server state for UCE requests.
private final ServerState mServerState;
public UceController(Context context, int subId) {
mSubId = subId;
mContext = context;
mServerState = new ServerState();
logi("create");
initLooper();
initControllers();
initRequestManager();
}
@VisibleForTesting
public UceController(Context context, int subId, ServerState serverState,
ControllerFactory controllerFactory, RequestManagerFactory requestManagerFactory) {
mSubId = subId;
mContext = context;
mServerState = serverState;
mControllerFactory = controllerFactory;
mRequestManagerFactory = requestManagerFactory;
initLooper();
initControllers();
initRequestManager();
}
private void initLooper() {
// Init the looper, it will be passed to each controller.
HandlerThread handlerThread = new HandlerThread("UceControllerHandlerThread");
handlerThread.start();
mLooper = handlerThread.getLooper();
}
private void initControllers() {
mEabController = mControllerFactory.createEabController(mContext, mSubId, mCtrlCallback,
mLooper);
mPublishController = mControllerFactory.createPublishController(mContext, mSubId,
mCtrlCallback, mLooper);
mSubscribeController = mControllerFactory.createSubscribeController(mContext, mSubId);
mOptionsController = mControllerFactory.createOptionsController(mContext, mSubId);
}
private void initRequestManager() {
mRequestManager = mRequestManagerFactory.createRequestManager(mContext, mSubId, mLooper,
mCtrlCallback);
mRequestManager.setSubscribeController(mSubscribeController);
mRequestManager.setOptionsController(mOptionsController);
}
/**
* The RcsFeature has been connected to the framework. This method runs on main thread.
*/
public void onRcsConnected(RcsFeatureManager manager) {
logi("onRcsConnected");
mIsRcsConnected = true;
// Notify each controllers that RCS is connected.
mEabController.onRcsConnected(manager);
mPublishController.onRcsConnected(manager);
mSubscribeController.onRcsConnected(manager);
mOptionsController.onRcsConnected(manager);
// Listen to the capability exchange event which is triggered by the ImsService
mRcsFeatureManager = manager;
mRcsFeatureManager.addCapabilityEventCallback(mCapabilityEventListener);
}
/**
* The framework has lost the binding to the RcsFeature. This method runs on main thread.
*/
public void onRcsDisconnected() {
logi("onRcsDisconnected");
mIsRcsConnected = false;
// Remove the listener because RCS is disconnected.
if (mRcsFeatureManager != null) {
mRcsFeatureManager.removeCapabilityEventCallback(mCapabilityEventListener);
mRcsFeatureManager = null;
}
// Notify each controllers that RCS is disconnected.
mEabController.onRcsDisconnected();
mPublishController.onRcsDisconnected();
mSubscribeController.onRcsDisconnected();
mOptionsController.onRcsDisconnected();
}
/**
* Notify to destroy this instance. This instance is unusable after destroyed.
*/
public void onDestroy() {
logi("onDestroy");
mIsDestroyedFlag = true;
// Remove the listener because the UceController instance is destroyed.
if (mRcsFeatureManager != null) {
mRcsFeatureManager.removeCapabilityEventCallback(mCapabilityEventListener);
mRcsFeatureManager = null;
}
// Destroy all the controllers
mRequestManager.onDestroy();
mEabController.onDestroy();
mPublishController.onDestroy();
mSubscribeController.onDestroy();
mOptionsController.onDestroy();
mLooper.quit();
}
/**
* Notify all associated classes that the carrier configuration has changed for the subId.
*/
public void onCarrierConfigChanged() {
mEabController.onCarrierConfigChanged();
mPublishController.onCarrierConfigChanged();
mSubscribeController.onCarrierConfigChanged();
mOptionsController.onCarrierConfigChanged();
}
/*
* The implementation of the interface UceControllerCallback. These methods are called by other
* controllers.
*/
private UceControllerCallback mCtrlCallback = new UceControllerCallback() {
@Override
public List<EabCapabilityResult> getCapabilitiesFromCache(List<Uri> uris) {
return mEabController.getCapabilities(uris);
}
@Override
public EabCapabilityResult getAvailabilityFromCache(Uri contactUri) {
return mEabController.getAvailability(contactUri);
}
@Override
public void saveCapabilities(List<RcsContactUceCapability> contactCapabilities) {
mEabController.saveCapabilities(contactCapabilities);
}
@Override
public RcsContactUceCapability getDeviceCapabilities(@CapabilityMechanism int mechanism) {
return mPublishController.getDeviceCapabilities(mechanism);
}
@Override
public void updateRequestForbidden(boolean isForbidden, @Nullable Integer errorCode,
long retryAfterMillis) {
mServerState.updateRequestForbidden(isForbidden, errorCode, retryAfterMillis);
}
@Override
public long getRetryAfterMillis() {
return mServerState.getRetryAfterMillis();
}
@Override
public boolean isRequestForbiddenByNetwork() {
return (mServerState.getForbiddenErrorCode() != null) ? true : false;
}
@Override
public void refreshCapabilities(@NonNull List<Uri> contactNumbers,
@NonNull IRcsUceControllerCallback callback) throws RemoteException{
logd("refreshCapabilities: " + contactNumbers.size());
UceController.this.requestCapabilitiesInternal(contactNumbers, true, callback);
}
};
@VisibleForTesting
public void setUceControllerCallback(UceControllerCallback callback) {
mCtrlCallback = callback;
}
/*
* Setup the listener to listen to the requests and updates from ImsService.
*/
private RcsFeatureManager.CapabilityExchangeEventCallback mCapabilityEventListener =
new RcsFeatureManager.CapabilityExchangeEventCallback() {
@Override
public void onRequestPublishCapabilities(
@StackPublishTriggerType int publishTriggerType) {
onRequestPublishCapabilitiesFromService(publishTriggerType);
}
@Override
public void onUnpublish() {
UceController.this.onUnpublish();
}
@Override
public void onRemoteCapabilityRequest(Uri contactUri,
List<String> remoteCapabilities, IOptionsRequestCallback cb) {
retrieveOptionsCapabilitiesForRemote(contactUri, remoteCapabilities, cb);
}
};
/**
* Request to get the contacts' capabilities. This method will retrieve the capabilities from
* the cache If the capabilities are out of date, it will trigger another request to get the
* latest contact's capabilities from the network.
*/
public void requestCapabilities(@NonNull List<Uri> uriList,
@NonNull IRcsUceControllerCallback c) throws RemoteException {
requestCapabilitiesInternal(uriList, false, c);
}
private void requestCapabilitiesInternal(@NonNull List<Uri> uriList, boolean skipFromCache,
@NonNull IRcsUceControllerCallback c) throws RemoteException {
if (uriList == null || uriList.isEmpty() || c == null) {
logw("requestCapabilities: parameter is empty");
if (c != null) {
c.onError(RcsUceAdapter.ERROR_GENERIC_FAILURE, 0L);
}
return;
}
if (isUnavailable()) {
logw("requestCapabilities: controller is unavailable");
c.onError(RcsUceAdapter.ERROR_GENERIC_FAILURE, 0L);
return;
}
// Check if UCE requests are forbidden by the network.
if (mServerState.isRequestForbidden()) {
Integer errorCode = mServerState.getForbiddenErrorCode();
long retryAfter = mServerState.getRetryAfterMillis();
logw("requestCapabilities: The request is forbidden, errorCode=" + errorCode
+ ", retryAfter=" + retryAfter);
errorCode = (errorCode != null) ? errorCode : RcsUceAdapter.ERROR_FORBIDDEN;
c.onError(errorCode, retryAfter);
return;
}
// Trigger the capabilities request task
logd("requestCapabilities: " + uriList.size());
mRequestManager.sendCapabilityRequest(uriList, skipFromCache, c);
}
/**
* Request to get the contact's capabilities. It will check the availability cache first. If
* the capability in the availability cache is expired then it will retrieve the capability
* from the network.
*/
public void requestAvailability(@NonNull Uri uri, @NonNull IRcsUceControllerCallback c)
throws RemoteException {
if (uri == null || c == null) {
logw("requestAvailability: parameter is empty");
if (c != null) {
c.onError(RcsUceAdapter.ERROR_GENERIC_FAILURE, 0L);
}
return;
}
if (isUnavailable()) {
logw("requestAvailability: controller is unavailable");
c.onError(RcsUceAdapter.ERROR_GENERIC_FAILURE, 0L);
return;
}
// Check if UCE requests are forbidden by the network.
if (mServerState.isRequestForbidden()) {
Integer errorCode = mServerState.getForbiddenErrorCode();
long retryAfter = mServerState.getRetryAfterMillis();
logw("requestAvailability: The request is forbidden, errorCode=" + errorCode
+ ", retryAfter=" + retryAfter);
errorCode = (errorCode != null) ? errorCode : RcsUceAdapter.ERROR_FORBIDDEN;
c.onError(errorCode, retryAfter);
return;
}
// Trigger the availability request task
logd("requestAvailability");
mRequestManager.sendAvailabilityRequest(uri, c);
}
/**
* Publish the device's capabilities. This request is triggered from the ImsService.
*/
public void onRequestPublishCapabilitiesFromService(@StackPublishTriggerType int triggerType) {
logd("onRequestPublishCapabilitiesFromService: " + triggerType);
// Reset the forbidden status if the service requests to publish the device's capabilities
mServerState.updateRequestForbidden(false, null, 0L);
// Send the publish request.
mPublishController.requestPublishCapabilitiesFromService(triggerType);
}
/**
* This method is triggered by the ImsService to notify framework that the device's
* capabilities has been unpublished from the network.
*/
public void onUnpublish() {
logi("onUnpublish");
mPublishController.onUnpublish();
}
/**
* Request publish the device's capabilities. This request is from the ImsService to send the
* capabilities to the remote side.
*/
public void retrieveOptionsCapabilitiesForRemote(@NonNull Uri contactUri,
@NonNull List<String> remoteCapabilities, @NonNull IOptionsRequestCallback c) {
logi("retrieveOptionsCapabilitiesForRemote");
mRequestManager.retrieveCapabilitiesForRemote(contactUri, remoteCapabilities, c);
}
/**
* Register a {@link PublishStateCallback} to receive the published state changed.
*/
public void registerPublishStateCallback(@NonNull IRcsUcePublishStateCallback c) {
mPublishController.registerPublishStateCallback(c);
}
/**
* Removes an existing {@link PublishStateCallback}.
*/
public void unregisterPublishStateCallback(@NonNull IRcsUcePublishStateCallback c) {
mPublishController.unregisterPublishStateCallback(c);
}
/**
* Get the UCE publish state if the PUBLISH is supported by the carrier.
*/
public @PublishState int getUcePublishState() {
return mPublishController.getUcePublishState();
}
/**
* Add new feature tags to the Set used to calculate the capabilities in PUBLISH.
* <p>
* Used for testing ONLY.
* @return the new capabilities that will be used for PUBLISH.
*/
public RcsContactUceCapability addRegistrationOverrideCapabilities(Set<String> featureTags) {
return mPublishController.addRegistrationOverrideCapabilities(featureTags);
}
/**
* Remove existing feature tags to the Set used to calculate the capabilities in PUBLISH.
* <p>
* Used for testing ONLY.
* @return the new capabilities that will be used for PUBLISH.
*/
public RcsContactUceCapability removeRegistrationOverrideCapabilities(Set<String> featureTags) {
return mPublishController.removeRegistrationOverrideCapabilities(featureTags);
}
/**
* Clear all overrides in the Set used to calculate the capabilities in PUBLISH.
* <p>
* Used for testing ONLY.
* @return the new capabilities that will be used for PUBLISH.
*/
public RcsContactUceCapability clearRegistrationOverrideCapabilities() {
return mPublishController.clearRegistrationOverrideCapabilities();
}
/**
* @return current RcsContactUceCapability instance that will be used for PUBLISH.
*/
public RcsContactUceCapability getLatestRcsContactUceCapability() {
return mPublishController.getLatestRcsContactUceCapability();
}
/**
* Get the PIDF XML associated with the last successful publish or null if not PUBLISHed to the
* network.
*/
public String getLastPidfXml() {
return mPublishController.getLastPidfXml();
}
/**
* Get the subscription ID.
*/
public int getSubId() {
return mSubId;
}
/**
* Check if the UceController is available.
* @return true if RCS is connected without destroyed.
*/
public boolean isUnavailable() {
if (!mIsRcsConnected || mIsDestroyedFlag) {
return true;
}
return false;
}
/**
* The internal class to store the server state which sent from the network. It will help to
* check if the network allows the UCE request.
*/
@VisibleForTesting
public static class ServerState {
private boolean mIsForbidden;
private Integer mForbiddenErrorCode;
// The timestamp when the network allows the UCE requests. This value may be null if the
// network doesn't specified any retryAfter info.
private Instant mAllowedTimestamp;
private final Object mServerStateLock = new Object();
public ServerState() {
mIsForbidden = false;
mForbiddenErrorCode = null;
mAllowedTimestamp = null;
}
public void updateRequestForbidden(boolean isForbidden, @Nullable Integer errorCode,
long retryAfterMillis) {
synchronized (mServerStateLock) {
mIsForbidden = isForbidden;
if (!mIsForbidden) {
mForbiddenErrorCode = null;
mAllowedTimestamp = null;
} else {
mForbiddenErrorCode =
(errorCode == null) ? RcsUceAdapter.ERROR_FORBIDDEN : errorCode;
mAllowedTimestamp = Instant.now().plus(retryAfterMillis, ChronoUnit.MILLIS);
}
Log.i(LOG_TAG, "updateRequestForbidden: isForbidden=" + mIsForbidden
+ ", errorCode=" + mForbiddenErrorCode + ", time=" + mAllowedTimestamp);
}
}
public boolean isRequestForbidden() {
synchronized (mServerStateLock) {
if (mIsForbidden && mAllowedTimestamp != null) {
return Instant.now().isBefore(mAllowedTimestamp);
}
return mIsForbidden;
}
}
public @Nullable Integer getForbiddenErrorCode() {
synchronized (mServerStateLock) {
if (!mIsForbidden) {
return null;
}
return mForbiddenErrorCode;
}
}
public long getRetryAfterMillis() {
synchronized (mServerStateLock) {
if (!mIsForbidden || mAllowedTimestamp == null) {
return 0L;
}
Duration duration = Duration.between(Instant.now(), mAllowedTimestamp);
long retryAfterMillis = duration.toMillis();
if (retryAfterMillis < 0) {
return 0L;
}
return retryAfterMillis;
}
}
}
public void dump(PrintWriter printWriter) {
IndentingPrintWriter pw = new IndentingPrintWriter(printWriter, " ");
pw.println("UceController" + "[subId: " + mSubId + "]:");
pw.increaseIndent();
pw.println("Log:");
pw.increaseIndent();
mLocalLog.dump(pw);
pw.decreaseIndent();
pw.println("---");
mPublishController.dump(pw);
pw.decreaseIndent();
}
private void logd(String log) {
Log.d(LOG_TAG, getLogPrefix().append(log).toString());
mLocalLog.log("[D] " + log);
}
private void logi(String log) {
Log.i(LOG_TAG, getLogPrefix().append(log).toString());
mLocalLog.log("[I] " + log);
}
private void logw(String log) {
Log.w(LOG_TAG, getLogPrefix().append(log).toString());
mLocalLog.log("[W] " + log);
}
private StringBuilder getLogPrefix() {
StringBuilder builder = new StringBuilder("[");
builder.append(mSubId);
builder.append("] ");
return builder;
}
}