1 /*
2  * Copyright (c) 2021 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.ims.rcs.uce.request;
18 
19 import android.telephony.ims.RcsUceAdapter;
20 import android.telephony.ims.RcsUceAdapter.ErrorCode;
21 import android.text.TextUtils;
22 import android.util.Log;
23 
24 import com.android.ims.rcs.uce.util.UceUtils;
25 
26 import java.util.Optional;
27 
28 /**
29  * The helper class to analyze the result of the callback onTerminated to determine whether the
30  * subscription request should be retried or not.
31  */
32 public class SubscriptionTerminatedHelper {
33 
34     private static final String LOG_TAG = UceUtils.getLogPrefix() + "SubscriptionTerminated";
35 
36     // The terminated reasons defined in RFC 3265 3.2.4
37     private static final String REASON_DEACTIVATED = "deactivated";
38     private static final String REASON_PROBATION = "probation";
39     private static final String REASON_REJECTED = "rejected";
40     private static final String REASON_TIMEOUT = "timeout";
41     private static final String REASON_GIVEUP = "giveup";
42     private static final String REASON_NORESOURCE = "noresource";
43 
44     /**
45      * The analysis result of the callback onTerminated.
46      */
47     static class TerminatedResult {
48         private final @ErrorCode Optional<Integer> mErrorCode;
49         private final long mRetryAfterMillis;
50 
TerminatedResult(@rrorCode Optional<Integer> errorCode, long retryAfterMillis)51         public TerminatedResult(@ErrorCode Optional<Integer> errorCode, long retryAfterMillis) {
52             mErrorCode = errorCode;
53             mRetryAfterMillis = retryAfterMillis;
54         }
55 
56         /**
57          * @return the error code when the request is failed. Optional.empty if the request is
58          * successful.
59          */
getErrorCode()60         public Optional<Integer> getErrorCode() {
61             return mErrorCode;
62         }
63 
getRetryAfterMillis()64         public long getRetryAfterMillis() {
65             return mRetryAfterMillis;
66         }
67 
toString()68         public String toString() {
69             StringBuilder builder = new StringBuilder();
70             builder.append("TerminatedResult: ")
71                     .append("errorCode=").append(mErrorCode)
72                     .append(", retryAfterMillis=").append(mRetryAfterMillis);
73             return builder.toString();
74         }
75     }
76 
77     /**
78      * According to the RFC 3265, Check the given reason to see whether clients should retry the
79      * subscribe request.
80      * <p>
81      * See RFC 3265 3.2.4 for the detail.
82      *
83      * @param reason The reason why the subscribe request is terminated. The reason is given by the
84      * network and it could be empty.
85      * @param retryAfterMillis How long should clients wait before retrying.
86      * @param allCapsHaveReceived Whether all the request contact capabilities have been received.
87      */
getAnalysisResult(String reason, long retryAfterMillis, boolean allCapsHaveReceived)88     public static TerminatedResult getAnalysisResult(String reason, long retryAfterMillis,
89             boolean allCapsHaveReceived) {
90         TerminatedResult result = null;
91         if (TextUtils.isEmpty(reason)) {
92             /*
93              * When the value of retryAfterMillis is larger then zero, the client should retry.
94              */
95             if (retryAfterMillis > 0L) {
96                 result = new TerminatedResult(Optional.of(RcsUceAdapter.ERROR_GENERIC_FAILURE),
97                         retryAfterMillis);
98             }
99         } else if (REASON_DEACTIVATED.equalsIgnoreCase(reason)) {
100             /*
101              * When the reason is "deactivated", clients should retry immediately.
102              */
103             long retry = getRequestRetryAfterMillis(retryAfterMillis);
104             result = new TerminatedResult(Optional.of(RcsUceAdapter.ERROR_GENERIC_FAILURE), retry);
105         } else if (REASON_PROBATION.equalsIgnoreCase(reason)) {
106             /*
107              * When the reason is "probation", it means that the subscription has been terminated,
108              * but the client should retry at some later time.
109              */
110             long retry = getRequestRetryAfterMillis(retryAfterMillis);
111             result = new TerminatedResult(Optional.of(RcsUceAdapter.ERROR_GENERIC_FAILURE), retry);
112         } else if (REASON_REJECTED.equalsIgnoreCase(reason)) {
113             /*
114              * When the reason is "rejected", it means that the subscription has been terminated
115              * due to chang in authorization policy. Clients should NOT retry.
116              */
117             result = new TerminatedResult(Optional.of(RcsUceAdapter.ERROR_NOT_AUTHORIZED), 0L);
118         } else if (REASON_TIMEOUT.equalsIgnoreCase(reason)) {
119             if (retryAfterMillis > 0L) {
120                 /*
121                  * When the parameter "retryAfterMillis" is greater than zero, it means that the
122                  * ImsService requires clients should retry later.
123                  */
124                 long retry = getRequestRetryAfterMillis(retryAfterMillis);
125                 result = new TerminatedResult(Optional.of(RcsUceAdapter.ERROR_REQUEST_TIMEOUT),
126                         retry);
127             } else if (!allCapsHaveReceived) {
128                 /*
129                  * The ImsService does not require to retry when the parameter "retryAfterMillis"
130                  * is zero. However, the request is still failed because it has not received all
131                  * the capabilities updated from the network.
132                  */
133                 result = new TerminatedResult(Optional.of(RcsUceAdapter.ERROR_REQUEST_TIMEOUT), 0L);
134             } else {
135                 /*
136                  * The subscribe request is successfully when the parameter retryAfter is zero and
137                  * all the request capabilities have been received.
138                  */
139                 result = new TerminatedResult(Optional.empty(), 0L);
140             }
141         } else if (REASON_GIVEUP.equalsIgnoreCase(reason)) {
142             /*
143              * The subscription has been terminated because the notifier could no obtain
144              * authorization in a timely fashion. Clients could retry the subscribe request.
145              */
146             long retry = getRequestRetryAfterMillis(retryAfterMillis);
147             result = new TerminatedResult(Optional.of(RcsUceAdapter.ERROR_NOT_AUTHORIZED), retry);
148         } else if (REASON_NORESOURCE.equalsIgnoreCase(reason)) {
149             /*
150              * The subscription has been terminated because the resource is no longer exists.
151              * Clients should NOT retry.
152              */
153             result = new TerminatedResult(Optional.of(RcsUceAdapter.ERROR_NOT_FOUND), 0L);
154         } else if (retryAfterMillis > 0L) {
155             /*
156              * Even if the reason is not listed above, clients should retry the request as long as
157              * the value of retry is non-zero.
158              */
159             long retry = getRequestRetryAfterMillis(retryAfterMillis);
160             result = new TerminatedResult(Optional.of(RcsUceAdapter.ERROR_GENERIC_FAILURE), retry);
161         }
162 
163         // The request should be successful. when the terminated is not in the above cases
164         if (result == null) {
165             result = new TerminatedResult(Optional.empty(), 0L);
166         }
167 
168         Log.d(LOG_TAG, "getAnalysisResult: reason=" + reason + ", retry=" + retryAfterMillis +
169                 ", allCapsHaveReceived=" + allCapsHaveReceived + ", " + result);
170         return result;
171     }
172 
173     /*
174      * Get the appropriated retryAfterMillis for the subscribe request.
175      */
getRequestRetryAfterMillis(long retryAfterMillis)176     private static long getRequestRetryAfterMillis(long retryAfterMillis) {
177         // Return the minimum retry after millis if the given retryAfterMillis is less than the
178         // minimum value.
179         long minRetryAfterMillis = UceUtils.getMinimumRequestRetryAfterMillis();
180         return (retryAfterMillis < minRetryAfterMillis) ? minRetryAfterMillis : retryAfterMillis;
181     }
182 }
183