blob: 68aeaa8fc5cd2d9874f57a36a312a45a8fd421da [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.presence.publish;
import static android.telephony.ims.RcsContactUceCapability.CAPABILITY_MECHANISM_PRESENCE;
import android.annotation.NonNull;
import android.content.Context;
import android.os.RemoteException;
import android.telephony.ims.RcsContactUceCapability;
import android.text.TextUtils;
import android.util.IndentingPrintWriter;
import android.util.LocalLog;
import android.util.Log;
import com.android.ims.RcsFeatureManager;
import com.android.ims.rcs.uce.presence.pidfparser.PidfParser;
import com.android.ims.rcs.uce.presence.publish.PublishController.PublishControllerCallback;
import com.android.ims.rcs.uce.presence.publish.PublishController.PublishTriggerType;
import com.android.ims.rcs.uce.util.UceUtils;
import com.android.internal.annotations.VisibleForTesting;
import java.io.PrintWriter;
import java.time.Instant;
import java.util.Optional;
/**
* Send the publish request and handle the response of the publish request result.
*/
public class PublishProcessor {
private static final String LOG_TAG = UceUtils.getLogPrefix() + "PublishProcessor";
// The length of time waiting for the response callback.
private static final long RESPONSE_CALLBACK_WAITING_TIME = 60000L;
private final int mSubId;
private final Context mContext;
private volatile boolean mIsDestroyed;
private volatile RcsFeatureManager mRcsFeatureManager;
// Manage the state of the publish processor.
private PublishProcessorState mProcessorState;
// The information of the device's capabilities.
private final DeviceCapabilityInfo mDeviceCapabilities;
// The callback of the PublishController
private final PublishControllerCallback mPublishCtrlCallback;
// The lock of processing the pending request.
private final Object mPendingRequestLock = new Object();
private final LocalLog mLocalLog = new LocalLog(UceUtils.LOG_SIZE);
public PublishProcessor(Context context, int subId, DeviceCapabilityInfo capabilityInfo,
PublishControllerCallback publishCtrlCallback) {
mSubId = subId;
mContext = context;
mDeviceCapabilities = capabilityInfo;
mPublishCtrlCallback = publishCtrlCallback;
mProcessorState = new PublishProcessorState(subId);
}
/**
* The RcsFeature has been connected to the framework.
*/
public void onRcsConnected(RcsFeatureManager featureManager) {
mLocalLog.log("onRcsConnected");
logi("onRcsConnected");
mRcsFeatureManager = featureManager;
// Check if there is a pending request.
checkAndSendPendingRequest();
}
/**
* The framework has lost the binding to the RcsFeature.
*/
public void onRcsDisconnected() {
mLocalLog.log("onRcsDisconnected");
logi("onRcsDisconnected");
mRcsFeatureManager = null;
mProcessorState.onRcsDisconnected();
}
/**
* Set the destroy flag
*/
public void onDestroy() {
mLocalLog.log("onDestroy");
logi("onDestroy");
mIsDestroyed = true;
}
/**
* Execute the publish request. This method is called by the handler of the PublishController.
* @param triggerType The type of triggering the publish request.
*/
public void doPublish(@PublishTriggerType int triggerType) {
mProcessorState.setPublishingFlag(true);
if (!doPublishInternal(triggerType)) {
// Reset the publishing flag if the request cannot be sent to the IMS service.
mProcessorState.setPublishingFlag(false);
}
}
/**
* Execute the publish request internally.
* @param triggerType The type of triggering the publish request.
* @return true if the publish is sent to the IMS service successfully, false otherwise.
*/
private boolean doPublishInternal(@PublishTriggerType int triggerType) {
if (mIsDestroyed) return false;
mLocalLog.log("doPublishInternal: trigger type=" + triggerType);
logi("doPublishInternal: trigger type=" + triggerType);
// Return if this request is not allowed to be executed.
if (!isRequestAllowed(triggerType)) {
mLocalLog.log("doPublishInternal: The request is not allowed.");
return false;
}
// Get the latest device's capabilities.
RcsContactUceCapability deviceCapability =
mDeviceCapabilities.getDeviceCapabilities(CAPABILITY_MECHANISM_PRESENCE, mContext);
if (deviceCapability == null) {
logw("doPublishInternal: device capability is null");
return false;
}
// Convert the device's capabilities to pidf format.
String pidfXml = PidfParser.convertToPidf(deviceCapability);
if (TextUtils.isEmpty(pidfXml)) {
logw("doPublishInternal: pidfXml is empty");
return false;
}
// Set the pending request and return if RCS is not connected. When the RCS is connected
// afterward, it will send a new request if there's a pending request.
RcsFeatureManager featureManager = mRcsFeatureManager;
if (featureManager == null) {
logw("doPublishInternal: RCS is not connected.");
setPendingRequest(triggerType);
return false;
}
// Publish to the Presence server.
return publishCapabilities(featureManager, pidfXml);
}
/*
* According to the given trigger type, check whether the request is allowed to be executed or
* not.
*/
private boolean isRequestAllowed(@PublishTriggerType int triggerType) {
// Check if the instance is destroyed.
if (mIsDestroyed) {
logd("isPublishAllowed: This instance is already destroyed");
return false;
}
// Check if it has provisioned. When the provisioning changes, a new publish request will
// be triggered.
if (!UceUtils.isEabProvisioned(mContext, mSubId)) {
logd("isPublishAllowed: NOT provisioned");
return false;
}
// Do not request publish if the IMS is not registered. When the IMS is registered
// afterward, a new publish request will be triggered.
if (!mDeviceCapabilities.isImsRegistered()) {
logd("isPublishAllowed: IMS is not registered");
return false;
}
// Skip this request if the PUBLISH is not allowed at current time. Resend the PUBLISH
// request and it will be triggered with an appropriate delay time.
if (!mProcessorState.isPublishAllowedAtThisTime()) {
logd("isPublishAllowed: Current time is not allowed, resend this request");
mPublishCtrlCallback.requestPublishFromInternal(triggerType);
return false;
}
return true;
}
// Publish the device capabilities with the given pidf.
private boolean publishCapabilities(@NonNull RcsFeatureManager featureManager,
@NonNull String pidfXml) {
PublishRequestResponse requestResponse = null;
try {
// Clear the pending flag because it is going to send the latest device's capabilities.
clearPendingRequest();
// Generate a unique taskId to track this request.
long taskId = mProcessorState.generatePublishTaskId();
requestResponse = new PublishRequestResponse(mPublishCtrlCallback, taskId, pidfXml);
mLocalLog.log("publish capabilities: taskId=" + taskId);
logi("publishCapabilities: taskId=" + taskId);
// request publication
featureManager.requestPublication(pidfXml, requestResponse.getResponseCallback());
// Send a request canceled timer to avoid waiting too long for the response callback.
mPublishCtrlCallback.setupRequestCanceledTimer(taskId, RESPONSE_CALLBACK_WAITING_TIME);
return true;
} catch (RemoteException e) {
mLocalLog.log("publish capability exception: " + e.getMessage());
logw("publishCapabilities: exception=" + e.getMessage());
// Exception occurred, end this request.
setRequestEnded(requestResponse);
checkAndSendPendingRequest();
return false;
}
}
/**
* Handle the command error callback of the publish request. This method is called by the
* handler of the PublishController.
*/
public void onCommandError(PublishRequestResponse requestResponse) {
if (!checkRequestRespValid(requestResponse)) {
mLocalLog.log("Command error callback is invalid");
logw("onCommandError: request response is invalid");
setRequestEnded(requestResponse);
checkAndSendPendingRequest();
return;
}
mLocalLog.log("Receive command error code=" + requestResponse.getCmdErrorCode());
logd("onCommandError: " + requestResponse.toString());
if (requestResponse.needRetry() && !mProcessorState.isReachMaximumRetries()) {
handleRequestRespWithRetry(requestResponse);
} else {
handleRequestRespWithoutRetry(requestResponse);
}
}
/**
* Handle the network response callback of the publish request. This method is called by the
* handler of the PublishController.
*/
public void onNetworkResponse(PublishRequestResponse requestResponse) {
if (!checkRequestRespValid(requestResponse)) {
mLocalLog.log("Network response callback is invalid");
logw("onNetworkResponse: request response is invalid");
setRequestEnded(requestResponse);
checkAndSendPendingRequest();
return;
}
mLocalLog.log("Receive network response code=" + requestResponse.getNetworkRespSipCode());
logd("onNetworkResponse: " + requestResponse.toString());
if (requestResponse.needRetry() && !mProcessorState.isReachMaximumRetries()) {
handleRequestRespWithRetry(requestResponse);
} else {
handleRequestRespWithoutRetry(requestResponse);
}
}
// Check if the request response callback is valid.
private boolean checkRequestRespValid(PublishRequestResponse requestResponse) {
if (requestResponse == null) {
logd("checkRequestRespValid: request response is null");
return false;
}
if (!mProcessorState.isPublishingNow()) {
logd("checkRequestRespValid: the request is finished");
return false;
}
// Abandon this response callback if the current taskId is different to the response
// callback taskId. This response callback is obsoleted.
long taskId = mProcessorState.getCurrentTaskId();
long responseTaskId = requestResponse.getTaskId();
if (taskId != responseTaskId) {
logd("checkRequestRespValid: invalid taskId! current taskId=" + taskId
+ ", response callback taskId=" + responseTaskId);
return false;
}
if (mIsDestroyed) {
logd("checkRequestRespValid: is already destroyed! taskId=" + taskId);
return false;
}
return true;
}
/*
* Handle the publishing request with retry. This method is called when it receives a failed
* request response and need to retry.
*/
private void handleRequestRespWithRetry(PublishRequestResponse requestResponse) {
// Increase the retry count
mProcessorState.increaseRetryCount();
// Reset the pending flag because it is going to resend a request.
clearPendingRequest();
// Finish this request and resend a new publish request
setRequestEnded(requestResponse);
mPublishCtrlCallback.requestPublishFromInternal(PublishController.PUBLISH_TRIGGER_RETRY);
}
/*
* Handle the publishing request without retry. This method is called when it receives the
* request response and it does not need to retry.
*/
private void handleRequestRespWithoutRetry(PublishRequestResponse requestResponse) {
Instant responseTime = requestResponse.getResponseTimestamp();
// Record the time when the request is successful and reset the retry count.
if (requestResponse.isRequestSuccess()) {
mProcessorState.setLastPublishedTime(responseTime);
mProcessorState.resetRetryCount();
}
// Update the publish state after the request has finished.
int publishState = requestResponse.getPublishState();
String pidfXml = requestResponse.getPidfXml();
mPublishCtrlCallback.updatePublishRequestResult(publishState, responseTime, pidfXml);
// Refresh the device state with the publish request result.
requestResponse.getResponseSipCode().ifPresent(sipCode -> {
String reason = requestResponse.getResponseReason().orElse("");
mPublishCtrlCallback.refreshDeviceState(sipCode, reason);
});
// Finish the request and check if there is pending request.
setRequestEnded(requestResponse);
checkAndSendPendingRequest();
}
/**
* Cancel the publishing request since it has token too long for waiting the response callback.
* This method is called by the handler of the PublishController.
*/
public void cancelPublishRequest(long taskId) {
mLocalLog.log("cancel publish request: taskId=" + taskId);
logd("cancelPublishRequest: taskId=" + taskId);
setRequestEnded(null);
checkAndSendPendingRequest();
}
/*
* Finish the publishing request. This method is required to be called before the publishing
* request is finished.
*/
private void setRequestEnded(PublishRequestResponse requestResponse) {
long taskId = -1L;
if (requestResponse != null) {
requestResponse.onDestroy();
taskId = requestResponse.getTaskId();
}
mProcessorState.setPublishingFlag(false);
mPublishCtrlCallback.clearRequestCanceledTimer();
mLocalLog.log("Set request ended: taskId=" + taskId);
logd("setRequestEnded: taskId=" + taskId);
}
/*
* Set the pending flag when it cannot be executed now.
*/
public void setPendingRequest(@PublishTriggerType int triggerType) {
synchronized (mPendingRequestLock) {
mProcessorState.setPendingRequest(triggerType);
}
}
/**
* Check and trigger a new publish request if there is a pending request.
*/
public void checkAndSendPendingRequest() {
synchronized (mPendingRequestLock) {
if (mIsDestroyed) return;
if (mProcessorState.hasPendingRequest()) {
// Retrieve the trigger type of the pending request
int type = mProcessorState.getPendingRequestTriggerType()
.orElse(PublishController.PUBLISH_TRIGGER_RETRY);
logd("checkAndSendPendingRequest: send pending request, type=" + type);
// Clear the pending flag because it is going to send a PUBLISH request.
mProcessorState.clearPendingRequest();
mPublishCtrlCallback.requestPublishFromInternal(type);
}
}
}
/**
* Clear the pending request. It means that the publish request is triggered and this flag can
* be removed.
*/
private void clearPendingRequest() {
synchronized (mPendingRequestLock) {
mProcessorState.clearPendingRequest();
}
}
/**
* Update the publishing allowed time with the given trigger type. This method wil be called
* before adding a PUBLISH request to the handler.
* @param triggerType The trigger type of this PUBLISH request
*/
public void updatePublishingAllowedTime(@PublishTriggerType int triggerType) {
mProcessorState.updatePublishingAllowedTime(triggerType);
}
/**
* @return The delay time to allow to execute the PUBLISH request. This method will be called
* to determine the delay time before adding a PUBLISH request to the handler.
*/
public Optional<Long> getPublishingDelayTime() {
return mProcessorState.getPublishingDelayTime();
}
/**
* Update the publish throttle.
*/
public void updatePublishThrottle(int publishThrottle) {
mProcessorState.updatePublishThrottle(publishThrottle);
}
/**
* @return true if the publish request is running now.
*/
public boolean isPublishingNow() {
return mProcessorState.isPublishingNow();
}
@VisibleForTesting
public void setProcessorState(PublishProcessorState processorState) {
mProcessorState = processorState;
}
private void logd(String log) {
Log.d(LOG_TAG, getLogPrefix().append(log).toString());
}
private void logi(String log) {
Log.i(LOG_TAG, getLogPrefix().append(log).toString());
}
private void logw(String log) {
Log.w(LOG_TAG, getLogPrefix().append(log).toString());
}
private StringBuilder getLogPrefix() {
StringBuilder builder = new StringBuilder("[");
builder.append(mSubId);
builder.append("] ");
return builder;
}
public void dump(PrintWriter printWriter) {
IndentingPrintWriter pw = new IndentingPrintWriter(printWriter, " ");
pw.println("PublishProcessor" + "[subId: " + mSubId + "]:");
pw.increaseIndent();
pw.print("ProcessorState: isPublishing=");
pw.print(mProcessorState.isPublishingNow());
pw.print(", hasReachedMaxRetries=");
pw.print(mProcessorState.isReachMaximumRetries());
pw.print(", delayTimeToAllowPublish=");
pw.println(mProcessorState.getPublishingDelayTime().orElse(-1L));
pw.println("Log:");
pw.increaseIndent();
mLocalLog.dump(pw);
pw.decreaseIndent();
pw.println("---");
pw.decreaseIndent();
}
}