diff options
author | James.cf Lin <jamescflin@google.com> | 2020-10-26 16:03:34 +0800 |
---|---|---|
committer | James.cf Lin <jamescflin@google.com> | 2020-10-28 22:20:38 +0800 |
commit | 25195604ac1852c8dff0ca22f1c9000b7f1f6477 (patch) | |
tree | 97a04991749de45714046e4f789f2d62b6758bdb | |
parent | e482d4563d2b9c8aae6cb4226214b88df34c5595 (diff) | |
download | ims-25195604ac1852c8dff0ca22f1c9000b7f1f6477.tar.gz |
[RCS UCE] Implement the device capabilities publishing.
Bug: 170476615
Test: atest
Change-Id: I87c4ed63a96aec6e64c473c7954257fb68b58589
12 files changed, 1134 insertions, 10 deletions
diff --git a/src/java/com/android/ims/RcsFeatureConnection.java b/src/java/com/android/ims/RcsFeatureConnection.java index 546a4b13..1b14c266 100644 --- a/src/java/com/android/ims/RcsFeatureConnection.java +++ b/src/java/com/android/ims/RcsFeatureConnection.java @@ -32,8 +32,10 @@ import android.telephony.ims.aidl.IRcsFeatureListener; import android.telephony.ims.feature.CapabilityChangeRequest; import android.telephony.ims.feature.ImsFeature; +import com.android.ims.rcs.uce.RcsCapabilityExchangeImplAdapter.PublishResponseCallback; import com.android.internal.annotations.VisibleForTesting; import com.android.telephony.Rlog; + import java.util.List; /** @@ -222,6 +224,11 @@ public class RcsFeatureConnection extends FeatureConnection { } } + public void requestPublication(String pidfXml, PublishResponseCallback responseCallback) + throws RemoteException { + // TODO: add the new API: requestPublication + } + public void requestCapabilities(List<Uri> uris, int taskId) throws RemoteException { synchronized (mLock) { checkServiceIsReady(); diff --git a/src/java/com/android/ims/RcsFeatureManager.java b/src/java/com/android/ims/RcsFeatureManager.java index 5879f4ad..a07b6526 100644 --- a/src/java/com/android/ims/RcsFeatureManager.java +++ b/src/java/com/android/ims/RcsFeatureManager.java @@ -47,6 +47,7 @@ import android.telephony.ims.stub.RcsSipOptionsImplBase; import android.util.Log; import com.android.ims.internal.IImsServiceFeatureCallback; +import com.android.ims.rcs.uce.RcsCapabilityExchangeImplAdapter.PublishResponseCallback; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.telephony.ITelephony; import com.android.telephony.Rlog; @@ -430,6 +431,11 @@ public class RcsFeatureManager implements FeatureUpdates { mRcsFeatureConnection.requestPublication(capabilities, taskId); } + public void requestPublication(String pidfXml, PublishResponseCallback responseCallback) + throws RemoteException { + mRcsFeatureConnection.requestPublication(pidfXml, responseCallback); + } + public void requestCapabilities(List<Uri> uris, int taskId) throws RemoteException { mRcsFeatureConnection.requestCapabilities(uris, taskId); } diff --git a/src/java/com/android/ims/rcs/uce/presence/publish/PublishController.java b/src/java/com/android/ims/rcs/uce/presence/publish/PublishController.java index 92781218..75c76281 100644 --- a/src/java/com/android/ims/rcs/uce/presence/publish/PublishController.java +++ b/src/java/com/android/ims/rcs/uce/presence/publish/PublishController.java @@ -95,6 +95,27 @@ public interface PublishController extends ControllerBase { void requestPublishFromInternal(@PublishTriggerType int type, long delay); /** + * Receive the command error callback of the request from ImsService. + */ + void onRequestCommandError(PublishRequestResponse requestResponse); + + /** + * Receive the network response callback fo the request from ImsService. + */ + void onRequestNetworkResp(PublishRequestResponse requestResponse); + + /** + * Set the timer to cancel the request. This timer is to prevent taking too long for + * waiting the response callback. + */ + void setupRequestCanceledTimer(long taskId, long delay); + + /** + * Clear the request canceled timer. This api will be called if the request is finished. + */ + void clearRequestCanceledTimer(); + + /** * Update the publish request result. */ void updatePublishRequestResult(int publishState); diff --git a/src/java/com/android/ims/rcs/uce/presence/publish/PublishControllerImpl.java b/src/java/com/android/ims/rcs/uce/presence/publish/PublishControllerImpl.java index df7621f9..f185417e 100644 --- a/src/java/com/android/ims/rcs/uce/presence/publish/PublishControllerImpl.java +++ b/src/java/com/android/ims/rcs/uce/presence/publish/PublishControllerImpl.java @@ -213,6 +213,30 @@ public class PublishControllerImpl implements PublishController { } @Override + public void onRequestCommandError(PublishRequestResponse requestResponse) { + logd("onRequestCommandError: taskId=" + requestResponse.getTaskId()); + mPublishHandler.onRequestCommandError(requestResponse); + } + + @Override + public void onRequestNetworkResp(PublishRequestResponse requestResponse) { + logd("onRequestNetworkResp: taskId=" + requestResponse.getTaskId()); + mPublishHandler.onRequestNetworkResponse(requestResponse); + } + + @Override + public void setupRequestCanceledTimer(long taskId, long delay) { + logd("setupRequestCanceledTimer: taskId=" + taskId + ", delay=" + delay); + mPublishHandler.setRequestCanceledTimer(taskId, delay); + } + + @Override + public void clearRequestCanceledTimer() { + logd("clearRequestCanceledTimer"); + mPublishHandler.clearRequestCanceledTimer(); + } + + @Override public void updatePublishRequestResult(@PublishState int publishState) { logd("updatePublishRequestResult: " + publishState); mPublishHandler.onPublishStateChanged(publishState); @@ -231,6 +255,9 @@ public class PublishControllerImpl implements PublishController { private class PublishHandler extends Handler { private static final int MSG_PUBLISH_STATE_CHANGED = 1; private static final int MSG_REQUEST_PUBLISH = 2; + private static final int MSG_REQUEST_CMD_ERROR = 3; + private static final int MSG_REQUEST_SIP_RESPONSE = 4; + private static final int MSG_REQUEST_CANCELED = 5; public PublishHandler(Looper looper) { super(looper); @@ -250,6 +277,21 @@ public class PublishControllerImpl implements PublishController { int type = (Integer) message.obj; handleRequestPublishMessage(type); break; + + case MSG_REQUEST_CMD_ERROR: + PublishRequestResponse cmdErrorResponse = (PublishRequestResponse) message.obj; + mPublishProcessor.onCommandError(cmdErrorResponse); + break; + + case MSG_REQUEST_SIP_RESPONSE: + PublishRequestResponse networkResponse = (PublishRequestResponse) message.obj; + mPublishProcessor.onNetworkResponse(networkResponse); + break; + + case MSG_REQUEST_CANCELED: + long taskId = (Long) message.obj; + handleRequestCanceledMessage(taskId); + break; } } @@ -297,6 +339,39 @@ public class PublishControllerImpl implements PublishController { sendMessage(message); } } + + public void onRequestCommandError(PublishRequestResponse requestResponse) { + if (mIsDestroyedFlag) return; + + Message message = obtainMessage(); + message.what = MSG_REQUEST_CMD_ERROR; + message.obj = requestResponse; + sendMessage(message); + } + + public void onRequestNetworkResponse(PublishRequestResponse requestResponse) { + if (mIsDestroyedFlag) return; + + Message message = obtainMessage(); + message.what = MSG_REQUEST_SIP_RESPONSE; + message.obj = requestResponse; + sendMessage(message); + } + + public void setRequestCanceledTimer(long taskId, long delay) { + if (mIsDestroyedFlag) return; + removeMessages(MSG_REQUEST_CANCELED, (Long) taskId); + + Message message = obtainMessage(); + message.what = MSG_REQUEST_CANCELED; + message.obj = (Long) taskId; + sendMessageDelayed(message, delay); + } + + public void clearRequestCanceledTimer() { + if (mIsDestroyedFlag) return; + removeMessages(MSG_REQUEST_CANCELED); + } } /** @@ -325,10 +400,14 @@ public class PublishControllerImpl implements PublishController { public void handleRequestPublishMessage(@PublishTriggerType int type) { if (mIsDestroyedFlag) return; - logd("handleRequestPublishMessage"); mPublishProcessor.doPublish(type); } + public void handleRequestCanceledMessage(long taskId) { + if (mIsDestroyedFlag) return; + mPublishProcessor.cancelPublishRequest(taskId); + } + @VisibleForTesting public void setPublishStateCallback(RemoteCallbackList<IRcsUcePublishStateCallback> list) { mPublishStateCallbacks = list; diff --git a/src/java/com/android/ims/rcs/uce/presence/publish/PublishProcessor.java b/src/java/com/android/ims/rcs/uce/presence/publish/PublishProcessor.java index 76231254..64a31394 100644 --- a/src/java/com/android/ims/rcs/uce/presence/publish/PublishProcessor.java +++ b/src/java/com/android/ims/rcs/uce/presence/publish/PublishProcessor.java @@ -17,27 +17,81 @@ package com.android.ims.rcs.uce.presence.publish; import android.content.Context; +import android.os.RemoteException; +import android.telephony.ims.RcsContactUceCapability; +import android.text.TextUtils; +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; + +/** + * Send the publish request and handle the response of the publish request result. + */ public class PublishProcessor { + private static final String LOG_TAG = "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 DeviceCapabilityInfo mDeviceCapabilities; + + // The callback of the PublishController + private PublishControllerCallback mPublishCtrlCallback; + + private final LocalLog mLocalLog = new LocalLog(20); + public PublishProcessor(Context context, int subId, DeviceCapabilityInfo capabilityInfo, - PublishControllerCallback callback) { + PublishControllerCallback publishCtrlCallback) { + mSubId = subId; + mContext = context; + mDeviceCapabilities = capabilityInfo; + mPublishCtrlCallback = publishCtrlCallback; + mProcessorState = new PublishProcessorState(); } + /** + * The RcsFeature has been connected to the framework. + */ public void onRcsConnected(RcsFeatureManager featureManager) { - // TODO: Implement this method + mLocalLog.log("onRcsConnected"); + logi("onRcsConnected"); + mRcsFeatureManager = featureManager; } + /** + * The framework has lost the binding to the RcsFeature. + */ public void onRcsDisconnected() { - // TODO: Implement this method + mLocalLog.log("onRcsDisconnected"); + logi("onRcsDisconnected"); + mRcsFeatureManager = null; } + /** + * Set the destroy flag + */ public void onDestroy() { - // TODO: Implement this method + mLocalLog.log("onDestroy"); + logi("onDestroy"); + mIsDestroyed = true; } /** @@ -45,6 +99,316 @@ public class PublishProcessor { * @param triggerType The type of triggering the publish request. */ public void doPublish(@PublishTriggerType int triggerType) { - // TODO: Implement this method + if (mIsDestroyed) return; + + mLocalLog.log("doPublish: trigger type=" + triggerType); + logi("doPublish: trigger type=" + triggerType); + + // Check if it should reset the retry count. + if (isResetRetryNeeded(triggerType)) { + mProcessorState.resetRetryCount(); + } + + // Return if this request is not allowed to execute. + if (!isRequestAllowed()) { + mLocalLog.log("doPublish: The request is not allowed"); + return; + } + + // Get the latest device's capabilities. + RcsContactUceCapability deviceCapability = + mDeviceCapabilities.getDeviceCapabilities(mContext); + if (deviceCapability == null) { + logw("doPublish: device capability is null"); + return; + } + + // Convert the device's capabilities to pidf format. + String pidfXml = PidfParser.convertToPidf(deviceCapability); + if (TextUtils.isEmpty(pidfXml)) { + logw("doPublish: pidfXml is empty"); + return; + } + + // Publish to the Presence server. + publishCapabilities(pidfXml); + } + + // Check if the giving trigger type should reset the retry count. + private boolean isResetRetryNeeded(@PublishTriggerType int triggerType) { + // Do no reset the retry count if the request is triggered by the previous failed retry. + if (triggerType == PublishController.PUBLISH_TRIGGER_RETRY) { + return false; + } + return true; + } + + // Check if the publish request is allowed to execute. + private boolean isRequestAllowed() { + // 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; + } + + // Set the pending flag if there's already a request running now. + if (mProcessorState.isPublishingNow()) { + logd("isPublishAllowed: There is already a request running now"); + mProcessorState.setPendingRequest(true); + return false; + } + + // Check if the publishing request has reached the maximum number of retries. + if (mProcessorState.isReachMaximumRetries()) { + logd("isPublishAllowed: It has reached maximum number of retries"); + return false; + } + + // Skip this request and re-send the request with the delay time if the publish request + // executes too frequently. + if (!mProcessorState.isCurrentTimeAllowed()) { + logd("isPublishAllowed: Current time is not allowed, resend this request"); + long delayTime = mProcessorState.getDelayTimeToAllowPublish(); + mPublishCtrlCallback.requestPublishFromInternal( + PublishController.PUBLISH_TRIGGER_RETRY, delayTime); + return false; + } + return true; + } + + // Publish the device capabilities with the given pidf + private void publishCapabilities(String pidfXml) { + if (mIsDestroyed) { + logw("publishCapabilities: This instance is already destroyed"); + return; + } + + // Check if the RCS is connected. + RcsFeatureManager featureManager = mRcsFeatureManager; + if (featureManager == null) { + mLocalLog.log("publish capabilities: not connected"); + logw("publishCapabilities: NOT connected"); + return; + } + + PublishRequestResponse requestResponse = null; + try { + // Set publishing flag + mProcessorState.setPublishingFlag(true); + + // Clear the pending request flag since we're publishing the latest device's capability + mProcessorState.setPendingRequest(false); + + // Generate a unique taskId to track this request. + long taskId = mProcessorState.generatePublishTaskId(); + requestResponse = new PublishRequestResponse(mPublishCtrlCallback, taskId); + + mLocalLog.log("publish capabilities: taskId=" + taskId); + logi("publishCapabilities: taskId=" + taskId); + + // request publication + featureManager.requestPublication(pidfXml, requestResponse); + + // Send a request canceled timer to avoid waiting too long for the response callback. + mPublishCtrlCallback.setupRequestCanceledTimer(taskId, RESPONSE_CALLBACK_WAITING_TIME); + + } catch (RemoteException e) { + mLocalLog.log("publish capability exception: " + e.getMessage()); + logw("publishCapabilities: exception=" + e.getMessage()); + // Exception occurred, end this request. + setRequestEnded(requestResponse); + checkAndSendPendingRequest(); + } + } + + /** + * 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()) { + // Increase the retry count + mProcessorState.increaseRetryCount(); + + // Reset the pending flag since it is going to resend a publish request. + mProcessorState.setPendingRequest(false); + + // Resend a publish request + long delayTime = mProcessorState.getDelayTimeToAllowPublish(); + mPublishCtrlCallback.requestPublishFromInternal( + PublishController.PUBLISH_TRIGGER_RETRY, delayTime); + } else { + // Update the publish state if the request is failed and doesn't need to retry. + int publishState = requestResponse.getPublishStateByCmdErrorCode(); + mPublishCtrlCallback.updatePublishRequestResult(publishState); + + // Check if there is a pending request + checkAndSendPendingRequest(); + } + + // End this request + setRequestEnded(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()) { + // Increase the retry count + mProcessorState.increaseRetryCount(); + + // Reset the pending flag since it is going to resend a publish request. + mProcessorState.setPendingRequest(false); + + // Resend a publish request + long delayTime = mProcessorState.getDelayTimeToAllowPublish(); + mPublishCtrlCallback.requestPublishFromInternal( + PublishController.PUBLISH_TRIGGER_RETRY, delayTime); + } else { + // Reset the retry count if the publish is success. + if (requestResponse.isRequestSuccess()) { + mProcessorState.resetRetryCount(); + } + // Update the publish state if the request doesn't need to retry. + int publishResult = requestResponse.getPublishStateByNetworkResponse(); + mPublishCtrlCallback.updatePublishRequestResult(publishResult); + + // Check if there is a pending request + checkAndSendPendingRequest(); + } + + // End this request + setRequestEnded(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; + } + + /** + * 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(); + } + + 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); + } + + private void checkAndSendPendingRequest() { + if (mIsDestroyed) return; + if (mProcessorState.hasPendingRequest()) { + logd("checkAndSendPendingRequest: send pending request"); + mProcessorState.setPublishingFlag(false); + + long delayTime = mProcessorState.getDelayTimeToAllowPublish(); + mPublishCtrlCallback.requestPublishFromInternal( + PublishController.PUBLISH_TRIGGER_RETRY, delayTime); + } + } + + @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) { + mLocalLog.dump(printWriter); } } diff --git a/src/java/com/android/ims/rcs/uce/presence/publish/PublishProcessorState.java b/src/java/com/android/ims/rcs/uce/presence/publish/PublishProcessorState.java new file mode 100644 index 00000000..2ac2fef4 --- /dev/null +++ b/src/java/com/android/ims/rcs/uce/presence/publish/PublishProcessorState.java @@ -0,0 +1,159 @@ +/* + * 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 android.util.Log; + +import com.android.ims.rcs.uce.util.UceUtils; + +import java.time.Duration; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.concurrent.TimeUnit; + +/** + * The helper class to manage the publish request parameters. + */ +public class PublishProcessorState { + + private static final String LOG_TAG = "PublishProcessorState"; + + // The waiting period before the first retry. + private static final int RETRY_BASE_PERIOD = 1; // minute + + // The maximum number of the publication retries. + private static final int PUBLISH_MAXIMUM_NUM_RETRIES = 4; + + private long mTaskId; + private volatile boolean mIsPublishing; + private volatile boolean mPendingRequest; + + private int mRetryCount; + + // The timestamp to allow to execute the publishing request. + private Instant mAllowedTimestamp; + + private final Object mLock = new Object(); + + public PublishProcessorState() { + } + + // Generate a unique task Id for this request. + public long generatePublishTaskId() { + synchronized (mLock) { + mTaskId = UceUtils.generateTaskId(); + return mTaskId; + } + } + + public long getCurrentTaskId() { + synchronized (mLock) { + return mTaskId; + } + } + + public void setPublishingFlag(boolean flag) { + mIsPublishing = flag; + } + + public boolean isPublishingNow() { + return mIsPublishing; + } + + public void setPendingRequest(boolean pendingRequest) { + mPendingRequest = pendingRequest; + } + + boolean hasPendingRequest() { + return mPendingRequest; + } + + // Check if it has reached the maximum retry count. + public boolean isReachMaximumRetries() { + synchronized (mLock) { + return (mRetryCount >= PUBLISH_MAXIMUM_NUM_RETRIES) ? true : false; + } + } + + // Check if the current timestamp is allowed to request publish. + public boolean isCurrentTimeAllowed() { + synchronized (mLock) { + Instant now = Instant.now(); + return (now.isAfter(mAllowedTimestamp)) ? true : false; + } + } + + // Get the delay time to allow to execute the publish request. + public long getDelayTimeToAllowPublish() { + synchronized (mLock) { + // Setup the delay to the time which publish request is allowed to execute. + long delayTime = ChronoUnit.MILLIS.between(Instant.now(), mAllowedTimestamp); + if (delayTime < 0) { + delayTime = 0L; + } + return delayTime; + } + } + + public void resetRetryCount() { + synchronized (mLock) { + mRetryCount = 0; + // Adjust the allowed timestamp of the publishing request. + adjustAllowedPublishTimestamp(); + } + } + + public void increaseRetryCount() { + synchronized (mLock) { + if (mRetryCount < PUBLISH_MAXIMUM_NUM_RETRIES) { + mRetryCount++; + } + adjustAllowedPublishTimestamp(); + } + } + + // Adjust the timestamp to allow request PUBLISH with the specific delay time + private void adjustAllowedPublishTimestamp() { + synchronized (mLock) { + Log.v(LOG_TAG, "adjustAllowedPublishTimestamp: retry=" + mRetryCount); + if (mAllowedTimestamp == null) { + // Now for the initialization. + mAllowedTimestamp = Instant.now(); + } else { + long nextRetryDuration = getNextRetryDuration(); + mAllowedTimestamp = Instant.now().plus(Duration.ofMillis(nextRetryDuration)); + } + Log.v(LOG_TAG, "adjustAllowedPublishTimestamp: timestamp=" + + mAllowedTimestamp.toString()); + } + } + + // Return the milliseconds of the next retry delay. + private long getNextRetryDuration() { + synchronized (mLock) { + int power = mRetryCount - 1; + if (power < 0) { + power = 0; + } + // Next retry duration (minute) + Double retryDuration = RETRY_BASE_PERIOD * Math.pow(2, power); + + // Convert to millis + return TimeUnit.MINUTES.toMillis(retryDuration.longValue()); + } + } +} diff --git a/src/java/com/android/ims/rcs/uce/presence/publish/PublishRequestResponse.java b/src/java/com/android/ims/rcs/uce/presence/publish/PublishRequestResponse.java new file mode 100644 index 00000000..1dac8650 --- /dev/null +++ b/src/java/com/android/ims/rcs/uce/presence/publish/PublishRequestResponse.java @@ -0,0 +1,159 @@ +/* + * 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 android.telephony.ims.ImsException; +import android.telephony.ims.RcsUceAdapter; + +import com.android.ims.rcs.uce.RcsCapabilityExchangeImplAdapter; +import com.android.ims.rcs.uce.RcsCapabilityExchangeImplAdapter.PublishResponseCallback; +import com.android.ims.rcs.uce.presence.publish.PublishController.PublishControllerCallback; +import com.android.ims.rcs.uce.util.NetworkSipCode; + +/** + * Receiving the result callback of the publish request. + */ +public class PublishRequestResponse implements PublishResponseCallback { + + private final long mTaskId; + private volatile boolean mNeedRetry; + private volatile PublishControllerCallback mPublishCtrlCallback; + + private int mCmdErrorCode; + private int mNetworkRespSipCode; + private String mNetworkRespReason; + + public PublishRequestResponse(PublishControllerCallback publishCtrlCallback, long taskId) { + mTaskId = taskId; + mPublishCtrlCallback = publishCtrlCallback; + } + + public long getTaskId() { + return mTaskId; + } + + public int getCmdErrorCode() { + return mCmdErrorCode; + } + + public int getNetworkRespSipCode() { + return mNetworkRespSipCode; + } + + public void onDestroy() { + mPublishCtrlCallback = null; + } + + @Override + public void onCommandError(int errorCode) throws ImsException { + mCmdErrorCode = errorCode; + updateRetryFlagByCommandError(); + + PublishControllerCallback ctrlCallback = mPublishCtrlCallback; + if (ctrlCallback != null) { + ctrlCallback.onRequestCommandError(this); + } + } + + @Override + public void onNetworkResponse(int sipCode, String reason) { + mNetworkRespSipCode = sipCode; + mNetworkRespReason = reason; + updateRetryFlagByNetworkResponse(); + + PublishControllerCallback ctrlCallback = mPublishCtrlCallback; + if (ctrlCallback != null) { + ctrlCallback.onRequestNetworkResp(this); + } + } + + private void updateRetryFlagByCommandError() { + switch(mCmdErrorCode) { + case RcsCapabilityExchangeImplAdapter.COMMAND_CODE_REQUEST_TIMEOUT: + case RcsCapabilityExchangeImplAdapter.COMMAND_CODE_INSUFFICIENT_MEMORY: + case RcsCapabilityExchangeImplAdapter.COMMAND_CODE_LOST_NETWORK_CONNECTION: + case RcsCapabilityExchangeImplAdapter.COMMAND_CODE_SERVICE_UNAVAILABLE: + mNeedRetry = true; + break; + } + } + + private void updateRetryFlagByNetworkResponse() { + switch (mNetworkRespSipCode) { + case NetworkSipCode.SIP_CODE_REQUEST_TIMEOUT: + case NetworkSipCode.SIP_CODE_INTERVAL_TOO_BRIEF: + case NetworkSipCode.SIP_CODE_TEMPORARILY_UNAVAILABLE: + case NetworkSipCode.SIP_CODE_BUSY: + case NetworkSipCode.SIP_CODE_SERVER_INTERNAL_ERROR: + case NetworkSipCode.SIP_CODE_SERVICE_UNAVAILABLE: + case NetworkSipCode.SIP_CODE_SERVER_TIMEOUT: + case NetworkSipCode.SIP_CODE_BUSY_EVERYWHERE: + case NetworkSipCode.SIP_CODE_DECLINE: + mNeedRetry = true; + break; + } + } + + /* + * Check whether the publishing request is successful. + */ + public boolean isRequestSuccess() { + return (mNetworkRespSipCode == NetworkSipCode.SIP_CODE_OK) ? true : false; + } + + /** + * Check whether the publishing request needs to be retried. + */ + public boolean needRetry() { + return mNeedRetry; + } + + /** + * Convert the command error code to the publish state + */ + public int getPublishStateByCmdErrorCode() { + if (RcsCapabilityExchangeImplAdapter.COMMAND_CODE_REQUEST_TIMEOUT == mCmdErrorCode) { + return RcsUceAdapter.PUBLISH_STATE_REQUEST_TIMEOUT; + } + return RcsUceAdapter.PUBLISH_STATE_OTHER_ERROR; + } + + /** + * Convert the network sip code to the publish state + */ + public int getPublishStateByNetworkResponse() { + if (NetworkSipCode.SIP_CODE_REQUEST_TIMEOUT == mNetworkRespSipCode) { + return RcsUceAdapter.PUBLISH_STATE_REQUEST_TIMEOUT; + } + return RcsUceAdapter.PUBLISH_STATE_OTHER_ERROR; + } + + /** + * Get the information of the publish request response. + */ + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("taskId=").append(mTaskId) + .append(", mCmdErrorCode=").append(mCmdErrorCode) + .append(", onNetworkResponse=").append(mNetworkRespSipCode) + .append(", mNetworkResponseReason=").append(mNetworkRespReason) + .append(", isRequestSuccess=").append(isRequestSuccess()) + .append(", needRetry=").append(mNeedRetry); + return builder.toString(); + } +} diff --git a/src/java/com/android/ims/rcs/uce/util/NetworkSipCode.java b/src/java/com/android/ims/rcs/uce/util/NetworkSipCode.java index 21d87fa0..425660a8 100644 --- a/src/java/com/android/ims/rcs/uce/util/NetworkSipCode.java +++ b/src/java/com/android/ims/rcs/uce/util/NetworkSipCode.java @@ -21,7 +21,15 @@ package com.android.ims.rcs.uce.util; */ public class NetworkSipCode { public static final int SIP_CODE_OK = 200; + public static final int SIP_CODE_REQUEST_TIMEOUT = 408; + public static final int SIP_CODE_INTERVAL_TOO_BRIEF = 423; + public static final int SIP_CODE_TEMPORARILY_UNAVAILABLE = 480; + public static final int SIP_CODE_BUSY = 486; + public static final int SIP_CODE_SERVER_INTERNAL_ERROR = 500; public static final int SIP_CODE_SERVICE_UNAVAILABLE = 503; + public static final int SIP_CODE_SERVER_TIMEOUT = 504; + public static final int SIP_CODE_BUSY_EVERYWHERE = 600; + public static final int SIP_CODE_DECLINE = 603; public static final String SIP_OK = "OK"; public static final String SIP_SERVICE_UNAVAILABLE = "Service Unavailable"; diff --git a/src/java/com/android/ims/rcs/uce/util/UceUtils.java b/src/java/com/android/ims/rcs/uce/util/UceUtils.java new file mode 100644 index 00000000..a411a56e --- /dev/null +++ b/src/java/com/android/ims/rcs/uce/util/UceUtils.java @@ -0,0 +1,64 @@ +/* + * 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.util; + +import android.content.Context; +import android.os.PersistableBundle; +import android.telephony.CarrierConfigManager; +import android.telephony.SubscriptionManager; +import android.telephony.ims.ProvisioningManager; +import android.util.Log; + +public class UceUtils { + + private static final String LOG_TAG = "UceUtils"; + + private static long TASK_ID = 0L; + + /** + * Generate the unique UCE request task id. + */ + public static synchronized long generateTaskId() { + return ++TASK_ID; + } + + public static boolean isEabProvisioned(Context context, int subId) { + boolean isProvisioned = false; + if (!SubscriptionManager.isValidSubscriptionId(subId)) { + Log.w(LOG_TAG, "isEabProvisioned: invalid subscriptionId " + subId); + return false; + } + CarrierConfigManager configManager = (CarrierConfigManager) + context.getSystemService(Context.CARRIER_CONFIG_SERVICE); + if (configManager != null) { + PersistableBundle config = configManager.getConfigForSubId(subId); + if (config != null && !config.getBoolean( + CarrierConfigManager.KEY_CARRIER_VOLTE_PROVISIONED_BOOL)) { + return true; + } + } + try { + ProvisioningManager manager = ProvisioningManager.createForSubscriptionId(subId); + isProvisioned = manager.getProvisioningIntValue( + ProvisioningManager.KEY_EAB_PROVISIONING_STATUS) + == ProvisioningManager.PROVISIONING_VALUE_ENABLED; + } catch (Exception e) { + Log.w(LOG_TAG, "isEabProvisioned: exception=" + e.getMessage()); + } + return isProvisioned; + } +} diff --git a/tests/src/com/android/ims/rcs/uce/presence/publish/DeviceCapabilityListenerTest.java b/tests/src/com/android/ims/rcs/uce/presence/publish/DeviceCapabilityListenerTest.java index 23b01fc3..05085f81 100644 --- a/tests/src/com/android/ims/rcs/uce/presence/publish/DeviceCapabilityListenerTest.java +++ b/tests/src/com/android/ims/rcs/uce/presence/publish/DeviceCapabilityListenerTest.java @@ -45,8 +45,6 @@ import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import org.mockito.ArgumentCaptor; -import org.mockito.Captor; import org.mockito.Mock; @RunWith(AndroidJUnit4.class) diff --git a/tests/src/com/android/ims/rcs/uce/presence/publish/PublishControllerImplTest.java b/tests/src/com/android/ims/rcs/uce/presence/publish/PublishControllerImplTest.java index 1041a22b..c9ec14ac 100644 --- a/tests/src/com/android/ims/rcs/uce/presence/publish/PublishControllerImplTest.java +++ b/tests/src/com/android/ims/rcs/uce/presence/publish/PublishControllerImplTest.java @@ -40,8 +40,6 @@ import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import org.mockito.ArgumentCaptor; -import org.mockito.Captor; import org.mockito.Mock; @RunWith(AndroidJUnit4.class) diff --git a/tests/src/com/android/ims/rcs/uce/presence/publish/PublishProcessorTest.java b/tests/src/com/android/ims/rcs/uce/presence/publish/PublishProcessorTest.java new file mode 100644 index 00000000..68373dfb --- /dev/null +++ b/tests/src/com/android/ims/rcs/uce/presence/publish/PublishProcessorTest.java @@ -0,0 +1,261 @@ +/* + * 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.RcsContactPresenceTuple.TUPLE_BASIC_STATUS_OPEN; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +import android.net.Uri; +import android.telephony.ims.RcsContactPresenceTuple; +import android.telephony.ims.RcsContactUceCapability; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import com.android.ims.ImsTestBase; +import com.android.ims.RcsFeatureManager; +import com.android.ims.rcs.uce.presence.publish.PublishController.PublishControllerCallback; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; + +@RunWith(AndroidJUnit4.class) +public class PublishProcessorTest extends ImsTestBase { + + @Mock RcsFeatureManager mRcsFeatureManager; + @Mock DeviceCapabilityInfo mDeviceCapabilities; + @Mock PublishControllerCallback mPublishCtrlCallback; + @Mock PublishProcessorState mProcessorState; + @Mock PublishRequestResponse mResponseCallback; + + private int mSub = 1; + private long mTaskId = 1L; + + @Before + public void setUp() throws Exception { + super.setUp(); + doReturn(true).when(mProcessorState).isCurrentTimeAllowed(); + doReturn(mTaskId).when(mProcessorState).getCurrentTaskId(); + + doReturn(true).when(mDeviceCapabilities).isImsRegistered(); + RcsContactUceCapability capability = getRcsContactUceCapability(); + doReturn(capability).when(mDeviceCapabilities).getDeviceCapabilities(any()); + + doReturn(mTaskId).when(mResponseCallback).getTaskId(); + } + + @After + public void tearDown() throws Exception { + super.tearDown(); + } + + @Test + @SmallTest + public void testDoPublish() throws Exception { + PublishProcessor publishProcessor = getPublishProcessor(); + + publishProcessor.doPublish(PublishController.PUBLISH_TRIGGER_SERVICE); + + verify(mDeviceCapabilities).getDeviceCapabilities(any()); + verify(mProcessorState).setPublishingFlag(true); + verify(mRcsFeatureManager).requestPublication(any(), any()); + verify(mPublishCtrlCallback).setupRequestCanceledTimer(anyLong(), anyLong()); + } + + @Test + @SmallTest + public void testDoPublishWithResetRetryCount() throws Exception { + PublishProcessor publishProcessor = getPublishProcessor(); + + publishProcessor.doPublish(PublishController.PUBLISH_TRIGGER_SERVICE); + + verify(mProcessorState).resetRetryCount(); + } + + @Test + @SmallTest + public void testPublishWithoutResetRetryCount() throws Exception { + PublishProcessor publishProcessor = getPublishProcessor(); + + publishProcessor.doPublish(PublishController.PUBLISH_TRIGGER_RETRY); + + verify(mProcessorState, never()).resetRetryCount(); + } + + @Test + @SmallTest + public void testNotPublishWhenImsNotRegistered() throws Exception { + doReturn(false).when(mDeviceCapabilities).isImsRegistered(); + PublishProcessor publishProcessor = getPublishProcessor(); + + publishProcessor.doPublish(PublishController.PUBLISH_TRIGGER_RETRY); + + verify(mRcsFeatureManager, never()).requestPublication(any(), any()); + } + + @Test + @SmallTest + public void testNotPublishWhenAlreadyPublishing() throws Exception { + doReturn(true).when(mProcessorState).isPublishingNow(); + PublishProcessor publishProcessor = getPublishProcessor(); + + publishProcessor.doPublish(PublishController.PUBLISH_TRIGGER_RETRY); + + verify(mProcessorState).setPendingRequest(true); + verify(mRcsFeatureManager, never()).requestPublication(any(), any()); + } + + @Test + @SmallTest + public void testNotPublishWhenReachMaximumRetries() throws Exception { + doReturn(true).when(mProcessorState).isReachMaximumRetries(); + PublishProcessor publishProcessor = getPublishProcessor(); + + publishProcessor.doPublish(PublishController.PUBLISH_TRIGGER_RETRY); + + verify(mRcsFeatureManager, never()).requestPublication(any(), any()); + } + + @Test + @SmallTest + public void testNotPublishWhenCurrentTimeNotAllowed() throws Exception { + doReturn(false).when(mProcessorState).isCurrentTimeAllowed(); + PublishProcessor publishProcessor = getPublishProcessor(); + + publishProcessor.doPublish(PublishController.PUBLISH_TRIGGER_RETRY); + + verify(mPublishCtrlCallback).requestPublishFromInternal( + eq(PublishController.PUBLISH_TRIGGER_RETRY), anyLong()); + verify(mRcsFeatureManager, never()).requestPublication(any(), any()); + } + + @Test + @SmallTest + public void testCommandErrorWithRetry() throws Exception { + doReturn(true).when(mProcessorState).isPublishingNow(); + doReturn(mTaskId).when(mProcessorState).getCurrentTaskId(); + doReturn(mTaskId).when(mResponseCallback).getTaskId(); + doReturn(true).when(mResponseCallback).needRetry(); + PublishProcessor publishProcessor = getPublishProcessor(); + + publishProcessor.onCommandError(mResponseCallback); + + verify(mProcessorState).increaseRetryCount(); + verify(mPublishCtrlCallback).requestPublishFromInternal( + eq(PublishController.PUBLISH_TRIGGER_RETRY), anyLong()); + verify(mResponseCallback).onDestroy(); + verify(mProcessorState).setPublishingFlag(false); + verify(mPublishCtrlCallback).clearRequestCanceledTimer(); + } + + @Test + @SmallTest + public void testCommandErrorWithoutRetry() throws Exception { + doReturn(true).when(mProcessorState).isPublishingNow(); + doReturn(mTaskId).when(mProcessorState).getCurrentTaskId(); + doReturn(mTaskId).when(mResponseCallback).getTaskId(); + doReturn(false).when(mResponseCallback).needRetry(); + PublishProcessor publishProcessor = getPublishProcessor(); + + publishProcessor.onCommandError(mResponseCallback); + + verify(mPublishCtrlCallback).updatePublishRequestResult(anyInt()); + verify(mResponseCallback).onDestroy(); + verify(mProcessorState).setPublishingFlag(false); + verify(mPublishCtrlCallback).clearRequestCanceledTimer(); + } + + @Test + @SmallTest + public void testNetworkResponseWithRetry() throws Exception { + doReturn(true).when(mProcessorState).isPublishingNow(); + doReturn(mTaskId).when(mProcessorState).getCurrentTaskId(); + doReturn(mTaskId).when(mResponseCallback).getTaskId(); + doReturn(true).when(mResponseCallback).needRetry(); + PublishProcessor publishProcessor = getPublishProcessor(); + + publishProcessor.onNetworkResponse(mResponseCallback); + + verify(mProcessorState).increaseRetryCount(); + verify(mPublishCtrlCallback).requestPublishFromInternal( + eq(PublishController.PUBLISH_TRIGGER_RETRY), anyLong()); + verify(mResponseCallback).onDestroy(); + verify(mProcessorState).setPublishingFlag(false); + verify(mPublishCtrlCallback).clearRequestCanceledTimer(); + } + + @Test + @SmallTest + public void testNetworkResponseSuccessful() throws Exception { + doReturn(true).when(mProcessorState).isPublishingNow(); + doReturn(mTaskId).when(mProcessorState).getCurrentTaskId(); + doReturn(mTaskId).when(mResponseCallback).getTaskId(); + doReturn(false).when(mResponseCallback).needRetry(); + doReturn(true).when(mResponseCallback).isRequestSuccess(); + PublishProcessor publishProcessor = getPublishProcessor(); + + publishProcessor.onNetworkResponse(mResponseCallback); + + verify(mPublishCtrlCallback).updatePublishRequestResult(anyInt()); + verify(mResponseCallback).onDestroy(); + verify(mProcessorState).setPublishingFlag(false); + verify(mPublishCtrlCallback).clearRequestCanceledTimer(); + } + + @Test + @SmallTest + public void testCancelPublishRequest() throws Exception { + PublishProcessor publishProcessor = getPublishProcessor(); + + publishProcessor.cancelPublishRequest(mTaskId); + + verify(mProcessorState).setPublishingFlag(false); + verify(mPublishCtrlCallback).clearRequestCanceledTimer(); + } + + private PublishProcessor getPublishProcessor() { + PublishProcessor publishProcessor = new PublishProcessor(mContext, mSub, + mDeviceCapabilities, mPublishCtrlCallback); + publishProcessor.setProcessorState(mProcessorState); + publishProcessor.onRcsConnected(mRcsFeatureManager); + return publishProcessor; + } + + private RcsContactUceCapability getRcsContactUceCapability() { + Uri contact = Uri.fromParts("sip", "test", null); + + RcsContactUceCapability.PresenceBuilder builder = + new RcsContactUceCapability.PresenceBuilder(contact, + RcsContactUceCapability.SOURCE_TYPE_CACHED, + RcsContactUceCapability.REQUEST_RESULT_FOUND); + + RcsContactPresenceTuple.Builder tupleBuilder = new RcsContactPresenceTuple.Builder( + TUPLE_BASIC_STATUS_OPEN, RcsContactPresenceTuple.SERVICE_ID_MMTEL, "1.0"); + + builder.addCapabilityTuple(tupleBuilder.build()); + return builder.build(); + } +} |