1 /*
2  * Copyright (C) 2023 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 android.adservices.exceptions;
18 
19 import static java.util.Locale.ENGLISH;
20 
21 import android.annotation.NonNull;
22 
23 import com.android.internal.util.Preconditions;
24 
25 import java.time.Duration;
26 import java.util.Objects;
27 
28 /**
29  * Exception thrown by the service when the HTTP failure response that caused the API to fail
30  * contains a <a href="https://httpwg.org/specs/rfc6585.html#status-429">Retry-After header</a>.
31  *
32  * @hide
33  */
34 public class RetryableAdServicesNetworkException extends AdServicesNetworkException {
35     /** @hide */
36     public static final Duration UNSET_RETRY_AFTER_VALUE = Duration.ZERO;
37 
38     /** @hide */
39     public static final Duration DEFAULT_RETRY_AFTER_VALUE = Duration.ofMillis(30 * 1000);
40 
41     /** @hide */
42     public static final String INVALID_RETRY_AFTER_MESSAGE =
43             "Retry-after time duration must be strictly greater than zero.";
44 
45     // TODO: (b/298100114) make this final again
46     private Duration mRetryAfter;
47 
48     /**
49      * Constructs an {@link RetryableAdServicesNetworkException} that is caused by a failed HTTP
50      * request.
51      *
52      * @param errorCode relevant {@link ErrorCode} corresponding to the failure.
53      * @param retryAfter time {@link Duration} to back-off until next retry.
54      */
RetryableAdServicesNetworkException( @rrorCode int errorCode, @NonNull Duration retryAfter)55     public RetryableAdServicesNetworkException(
56             @ErrorCode int errorCode, @NonNull Duration retryAfter) {
57         super(errorCode);
58 
59         Objects.requireNonNull(retryAfter);
60         Preconditions.checkArgument(
61                 retryAfter.compareTo(UNSET_RETRY_AFTER_VALUE) > 0, INVALID_RETRY_AFTER_MESSAGE);
62 
63         mRetryAfter = retryAfter;
64     }
65 
66     /**
67      * If {@link #mRetryAfter} < {@code defaultDuration}, it gets set to {@code defaultDuration}. If
68      * {@link #mRetryAfter} > {@code maxDuration}, it gets set to {@code maxDuration}. If it falls
69      * in the range of both numbers, it stays the same.
70      *
71      * @hide
72      */
setRetryAfterToValidDuration(long defaultDuration, long maxDuration)73     public void setRetryAfterToValidDuration(long defaultDuration, long maxDuration) {
74         // TODO: (b/298100114) this is a hack! this method should be removed after resolving the bug
75         mRetryAfter =
76                 Duration.ofMillis(
77                         Math.min(Math.max(mRetryAfter.toMillis(), defaultDuration), maxDuration));
78     }
79 
80     /**
81      * @return the positive retry-after {@link Duration} if set or else {@link Duration#ZERO} by
82      *     default.
83      */
84     @NonNull
getRetryAfter()85     public Duration getRetryAfter() {
86         return mRetryAfter;
87     }
88 
89     /**
90      * @return a human-readable representation of {@link RetryableAdServicesNetworkException}.
91      */
92     @Override
toString()93     public String toString() {
94         return String.format(
95                 ENGLISH,
96                 "%s: {Error code: %s, Retry after: %sms}",
97                 this.getClass().getCanonicalName(),
98                 this.getErrorCode(),
99                 this.getRetryAfter().toMillis());
100     }
101 }
102