/* * 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.telephony.ims.SipDetails; import android.telephony.ims.stub.ImsRegistrationImplBase; import android.telephony.ims.stub.RcsCapabilityExchangeImplBase; 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.UceStatsWriter; 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; private final UceStatsWriter mUceStatsWriter; // 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); mUceStatsWriter = UceStatsWriter.getInstance(); } @VisibleForTesting public PublishProcessor(Context context, int subId, DeviceCapabilityInfo capabilityInfo, PublishControllerCallback publishCtrlCallback, UceStatsWriter instance) { mSubId = subId; mContext = context; mDeviceCapabilities = capabilityInfo; mPublishCtrlCallback = publishCtrlCallback; mProcessorState = new PublishProcessorState(subId); mUceStatsWriter = instance; } /** * 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(); // reset the publish capabilities. mDeviceCapabilities.resetPresenceCapability(); } /** * 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; if (triggerType == PublishController.PUBLISH_TRIGGER_SERVICE) { deviceCapability = mDeviceCapabilities.getDeviceCapabilities( CAPABILITY_MECHANISM_PRESENCE, mContext); } else { deviceCapability = mDeviceCapabilities.getChangedPresenceCapability(mContext); } if (deviceCapability == null) { logi("doPublishInternal: device capability hasn't changed or 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; } featureManager.getImsRegistrationTech((tech) -> { int registrationTech = (tech == null) ? ImsRegistrationImplBase.REGISTRATION_TECH_NONE : tech; mUceStatsWriter.setImsRegistrationServiceDescStats(mSubId, deviceCapability.getCapabilityTuples(), registrationTech); }); // 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 (!isEabProvisioned()) { 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); // Inform that the publish request has been sent to the Ims Service. mPublishCtrlCallback.notifyPendingPublishRequest(); 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()); int cmdError = requestResponse.getCmdErrorCode().orElse(0); boolean successful = false; if (cmdError == RcsCapabilityExchangeImplBase.COMMAND_CODE_NO_CHANGE) { successful = true; } mUceStatsWriter.setUceEvent(mSubId, UceStatsWriter.PUBLISH_EVENT, successful, cmdError, 0); 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()); int responseCode = requestResponse.getNetworkRespSipCode().orElse(0); mUceStatsWriter.setUceEvent(mSubId, UceStatsWriter.PUBLISH_EVENT, true, 0, responseCode); 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 last capabilities because of the request is failed mDeviceCapabilities.setPresencePublishResult(false); // 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) { updatePublishStateFromResponse(requestResponse); // Finish the request and check if there is pending request. setRequestEnded(requestResponse); checkAndSendPendingRequest(); } // After checking the response, it handles calling PublishCtrlCallback. private void updatePublishStateFromResponse(PublishRequestResponse response) { Instant responseTime = response.getResponseTimestamp(); // Record the time when the request is successful and reset the retry count. boolean publishSuccess = false; if (response.isRequestSuccess()) { mProcessorState.setLastPublishedTime(responseTime); mProcessorState.resetRetryCount(); publishSuccess = true; } // set the last capabilities according to the result of request. mDeviceCapabilities.setPresencePublishResult(publishSuccess); // Update the publish state after the request has finished. int publishState = response.getPublishState(); String pidfXml = response.getPidfXml(); SipDetails details = response.getSipDetails().orElse(null); mPublishCtrlCallback.updatePublishRequestResult(publishState, responseTime, pidfXml, details); // Refresh the device state with the publish request result. response.getResponseSipCode().ifPresent(sipCode -> { String reason = response.getResponseReason().orElse(""); mPublishCtrlCallback.refreshDeviceState(sipCode, reason); }); } /** * 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 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(); } /** * Reset the retry count and time related publish. */ public void resetState() { mProcessorState.resetState(); // reset the publish capabilities. mDeviceCapabilities.resetPresenceCapability(); } /** * Update publish status after handling on onPublishUpdate case */ public void publishUpdated(PublishRequestResponse response) { updatePublishStateFromResponse(response); if (response != null) { response.onDestroy(); } } @VisibleForTesting public void setProcessorState(PublishProcessorState processorState) { mProcessorState = processorState; } @VisibleForTesting protected boolean isEabProvisioned() { return UceUtils.isEabProvisioned(mContext, mSubId); } 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(); } }