/* * 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.request; import android.annotation.NonNull; import android.annotation.Nullable; import android.net.Uri; import android.telephony.ims.RcsContactTerminatedReason; import android.telephony.ims.RcsContactUceCapability; import android.telephony.ims.RcsUceAdapter; import android.telephony.ims.RcsUceAdapter.ErrorCode; import android.telephony.ims.SipDetails; import android.telephony.ims.stub.RcsCapabilityExchangeImplBase; import android.telephony.ims.stub.RcsCapabilityExchangeImplBase.CommandCode; import android.text.TextUtils; import android.util.Log; import com.android.ims.rcs.uce.UceController; import com.android.ims.rcs.uce.presence.pidfparser.PidfParserUtils; import com.android.ims.rcs.uce.util.NetworkSipCode; import com.android.ims.rcs.uce.util.UceUtils; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; /** * The container of the result of the capabilities request. */ public class CapabilityRequestResponse { private static final String LOG_TAG = UceUtils.getLogPrefix() + "CapabilityRequestResp"; // The error code when the request encounters internal errors. private @ErrorCode Optional mRequestInternalError; // The command error code of the request. It is assigned by the callback "onCommandError" private @CommandCode Optional mCommandError; // The SIP code and reason of the network response. private Optional mNetworkRespSipCode; private Optional mReasonPhrase; // The SIP code and the phrase read from the reason header private Optional mReasonHeaderCause; private Optional mReasonHeaderText; // The reason why the this request was terminated and how long after it can be retried. // This value is assigned by the callback "onTerminated" private Optional mTerminatedReason; private Optional mRetryAfterMillis; // The list of the valid capabilities which is retrieved from the cache. private List mCachedCapabilityList; // The list of the updated capabilities. This is assigned by the callback // "onNotifyCapabilitiesUpdate" private List mUpdatedCapabilityList; // The list of the terminated resource. This is assigned by the callback // "onResourceTerminated" private List mTerminatedResource; // The list of the remote contact's capability. private Set mRemoteCaps; // The collection to record whether the request contacts have received the capabilities updated. private Map mContactCapsReceived; // The SIP detail information of the network response. private Optional mSipDetails; public CapabilityRequestResponse() { mRequestInternalError = Optional.empty(); mCommandError = Optional.empty(); mNetworkRespSipCode = Optional.empty(); mReasonPhrase = Optional.empty(); mReasonHeaderCause = Optional.empty(); mReasonHeaderText = Optional.empty(); mTerminatedReason = Optional.empty(); mRetryAfterMillis = Optional.of(0L); mTerminatedResource = new ArrayList<>(); mCachedCapabilityList = new ArrayList<>(); mUpdatedCapabilityList = new ArrayList<>(); mRemoteCaps = new HashSet<>(); mContactCapsReceived = new HashMap<>(); mSipDetails = Optional.empty(); } /** * Set the request contacts which is expected to receive the capabilities updated. */ public synchronized void setRequestContacts(List contactUris) { // Initialize the default value to FALSE. All the numbers have not received the // capabilities updated. contactUris.forEach(contact -> mContactCapsReceived.put(contact, Boolean.FALSE)); Log.d(LOG_TAG, "setRequestContacts: size=" + mContactCapsReceived.size()); } /** * Get the contacts that have not received the capabilities updated yet. */ public synchronized List getNotReceiveCapabilityUpdatedContact() { return mContactCapsReceived.entrySet() .stream() .filter(entry -> Objects.equals(entry.getValue(), Boolean.FALSE)) .map(Map.Entry::getKey) .collect(Collectors.toList()); } /** * Set the request contacts which is expected to receive the capabilities updated. */ public synchronized boolean haveAllRequestCapsUpdatedBeenReceived() { return !(mContactCapsReceived.containsValue(Boolean.FALSE)); } /** * Set the error code when the request encounters internal unexpected errors. * @param errorCode the error code of the internal request error. */ public synchronized void setRequestInternalError(@ErrorCode int errorCode) { mRequestInternalError = Optional.of(errorCode); } /** * Get the request internal error code. */ public synchronized Optional getRequestInternalError() { return mRequestInternalError; } /** * Set the command error code which is sent from ImsService and set the capability error code. */ public synchronized void setCommandError(@CommandCode int commandError) { mCommandError = Optional.of(commandError); } /** * Get the command error codeof this request. */ public synchronized Optional getCommandError() { return mCommandError; } /** * Set the network response of this request which is sent by the network. */ public synchronized void setNetworkResponseCode(int sipCode, String reason) { mNetworkRespSipCode = Optional.of(sipCode); mReasonPhrase = Optional.ofNullable(reason); } /** * Set the network response of this request which is sent by the network. */ public synchronized void setSipDetails(@NonNull SipDetails details) { setNetworkResponseCode(details.getResponseCode(), details.getResponsePhrase()); if (details.getReasonHeaderCause() != 0) { mReasonHeaderCause = Optional.of(details.getReasonHeaderCause()); } else { mReasonHeaderCause = Optional.empty(); } if (TextUtils.isEmpty(details.getReasonHeaderText())) { mReasonHeaderText = Optional.empty(); } else { mReasonHeaderText = Optional.ofNullable(details.getReasonHeaderText()); } mSipDetails = Optional.ofNullable(details); } // Get the sip code of the network response. public synchronized Optional getNetworkRespSipCode() { return mNetworkRespSipCode; } // Get the reason of the network response. public synchronized Optional getReasonPhrase() { return mReasonPhrase; } // Get the response sip code from the reason header. public synchronized Optional getReasonHeaderCause() { return mReasonHeaderCause; } // Get the response phrae from the reason header. public synchronized Optional getReasonHeaderText() { return mReasonHeaderText; } public Optional getResponseSipCode() { if (mReasonHeaderCause.isPresent()) { return mReasonHeaderCause; } else { return mNetworkRespSipCode; } } public Optional getResponseReason() { if (mReasonPhrase.isPresent()) { return mReasonPhrase; } else { return mReasonHeaderText; } } /** * Set the reason and retry-after info when the callback onTerminated is called. * @param reason The reason why this request is terminated. * @param retryAfterMillis How long to wait before retry this request. */ public synchronized void setTerminated(String reason, long retryAfterMillis) { mTerminatedReason = Optional.ofNullable(reason); mRetryAfterMillis = Optional.of(retryAfterMillis); } /** * @return The reason of terminating the subscription request. empty string if it has not * been given. */ public synchronized String getTerminatedReason() { return mTerminatedReason.orElse(""); } /** * @return Return the retryAfterMillis, 0L if the value is not present. */ public synchronized long getRetryAfterMillis() { return mRetryAfterMillis.orElse(0L); } /** * Retrieve the SIP information which received from the network. */ public Optional getSipDetails() { return mSipDetails; } /** * Add the capabilities which are retrieved from the cache. */ public synchronized void addCachedCapabilities(List capabilityList) { mCachedCapabilityList.addAll(capabilityList); // Update the flag to indicate that these contacts have received the capabilities updated. updateCapsReceivedFlag(capabilityList); } /** * Update the flag to indicate that the given contacts have received the capabilities updated. */ private synchronized void updateCapsReceivedFlag(List updatedCapList) { for (RcsContactUceCapability updatedCap : updatedCapList) { Uri updatedUri = updatedCap.getContactUri(); if (updatedUri == null) continue; String updatedUriStr = updatedUri.toString(); for (Map.Entry contactCapEntry : mContactCapsReceived.entrySet()) { String number = UceUtils.getContactNumber(contactCapEntry.getKey()); if (!TextUtils.isEmpty(number) && updatedUriStr.contains(number)) { // Set the flag that this contact has received the capability updated. contactCapEntry.setValue(true); } } } } /** * Clear the cached capabilities when the cached capabilities have been sent to client. */ public synchronized void removeCachedContactCapabilities() { mCachedCapabilityList.clear(); } /** * @return the cached capabilities. */ public synchronized List getCachedContactCapability() { return Collections.unmodifiableList(mCachedCapabilityList); } /** * Add the updated contact capabilities which sent from ImsService. */ public synchronized void addUpdatedCapabilities(List capabilityList) { mUpdatedCapabilityList.addAll(capabilityList); // Update the flag to indicate that these contacts have received the capabilities updated. updateCapsReceivedFlag(capabilityList); } /** * Remove the given capabilities from the UpdatedCapabilityList when these capabilities have * updated to the requester. */ public synchronized void removeUpdatedCapabilities(List capList) { mUpdatedCapabilityList.removeAll(capList); } /** * Get all the updated capabilities to trigger the capability receive callback. */ public synchronized List getUpdatedContactCapability() { return Collections.unmodifiableList(mUpdatedCapabilityList); } /** * Add the terminated resources which sent from ImsService. */ public synchronized void addTerminatedResource(List resourceList) { // Convert the RcsContactTerminatedReason to RcsContactUceCapability List capabilityList = resourceList.stream() .filter(Objects::nonNull) .map(reason -> PidfParserUtils.getTerminatedCapability( reason.getContactUri(), reason.getReason())).collect(Collectors.toList()); // Save the terminated resource. mTerminatedResource.addAll(capabilityList); // Update the flag to indicate that these contacts have received the capabilities updated. updateCapsReceivedFlag(capabilityList); } /* * Remove the given capabilities from the mTerminatedResource when these capabilities have * updated to the requester. */ public synchronized void removeTerminatedResources(List resourceList) { mTerminatedResource.removeAll(resourceList); } /** * Get the terminated resources which sent from ImsService. */ public synchronized List getTerminatedResources() { return Collections.unmodifiableList(mTerminatedResource); } /** * Set the remote's capabilities which are sent from the network. */ public synchronized void setRemoteCapabilities(Set remoteCaps) { if (remoteCaps != null) { remoteCaps.stream().filter(Objects::nonNull).forEach(capability -> mRemoteCaps.add(capability)); } } /** * Get the remote capability feature tags. */ public synchronized Set getRemoteCapability() { return Collections.unmodifiableSet(mRemoteCaps); } /** * Check if the network response is success. * @return true if the network response code is OK or Accepted and the Reason header cause * is either not present or OK. */ public synchronized boolean isNetworkResponseOK() { final int sipCodeOk = NetworkSipCode.SIP_CODE_OK; final int sipCodeAccepted = NetworkSipCode.SIP_CODE_ACCEPTED; Optional respSipCode = getNetworkRespSipCode(); if (respSipCode.filter(c -> (c == sipCodeOk || c == sipCodeAccepted)).isPresent() && (!getReasonHeaderCause().isPresent() || getReasonHeaderCause().filter(c -> c == sipCodeOk).isPresent())) { return true; } return false; } /** * Check whether the request is forbidden or not. * @return true if the Reason header sip code is 403(Forbidden) or the response sip code is 403. */ public synchronized boolean isRequestForbidden() { final int sipCodeForbidden = NetworkSipCode.SIP_CODE_FORBIDDEN; if (getReasonHeaderCause().isPresent()) { return getReasonHeaderCause().filter(c -> c == sipCodeForbidden).isPresent(); } else { return getNetworkRespSipCode().filter(c -> c == sipCodeForbidden).isPresent(); } } /** * Check the contacts of the request is not found. * @return true if the sip code of the network response is one of NOT_FOUND(404), * SIP_CODE_METHOD_NOT_ALLOWED(405) or DOES_NOT_EXIST_ANYWHERE(604) */ public synchronized boolean isNotFound() { Optional respSipCode = Optional.empty(); if (getReasonHeaderCause().isPresent()) { respSipCode = getReasonHeaderCause(); } else if (getNetworkRespSipCode().isPresent()) { respSipCode = getNetworkRespSipCode(); } if (respSipCode.isPresent()) { int sipCode = respSipCode.get(); if (sipCode == NetworkSipCode.SIP_CODE_NOT_FOUND || sipCode == NetworkSipCode.SIP_CODE_METHOD_NOT_ALLOWED || sipCode == NetworkSipCode.SIP_CODE_DOES_NOT_EXIST_ANYWHERE) { return true; } } return false; } /** * This method convert from the command error code which are defined in the * RcsCapabilityExchangeImplBase to the Capabilities error code which are defined in the * RcsUceAdapter. */ public static int getCapabilityErrorFromCommandError(@CommandCode int cmdError) { int uceError; switch (cmdError) { case RcsCapabilityExchangeImplBase.COMMAND_CODE_SERVICE_UNKNOWN: case RcsCapabilityExchangeImplBase.COMMAND_CODE_GENERIC_FAILURE: case RcsCapabilityExchangeImplBase.COMMAND_CODE_INVALID_PARAM: case RcsCapabilityExchangeImplBase.COMMAND_CODE_FETCH_ERROR: case RcsCapabilityExchangeImplBase.COMMAND_CODE_NOT_SUPPORTED: case RcsCapabilityExchangeImplBase.COMMAND_CODE_NO_CHANGE: uceError = RcsUceAdapter.ERROR_GENERIC_FAILURE; break; case RcsCapabilityExchangeImplBase.COMMAND_CODE_NOT_FOUND: uceError = RcsUceAdapter.ERROR_NOT_FOUND; break; case RcsCapabilityExchangeImplBase.COMMAND_CODE_REQUEST_TIMEOUT: uceError = RcsUceAdapter.ERROR_REQUEST_TIMEOUT; break; case RcsCapabilityExchangeImplBase.COMMAND_CODE_INSUFFICIENT_MEMORY: uceError = RcsUceAdapter.ERROR_INSUFFICIENT_MEMORY; break; case RcsCapabilityExchangeImplBase.COMMAND_CODE_LOST_NETWORK_CONNECTION: uceError = RcsUceAdapter.ERROR_LOST_NETWORK; break; case RcsCapabilityExchangeImplBase.COMMAND_CODE_SERVICE_UNAVAILABLE: uceError = RcsUceAdapter.ERROR_SERVER_UNAVAILABLE; break; default: uceError = RcsUceAdapter.ERROR_GENERIC_FAILURE; break; } return uceError; } /** * Convert the SIP error code which sent by ImsService to the capability error code. */ public static int getCapabilityErrorFromSipCode(CapabilityRequestResponse response) { int sipError; String respReason; // Check the sip code in the Reason header first if the Reason Header is present. if (response.getReasonHeaderCause().isPresent()) { sipError = response.getReasonHeaderCause().get(); respReason = response.getReasonHeaderText().orElse(""); } else { sipError = response.getNetworkRespSipCode().orElse(-1); respReason = response.getReasonPhrase().orElse(""); } return NetworkSipCode.getCapabilityErrorFromSipCode(sipError, respReason, UceController.REQUEST_TYPE_CAPABILITY); } @Override public synchronized String toString() { StringBuilder builder = new StringBuilder(); return builder.append("RequestInternalError=").append(mRequestInternalError.orElse(-1)) .append(", CommandErrorCode=").append(mCommandError.orElse(-1)) .append(", NetworkResponseCode=").append(mNetworkRespSipCode.orElse(-1)) .append(", NetworkResponseReason=").append(mReasonPhrase.orElse("")) .append(", ReasonHeaderCause=").append(mReasonHeaderCause.orElse(-1)) .append(", ReasonHeaderText=").append(mReasonHeaderText.orElse("")) .append(", TerminatedReason=").append(mTerminatedReason.orElse("")) .append(", RetryAfterMillis=").append(mRetryAfterMillis.orElse(0L)) .append(", Terminated resource size=" + mTerminatedResource.size()) .append(", cached capability size=" + mCachedCapabilityList.size()) .append(", Updated capability size=" + mUpdatedCapabilityList.size()) .append(", RemoteCaps size=" + mRemoteCaps.size()) .toString(); } }