1 /*
2  * Copyright 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 androidx.camera.core;
18 
19 import androidx.annotation.IntDef;
20 import androidx.annotation.IntRange;
21 import androidx.annotation.RestrictTo;
22 import androidx.camera.core.impl.CameraProviderInitRetryPolicy;
23 import androidx.camera.core.impl.RetryPolicyInternal;
24 import androidx.camera.core.impl.TimeoutRetryPolicy;
25 import androidx.core.util.Preconditions;
26 
27 import org.jspecify.annotations.NonNull;
28 import org.jspecify.annotations.Nullable;
29 
30 import java.lang.annotation.Retention;
31 import java.lang.annotation.RetentionPolicy;
32 
33 /**
34  * Defines a strategy for retrying upon initialization failures of the {@link CameraProvider}.
35  * When the initialization task is interrupted by an error or exception during execution, the
36  * task will determine whether to be rescheduled based on the specified {@link RetryPolicy}.
37  *
38  * <p>Several predefined retry policies are available:
39  * <ul>
40  * <li>{@link RetryPolicy#NEVER}: Never retries the initialization.</li>
41  * <li>{@link RetryPolicy#DEFAULT}: The default retry policy, which retries initialization up to
42  *   a maximum timeout of {@link #getDefaultRetryTimeoutInMillis()}, providing a general-purpose
43  *   approach for handling most errors.</li>
44  * <li>{@link RetryPolicy#RETRY_UNAVAILABLE_CAMERA}: The retry policy automatically retries upon
45  *   encountering errors like the {@link #DEFAULT} policy, and specifically designed to handle
46  *   potential device inconsistencies in reporting available camera instances. In cases where the
47  *   initial camera configuration fails due to the device underreporting (i.e., not accurately
48  *   disclosing all available camera instances) the policy proactively triggers a
49  *   reinitialization attempt. If the cameras are not successfully configured within
50  *   {@link #getDefaultRetryTimeoutInMillis()}, the initialization is considered a failure.</li>
51  * </ul>
52  * <p>If no {@link RetryPolicy} is specified, {@link RetryPolicy#DEFAULT} will be used as
53  * the default.
54  *
55  * <p>Examples of configuring the {@link RetryPolicy}:
56  * <pre>{@code
57  * // Configuring {@link RetryPolicy#RETRY_UNAVAILABLE_CAMERA} to the CameraXConfig
58  * ProcessCameraProvider.configureInstance(
59  *    CameraXConfig.Builder.fromConfig(Camera2Config.defaultConfig())
60  *      .setCameraProviderInitRetryPolicy(RetryPolicy.RETRY_UNAVAILABLE_CAMERA)
61  *      .build()
62  * );
63  * ...
64  * }</pre>
65  *
66  * <p>Configuring a customized {@link RetryPolicy}:
67  * <pre>{@code
68  * ProcessCameraProvider.configureInstance(CameraXConfig.Builder.fromConfig(
69  *         Camera2Config.defaultConfig()).setCameraProviderInitRetryPolicy(
70  *         executionState -> {
71  *             if (executionState.getExecutedTimeInMillis() > 10000L
72  *                     || executionState.getNumOfAttempts() > 10
73  *                     || executionState.getStatus() == ExecutionState.STATUS_CONFIGURATION_FAIL) {
74  *                 return RetryConfig.NOT_RETRY;
75  *             } else if (executionState.getStatus() == ExecutionState.STATUS_CAMERA_UNAVAILABLE) {
76  *                 return RetryConfig.DEFAULT_DELAY_RETRY;
77  *             } else {
78  *                 Log.d("CameraX", "Unknown error occur: " + executionState.getCause());
79  *                 return RetryConfig.MINI_DELAY_RETRY;
80  *             }
81  *         }).build());
82  * ...
83  * }</pre>
84  * In the second example, the custom retry policy retries the initialization up to 10 times or
85  * for a maximum of 10 seconds. If an unknown error occurs, the retry policy delays the next
86  * retry after a delay defined by {@link RetryConfig#MINI_DELAY_RETRY}. The retry process
87  * stops if the status is {@link ExecutionState#STATUS_CONFIGURATION_FAIL}. For
88  * {@link ExecutionState#STATUS_CAMERA_UNAVAILABLE}, the retry policy applies
89  * {@link RetryConfig#DEFAULT_DELAY_RETRY}.
90  */
91 @ExperimentalRetryPolicy
92 public interface RetryPolicy {
93 
94     /**
95      * Default timeout in milliseconds of the initialization.
96      */
97     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
98     long DEFAULT_RETRY_TIMEOUT_IN_MILLIS = 6000L;
99 
100     /**
101      * A retry policy that prevents any retry attempts and
102      * immediately halts the initialization upon encountering an error.
103      */
104     @NonNull RetryPolicy NEVER = executionState -> RetryConfig.NOT_RETRY;
105 
106     /**
107      * This retry policy increases initialization success by automatically retrying upon
108      * encountering errors.
109      *
110      * <p>By default, it continues retrying for a maximum of
111      * {@link #getDefaultRetryTimeoutInMillis()} milliseconds. To adjust the timeout duration to
112      * specific requirements, utilize {@link RetryPolicy.Builder}.
113      *
114      * <p>Example: Create a policy based on {@link #DEFAULT} with a 10-second timeout:
115      * <pre>{@code
116      * RetryPolicy customTimeoutPolicy =
117      *     new RetryPolicy.Builder(RetryPolicy.DEFAULT).setTimeoutInMillis(10000L).build();
118      * }</pre>
119      */
120     @NonNull RetryPolicy DEFAULT = new CameraProviderInitRetryPolicy.Legacy(
121             getDefaultRetryTimeoutInMillis());
122 
123     /**
124      * This retry policy automatically retries upon encountering errors and specifically
125      * designed to handle potential device inconsistencies in reporting available camera
126      * instances. If the initial camera configuration fails due to underreporting, the policy
127      * automatically attempts reinitialization.
128      * <p>By default, it perseveres for a maximum of {@link #getDefaultRetryTimeoutInMillis()}
129      * milliseconds before conceding initialization as unsuccessful. For finer control over the
130      * timeout duration, utilize {@link RetryPolicy.Builder}.
131      * <p>Example: Create a policy based on {@link #RETRY_UNAVAILABLE_CAMERA} with a 10-second
132      * timeout:
133      * <pre>{@code
134      * RetryPolicy customTimeoutPolicy = new RetryPolicy.Builder(
135      *     RetryPolicy.RETRY_UNAVAILABLE_CAMERA).setTimeoutInMillis(10000L).build();
136      * }</pre>
137      */
138     @NonNull RetryPolicy RETRY_UNAVAILABLE_CAMERA =
139             new CameraProviderInitRetryPolicy(getDefaultRetryTimeoutInMillis());
140 
141     /**
142      * Retrieves the default timeout value, in milliseconds, used for initialization retries.
143      *
144      * @return The default timeout duration, expressed in milliseconds. This value determines the
145      * maximum time to wait for a successful initialization before considering the process as
146      * failed.
147      */
getDefaultRetryTimeoutInMillis()148     static long getDefaultRetryTimeoutInMillis() {
149         return DEFAULT_RETRY_TIMEOUT_IN_MILLIS;
150     }
151 
152     /**
153      * Called to request a decision on whether to retry the initialization process.
154      *
155      * @param executionState Information about the current execution state of the camera
156      *                       initialization.
157      * @return A RetryConfig indicating whether to retry, along with any associated delay.
158      */
onRetryDecisionRequested(@onNull ExecutionState executionState)159     @NonNull RetryConfig onRetryDecisionRequested(@NonNull ExecutionState executionState);
160 
161     /**
162      * Returns the maximum allowed retry duration in milliseconds. Initialization will
163      * be terminated if retries take longer than this timeout. A value of 0 indicates
164      * no timeout is enforced.
165      *
166      * <p>The default value is 0 (no timeout).
167      *
168      * @return The retry timeout in milliseconds.
169      */
getTimeoutInMillis()170     default long getTimeoutInMillis() {
171         return 0;
172     }
173 
174     /**
175      * A builder class for customizing RetryPolicy behavior.
176      *
177      * <p>Use the {@link #Builder(RetryPolicy)} to modify existing RetryPolicy instances,
178      * typically starting with the predefined options like {@link RetryPolicy#DEFAULT} or
179      * {@link RetryPolicy#RETRY_UNAVAILABLE_CAMERA}. The most common use is to set a custom timeout.
180      *
181      * <p>Example: Create a policy based on {@link RetryPolicy#DEFAULT} with a 10-second timeout:
182      * <pre>{@code
183      * new RetryPolicy.Builder(RetryPolicy.DEFAULT).setTimeoutInMillis(10000L).build()
184      * }</pre>
185      */
186     @ExperimentalRetryPolicy
187     final class Builder {
188 
189         private final RetryPolicy mBasePolicy;
190         private long mTimeoutInMillis;
191 
192         /**
193          * Creates a builder based on an existing {@link RetryPolicy}.
194          *
195          * <p>This allows you to start with a predefined policy and add further customizations.
196          * For example, set a timeout to prevent retries from continuing endlessly.
197          *
198          * @param basePolicy The RetryPolicy to use as a starting point.
199          */
Builder(@onNull RetryPolicy basePolicy)200         public Builder(@NonNull RetryPolicy basePolicy) {
201             mBasePolicy = basePolicy;
202             mTimeoutInMillis = basePolicy.getTimeoutInMillis();
203         }
204 
205         /**
206          * Sets a timeout in milliseconds. If retries exceed this duration, they will be
207          * terminated with {@link RetryConfig#NOT_RETRY}.
208          *
209          * @param timeoutInMillis The maximum duration for retries in milliseconds. A value of 0
210          *                        indicates no timeout.
211          * @return {@code this} for method chaining.
212          */
setTimeoutInMillis(long timeoutInMillis)213         public @NonNull Builder setTimeoutInMillis(long timeoutInMillis) {
214             mTimeoutInMillis = timeoutInMillis;
215             return this;
216         }
217 
218         /**
219          * Creates the customized {@link RetryPolicy} instance.
220          *
221          * @return The new {@link RetryPolicy}.
222          */
build()223         public @NonNull RetryPolicy build() {
224             if (mBasePolicy instanceof RetryPolicyInternal) {
225                 return ((RetryPolicyInternal) mBasePolicy).copy(mTimeoutInMillis);
226             }
227             return new TimeoutRetryPolicy(mTimeoutInMillis, mBasePolicy);
228         }
229     }
230 
231     /**
232      * Provides insights into the current execution state of the camera initialization process.
233      *
234      * <p>The ExecutionState empowers app developers to make informed decisions about retry
235      * strategies and fine-tune timeout settings. It also facilitates comprehensive app-level
236      * logging for tracking initialization success/failure rates and overall camera performance.
237      *
238      * <p>Key use cases:
239      * <ul>
240      *   <li>Determining whether to retry initialization based on specific error conditions.</li>
241      *   <li>Logging detailed execution information for debugging and analysis.</li>
242      *   <li>Monitoring retry success/failure rates to identify potential issues.</li>
243      * </ul>
244      */
245     @ExperimentalRetryPolicy
246     interface ExecutionState {
247 
248         /**
249          * Defines the status codes for the {@link ExecutionState}.
250          */
251         @IntDef({STATUS_UNKNOWN_ERROR, STATUS_CONFIGURATION_FAIL, STATUS_CAMERA_UNAVAILABLE})
252         @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
253         @Retention(RetentionPolicy.SOURCE)
254         @interface Status {
255         }
256 
257         /**
258          * Indicates that the initialization encountered an unknown error. This error may be
259          * transient, and retrying may resolve the issue.
260          */
261         int STATUS_UNKNOWN_ERROR = 0;
262 
263         /**
264          * Indicates that initialization of CameraX failed due to invalid customization options
265          * within the provided {@link CameraXConfig}. This error typically indicates a problem
266          * with the configuration settings and may require developer attention to resolve.
267          *
268          * <p>Possible Causes:
269          * <ul>
270          *  <li>Incorrect or incompatible settings within the {@link CameraXConfig}.</li>
271          *  <li>Conflicting options that prevent successful initialization.</li>
272          *  <li>Missing or invalid values for essential configuration parameters.</li>
273          * </ul>
274          *
275          * <p>To troubleshoot:
276          * Please see {@code getCause().getMessage()} for details, and review configuration of
277          * the {@link CameraXConfig} to identify potential errors or inconsistencies in the
278          * customization options.
279          *
280          * @see androidx.camera.lifecycle.ProcessCameraProvider#configureInstance(CameraXConfig)
281          * @see CameraXConfig.Builder
282          */
283         int STATUS_CONFIGURATION_FAIL = 1;
284 
285         /**
286          * Indicates that the {@link CameraProvider} failed to initialize due to an unavailable
287          * camera.
288          * This error suggests a temporary issue with the device's cameras, and retrying may
289          * resolve the issue.
290          */
291         int STATUS_CAMERA_UNAVAILABLE = 2;
292 
293         /**
294          * Retrieves the status of the most recently completed initialization attempt.
295          *
296          * @return The status code representing the outcome of the initialization.
297          */
298         @Status
getStatus()299         int getStatus();
300 
301         /**
302          * Gets the cause that occurred during the task execution, if any.
303          *
304          * @return The cause that occurred during the task execution, or null if there was no error.
305          */
getCause()306         @Nullable Throwable getCause();
307 
308         /**
309          * Gets the total execution time of the initialization task in milliseconds.
310          *
311          * @return The total execution time of the initialization task in milliseconds.
312          */
getExecutedTimeInMillis()313         long getExecutedTimeInMillis();
314 
315         /**
316          * Indicates the total number of attempts made to initialize the camera, including the
317          * current attempt. The count starts from 1.
318          *
319          * @return The total number of attempts made to initialize the camera.
320          */
getNumOfAttempts()321         int getNumOfAttempts();
322     }
323 
324     /**
325      * Represents the outcome of a {@link RetryPolicy} decision.
326      */
327     @ExperimentalRetryPolicy
328     final class RetryConfig {
329 
330         private static final long MINI_DELAY_MILLIS = 100L;
331         private static final long DEFAULT_DELAY_MILLIS = 500L;
332 
333         /** A RetryConfig indicating that no further retries should be attempted. */
334         public static final @NonNull RetryConfig NOT_RETRY = new RetryConfig(false, 0L);
335 
336         /**
337          * A RetryConfig indicating that the initialization should be retried after the default
338          * delay (determined by {@link #getDefaultRetryDelayInMillis()}). This delay provides
339          * sufficient time for typical device recovery processes, balancing retry efficiency
340          * and minimizing user wait time.
341          */
342         public static final @NonNull RetryConfig DEFAULT_DELAY_RETRY = new RetryConfig(true);
343 
344         /**
345          * A RetryConfig indicating that the initialization should be retried after a minimum
346          * delay of 100 milliseconds.
347          *
348          * This short delay serves two purposes:
349          * <ul>
350          * <li>Reduced Latency: Minimizes the wait time for the camera to become available,
351          * improving user experience.
352          * <li>Camera Self-Recovery: Provides a brief window for the camera to potentially
353          * recover from temporary issues.
354          * </ul>
355          * This approach balances quick retries with potential self-recovery, aiming for the
356          * fastest possible camera restoration.
357          */
358         public static final @NonNull RetryConfig MINI_DELAY_RETRY =
359                 new RetryConfig(true, MINI_DELAY_MILLIS);
360 
361         /**
362          * A RetryConfig indicating that the initialization should be considered complete
363          * without retrying. This config is intended for internal use and is not intended to
364          * trigger further retries. It represents the legacy behavior of not failing the
365          * initialization task for minor issues.
366          */
367         @RestrictTo(RestrictTo.Scope.LIBRARY)
368         public static @NonNull RetryConfig COMPLETE_WITHOUT_FAILURE =
369                 new RetryConfig(false, 0, true);
370 
371         /**
372          * Returns the recommended default delay to optimize retry attempts and camera recovery.
373          *
374          * @return The default delay value, carefully calibrated based on extensive lab testing to:
375          * <ul>
376          *  <li>Provide sufficient time for typical device recovery processes in common bad
377          *      camera states.</li>
378          *  <li>Strike an optimal balance between minimizing latency and avoiding excessive retries,
379          *      ensuring efficient camera initialization without unnecessary overhead.</li>
380          * </ul>
381          * This value represents the generally recommended delay for most scenarios, striking a
382          * balance between providing adequate time for camera recovery and maintaining a smooth
383          * user experience.
384          */
getDefaultRetryDelayInMillis()385         public static long getDefaultRetryDelayInMillis() {
386             return DEFAULT_DELAY_MILLIS;
387         }
388 
389         private final long mDelayInMillis;
390         private final boolean mShouldRetry;
391         private final boolean mCompleteWithoutFailure;
392 
RetryConfig(boolean shouldRetry)393         private RetryConfig(boolean shouldRetry) {
394             this(shouldRetry, RetryConfig.getDefaultRetryDelayInMillis());
395         }
396 
RetryConfig(boolean shouldRetry, long delayInMillis)397         private RetryConfig(boolean shouldRetry, long delayInMillis) {
398             this(shouldRetry, delayInMillis, false);
399         }
400 
401         /**
402          * Constructor for determining whether to retry the initialization.
403          *
404          * @param shouldRetry            Whether to retry the initialization.
405          * @param delayInMillis          The delay time in milliseconds before starting the next
406          *                               retry.
407          * @param completeWithoutFailure Indicates whether to skip retries and not fail the
408          *                               initialization. This is a flag for legacy behavior to
409          *                               avoid failing the initialization task for minor issues.
410          *                               When this flag is set to true, `shouldRetry` must be
411          *                               false.
412          */
RetryConfig(boolean shouldRetry, long delayInMillis, boolean completeWithoutFailure)413         private RetryConfig(boolean shouldRetry, long delayInMillis,
414                 boolean completeWithoutFailure) {
415             mShouldRetry = shouldRetry;
416             mDelayInMillis = delayInMillis;
417             if (completeWithoutFailure) {
418                 Preconditions.checkArgument(!shouldRetry,
419                         "shouldRetry must be false when completeWithoutFailure is set to true");
420             }
421             mCompleteWithoutFailure = completeWithoutFailure;
422         }
423 
424         /**
425          * Determines whether the initialization should be retried.
426          *
427          * @return true if the initialization should be retried, false otherwise.
428          */
shouldRetry()429         public boolean shouldRetry() {
430             return mShouldRetry;
431         }
432 
433         /**
434          * Gets the delay time in milliseconds before the next retry attempt should be made.
435          *
436          * @return The delay time in milliseconds.
437          */
getRetryDelayInMillis()438         public long getRetryDelayInMillis() {
439             return mDelayInMillis;
440         }
441 
442         /**
443          * Signals to treat initialization errors as successful for legacy behavior compatibility.
444          *
445          * <p>This config is intended for internal use and is not intended to trigger further
446          * retries.
447          *
448          * @return true if initialization should be deemed complete without additional retries,
449          * despite any errors encountered. Otherwise, returns false.
450          */
451         @RestrictTo(RestrictTo.Scope.LIBRARY)
shouldCompleteWithoutFailure()452         public boolean shouldCompleteWithoutFailure() {
453             return mCompleteWithoutFailure;
454         }
455 
456         /**
457          * A builder class for creating and customizing {@link RetryConfig} objects.
458          *
459          * <p>While predefined configs like {@link RetryConfig#DEFAULT_DELAY_RETRY} are
460          * recommended for typical recovery scenarios, this builder allows for fine-tuned control
461          * when specific requirements necessitate a different approach.
462          */
463         @ExperimentalRetryPolicy
464         public static final class Builder {
465 
466             private boolean mShouldRetry = true;
467             private long mTimeoutInMillis = RetryConfig.getDefaultRetryDelayInMillis();
468 
469             /**
470              * Specifies whether a retry should be attempted.
471              *
472              * @param shouldRetry  If true (the default), initialization will be retried.
473              *                     If false, initialization will not be retried.
474              * @return {@code this} for method chaining.
475              */
setShouldRetry(boolean shouldRetry)476             public @NonNull Builder setShouldRetry(boolean shouldRetry) {
477                 mShouldRetry = shouldRetry;
478                 return this;
479             }
480 
481             /**
482              * Sets the retry delay in milliseconds.
483              *
484              * <p>If set, the initialization will be retried after the specified delay. For optimal
485              * results, the delay should be within the range of 100 to 2000 milliseconds. This
486              * aligns with lab testing, which suggests this range provides sufficient recovery
487              * time for most common camera issues while minimizing latency.
488              *
489              * @param timeoutInMillis The delay in milliseconds.
490              * @return {@code this} for method chaining.
491              */
setRetryDelayInMillis( @ntRangefrom = 100, to = 2000) long timeoutInMillis)492             public @NonNull Builder setRetryDelayInMillis(
493                     @IntRange(from = 100, to = 2000) long timeoutInMillis) {
494                 mTimeoutInMillis = timeoutInMillis;
495                 return this;
496             }
497 
498             /**
499              * Builds the customized {@link RetryConfig} object.
500              *
501              * @return The configured RetryConfig.
502              */
build()503             public @NonNull RetryConfig build() {
504                 return new RetryConfig(mShouldRetry, mTimeoutInMillis);
505             }
506         }
507     }
508 }
509