RetryPolicy.java

/*
 * Copyright 2023 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 androidx.camera.core;

import androidx.annotation.IntDef;
import androidx.annotation.IntRange;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.camera.core.impl.CameraProviderInitRetryPolicy;
import androidx.camera.core.impl.RetryPolicyInternal;
import androidx.camera.core.impl.TimeoutRetryPolicy;
import androidx.core.util.Preconditions;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

/**
 * Defines a strategy for retrying upon initialization failures of the {@link CameraProvider}.
 * When the initialization task is interrupted by an error or exception during execution, the
 * task will determine whether to be rescheduled based on the specified {@link RetryPolicy}.
 *
 * <p>Several predefined retry policies are available:
 * <ul>
 * <li>{@link RetryPolicy#NEVER}: Never retries the initialization.</li>
 * <li>{@link RetryPolicy#DEFAULT}: The default retry policy, which retries initialization up to
 *   a maximum timeout of {@link #getDefaultRetryTimeoutInMillis()}, providing a general-purpose
 *   approach for handling most errors.</li>
 * <li>{@link RetryPolicy#RETRY_UNAVAILABLE_CAMERA}: The retry policy automatically retries upon
 *   encountering errors like the {@link #DEFAULT} policy, and specifically designed to handle
 *   potential device inconsistencies in reporting available camera instances. In cases where the
 *   initial camera configuration fails due to the device underreporting (i.e., not accurately
 *   disclosing all available camera instances) the policy proactively triggers a
 *   reinitialization attempt. If the cameras are not successfully configured within
 *   {@link #getDefaultRetryTimeoutInMillis()}, the initialization is considered a failure.</li>
 * </ul>
 * <p>If no {@link RetryPolicy} is specified, {@link RetryPolicy#DEFAULT} will be used as
 * the default.
 *
 * <p>Examples of configuring the {@link RetryPolicy}:
 * <pre>{@code
 * // Configuring {@link RetryPolicy#RETRY_UNAVAILABLE_CAMERA} to the CameraXConfig
 * ProcessCameraProvider.configureInstance(
 *    CameraXConfig.Builder.fromConfig(Camera2Config.defaultConfig())
 *      .setCameraProviderInitRetryPolicy(RetryPolicy.RETRY_UNAVAILABLE_CAMERA)
 *      .build()
 * );
 * ...
 * }</pre>
 *
 * <p>Configuring a customized {@link RetryPolicy}:
 * <pre>{@code
 * ProcessCameraProvider.configureInstance(CameraXConfig.Builder.fromConfig(
 *         Camera2Config.defaultConfig()).setCameraProviderInitRetryPolicy(
 *         executionState -> {
 *             if (executionState.getExecutedTimeInMillis() > 10000L
 *                     || executionState.getNumOfAttempts() > 10
 *                     || executionState.getStatus() == ExecutionState.STATUS_CONFIGURATION_FAIL) {
 *                 return RetryConfig.NOT_RETRY;
 *             } else if (executionState.getStatus() == ExecutionState.STATUS_CAMERA_UNAVAILABLE) {
 *                 return RetryConfig.DEFAULT_DELAY_RETRY;
 *             } else {
 *                 Log.d("CameraX", "Unknown error occur: " + executionState.getCause());
 *                 return RetryConfig.MINI_DELAY_RETRY;
 *             }
 *         }).build());
 * ...
 * }</pre>
 * In the second example, the custom retry policy retries the initialization up to 10 times or
 * for a maximum of 10 seconds. If an unknown error occurs, the retry policy delays the next
 * retry after a delay defined by {@link RetryConfig#MINI_DELAY_RETRY}. The retry process
 * stops if the status is {@link ExecutionState#STATUS_CONFIGURATION_FAIL}. For
 * {@link ExecutionState#STATUS_CAMERA_UNAVAILABLE}, the retry policy applies
 * {@link RetryConfig#DEFAULT_DELAY_RETRY}.
 */
@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
@ExperimentalRetryPolicy
public interface RetryPolicy {

    /**
     * Default timeout in milliseconds of the initialization.
     */
    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    long DEFAULT_RETRY_TIMEOUT_IN_MILLIS = 6000L;

    /**
     * A retry policy that prevents any retry attempts and
     * immediately halts the initialization upon encountering an error.
     */
    @NonNull
    RetryPolicy NEVER = executionState -> RetryConfig.NOT_RETRY;

    /**
     * This retry policy increases initialization success by automatically retrying upon
     * encountering errors.
     *
     * <p>By default, it continues retrying for a maximum of
     * {@link #getDefaultRetryTimeoutInMillis()} milliseconds. To adjust the timeout duration to
     * specific requirements, utilize {@link RetryPolicy.Builder}.
     *
     * <p>Example: Create a policy based on {@link #DEFAULT} with a 10-second timeout:
     * <pre>{@code
     * RetryPolicy customTimeoutPolicy =
     *     new RetryPolicy.Builder(RetryPolicy.DEFAULT).setTimeoutInMillis(10000L).build();
     * }</pre>
     */
    @NonNull
    RetryPolicy DEFAULT = new CameraProviderInitRetryPolicy.Legacy(
            getDefaultRetryTimeoutInMillis());

    /**
     * This retry policy automatically retries upon encountering errors and specifically
     * designed to handle potential device inconsistencies in reporting available camera
     * instances. If the initial camera configuration fails due to underreporting, the policy
     * automatically attempts reinitialization.
     * <p>By default, it perseveres for a maximum of {@link #getDefaultRetryTimeoutInMillis()}
     * milliseconds before conceding initialization as unsuccessful. For finer control over the
     * timeout duration, utilize {@link RetryPolicy.Builder}.
     * <p>Example: Create a policy based on {@link #RETRY_UNAVAILABLE_CAMERA} with a 10-second
     * timeout:
     * <pre>{@code
     * RetryPolicy customTimeoutPolicy = new RetryPolicy.Builder(
     *     RetryPolicy.RETRY_UNAVAILABLE_CAMERA).setTimeoutInMillis(10000L).build();
     * }</pre>
     */
    @NonNull
    RetryPolicy RETRY_UNAVAILABLE_CAMERA =
            new CameraProviderInitRetryPolicy(getDefaultRetryTimeoutInMillis());

    /**
     * Retrieves the default timeout value, in milliseconds, used for initialization retries.
     *
     * @return The default timeout duration, expressed in milliseconds. This value determines the
     * maximum time to wait for a successful initialization before considering the process as
     * failed.
     */
    static long getDefaultRetryTimeoutInMillis() {
        return DEFAULT_RETRY_TIMEOUT_IN_MILLIS;
    }

    /**
     * Called to request a decision on whether to retry the initialization process.
     *
     * @param executionState Information about the current execution state of the camera
     *                       initialization.
     * @return A RetryConfig indicating whether to retry, along with any associated delay.
     */
    @NonNull
    RetryConfig onRetryDecisionRequested(@NonNull ExecutionState executionState);

    /**
     * Returns the maximum allowed retry duration in milliseconds. Initialization will
     * be terminated if retries take longer than this timeout. A value of 0 indicates
     * no timeout is enforced.
     *
     * <p>The default value is 0 (no timeout).
     *
     * @return The retry timeout in milliseconds.
     */
    default long getTimeoutInMillis() {
        return 0;
    }

    /**
     * A builder class for customizing RetryPolicy behavior.
     *
     * <p>Use the {@link #Builder(RetryPolicy)} to modify existing RetryPolicy instances,
     * typically starting with the predefined options like {@link RetryPolicy#DEFAULT} or
     * {@link RetryPolicy#RETRY_UNAVAILABLE_CAMERA}. The most common use is to set a custom timeout.
     *
     * <p>Example: Create a policy based on {@link RetryPolicy#DEFAULT} with a 10-second timeout:
     * <pre>{@code
     * new RetryPolicy.Builder(RetryPolicy.DEFAULT).setTimeoutInMillis(10000L).build()
     * }</pre>
     */
    @ExperimentalRetryPolicy
    final class Builder {

        private final RetryPolicy mBasePolicy;
        private long mTimeoutInMillis;

        /**
         * Creates a builder based on an existing {@link RetryPolicy}.
         *
         * <p>This allows you to start with a predefined policy and add further customizations.
         * For example, set a timeout to prevent retries from continuing endlessly.
         *
         * @param basePolicy The RetryPolicy to use as a starting point.
         */
        public Builder(@NonNull RetryPolicy basePolicy) {
            mBasePolicy = basePolicy;
            mTimeoutInMillis = basePolicy.getTimeoutInMillis();
        }

        /**
         * Sets a timeout in milliseconds. If retries exceed this duration, they will be
         * terminated with {@link RetryConfig#NOT_RETRY}.
         *
         * @param timeoutInMillis The maximum duration for retries in milliseconds. A value of 0
         *                        indicates no timeout.
         * @return {@code this} for method chaining.
         */
        @NonNull
        public Builder setTimeoutInMillis(long timeoutInMillis) {
            mTimeoutInMillis = timeoutInMillis;
            return this;
        }

        /**
         * Creates the customized {@link RetryPolicy} instance.
         *
         * @return The new {@link RetryPolicy}.
         */
        @NonNull
        public RetryPolicy build() {
            if (mBasePolicy instanceof RetryPolicyInternal) {
                return ((RetryPolicyInternal) mBasePolicy).copy(mTimeoutInMillis);
            }
            return new TimeoutRetryPolicy(mTimeoutInMillis, mBasePolicy);
        }
    }

    /**
     * Provides insights into the current execution state of the camera initialization process.
     *
     * <p>The ExecutionState empowers app developers to make informed decisions about retry
     * strategies and fine-tune timeout settings. It also facilitates comprehensive app-level
     * logging for tracking initialization success/failure rates and overall camera performance.
     *
     * <p>Key use cases:
     * <ul>
     *   <li>Determining whether to retry initialization based on specific error conditions.</li>
     *   <li>Logging detailed execution information for debugging and analysis.</li>
     *   <li>Monitoring retry success/failure rates to identify potential issues.</li>
     * </ul>
     */
    @ExperimentalRetryPolicy
    interface ExecutionState {

        /**
         * Defines the status codes for the {@link ExecutionState}.
         */
        @IntDef({STATUS_UNKNOWN_ERROR, STATUS_CONFIGURATION_FAIL, STATUS_CAMERA_UNAVAILABLE})
        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
        @Retention(RetentionPolicy.SOURCE)
        @interface Status {
        }

        /**
         * Indicates that the initialization encountered an unknown error. This error may be
         * transient, and retrying may resolve the issue.
         */
        int STATUS_UNKNOWN_ERROR = 0;

        /**
         * Indicates that initialization of CameraX failed due to invalid customization options
         * within the provided {@link CameraXConfig}. This error typically indicates a problem
         * with the configuration settings and may require developer attention to resolve.
         *
         * <p>Possible Causes:
         * <ul>
         *  <li>Incorrect or incompatible settings within the {@link CameraXConfig}.</li>
         *  <li>Conflicting options that prevent successful initialization.</li>
         *  <li>Missing or invalid values for essential configuration parameters.</li>
         * </ul>
         *
         * <p>To troubleshoot:
         * Please see {@code getCause().getMessage()} for details, and review configuration of
         * the {@link CameraXConfig} to identify potential errors or inconsistencies in the
         * customization options.
         *
         * @see androidx.camera.lifecycle.ProcessCameraProvider#configureInstance(CameraXConfig)
         * @see CameraXConfig.Builder
         */
        int STATUS_CONFIGURATION_FAIL = 1;

        /**
         * Indicates that the {@link CameraProvider} failed to initialize due to an unavailable
         * camera.
         * This error suggests a temporary issue with the device's cameras, and retrying may
         * resolve the issue.
         */
        int STATUS_CAMERA_UNAVAILABLE = 2;

        /**
         * Retrieves the status of the most recently completed initialization attempt.
         *
         * @return The status code representing the outcome of the initialization.
         */
        @Status
        int getStatus();

        /**
         * Gets the cause that occurred during the task execution, if any.
         *
         * @return The cause that occurred during the task execution, or null if there was no error.
         */
        @Nullable
        Throwable getCause();

        /**
         * Gets the total execution time of the initialization task in milliseconds.
         *
         * @return The total execution time of the initialization task in milliseconds.
         */
        long getExecutedTimeInMillis();

        /**
         * Indicates the total number of attempts made to initialize the camera, including the
         * current attempt. The count starts from 1.
         *
         * @return The total number of attempts made to initialize the camera.
         */
        int getNumOfAttempts();
    }

    /**
     * Represents the outcome of a {@link RetryPolicy} decision.
     */
    @ExperimentalRetryPolicy
    final class RetryConfig {

        private static final long MINI_DELAY_MILLIS = 100L;
        private static final long DEFAULT_DELAY_MILLIS = 500L;

        /** A RetryConfig indicating that no further retries should be attempted. */
        @NonNull
        public static final RetryConfig NOT_RETRY = new RetryConfig(false, 0L);

        /**
         * A RetryConfig indicating that the initialization should be retried after the default
         * delay (determined by {@link #getDefaultRetryDelayInMillis()}). This delay provides
         * sufficient time for typical device recovery processes, balancing retry efficiency
         * and minimizing user wait time.
         */
        @NonNull
        public static final RetryConfig DEFAULT_DELAY_RETRY = new RetryConfig(true);

        /**
         * A RetryConfig indicating that the initialization should be retried after a minimum
         * delay of 100 milliseconds.
         *
         * This short delay serves two purposes:
         * <ul>
         * <li>Reduced Latency: Minimizes the wait time for the camera to become available,
         * improving user experience.
         * <li>Camera Self-Recovery: Provides a brief window for the camera to potentially
         * recover from temporary issues.
         * </ul>
         * This approach balances quick retries with potential self-recovery, aiming for the
         * fastest possible camera restoration.
         */
        @NonNull
        public static final RetryConfig MINI_DELAY_RETRY = new RetryConfig(true, MINI_DELAY_MILLIS);

        /**
         * A RetryConfig indicating that the initialization should be considered complete
         * without retrying. This config is intended for internal use and is not intended to
         * trigger further retries. It represents the legacy behavior of not failing the
         * initialization task for minor issues.
         */
        @RestrictTo(RestrictTo.Scope.LIBRARY)
        @NonNull
        public static RetryConfig COMPLETE_WITHOUT_FAILURE = new RetryConfig(false, 0, true);

        /**
         * Returns the recommended default delay to optimize retry attempts and camera recovery.
         *
         * @return The default delay value, carefully calibrated based on extensive lab testing to:
         * <ul>
         *  <li>Provide sufficient time for typical device recovery processes in common bad
         *      camera states.</li>
         *  <li>Strike an optimal balance between minimizing latency and avoiding excessive retries,
         *      ensuring efficient camera initialization without unnecessary overhead.</li>
         * </ul>
         * This value represents the generally recommended delay for most scenarios, striking a
         * balance between providing adequate time for camera recovery and maintaining a smooth
         * user experience.
         */
        public static long getDefaultRetryDelayInMillis() {
            return DEFAULT_DELAY_MILLIS;
        }

        private final long mDelayInMillis;
        private final boolean mShouldRetry;
        private final boolean mCompleteWithoutFailure;

        private RetryConfig(boolean shouldRetry) {
            this(shouldRetry, RetryConfig.getDefaultRetryDelayInMillis());
        }

        private RetryConfig(boolean shouldRetry, long delayInMillis) {
            this(shouldRetry, delayInMillis, false);
        }

        /**
         * Constructor for determining whether to retry the initialization.
         *
         * @param shouldRetry            Whether to retry the initialization.
         * @param delayInMillis          The delay time in milliseconds before starting the next
         *                               retry.
         * @param completeWithoutFailure Indicates whether to skip retries and not fail the
         *                               initialization. This is a flag for legacy behavior to
         *                               avoid failing the initialization task for minor issues.
         *                               When this flag is set to true, `shouldRetry` must be
         *                               false.
         */
        private RetryConfig(boolean shouldRetry, long delayInMillis,
                boolean completeWithoutFailure) {
            mShouldRetry = shouldRetry;
            mDelayInMillis = delayInMillis;
            if (completeWithoutFailure) {
                Preconditions.checkArgument(!shouldRetry,
                        "shouldRetry must be false when completeWithoutFailure is set to true");
            }
            mCompleteWithoutFailure = completeWithoutFailure;
        }

        /**
         * Determines whether the initialization should be retried.
         *
         * @return true if the initialization should be retried, false otherwise.
         */
        public boolean shouldRetry() {
            return mShouldRetry;
        }

        /**
         * Gets the delay time in milliseconds before the next retry attempt should be made.
         *
         * @return The delay time in milliseconds.
         */
        public long getRetryDelayInMillis() {
            return mDelayInMillis;
        }

        /**
         * Signals to treat initialization errors as successful for legacy behavior compatibility.
         *
         * <p>This config is intended for internal use and is not intended to trigger further
         * retries.
         *
         * @return true if initialization should be deemed complete without additional retries,
         * despite any errors encountered. Otherwise, returns false.
         */
        @RestrictTo(RestrictTo.Scope.LIBRARY)
        public boolean shouldCompleteWithoutFailure() {
            return mCompleteWithoutFailure;
        }

        /**
         * A builder class for creating and customizing {@link RetryConfig} objects.
         *
         * <p>While predefined configs like {@link RetryConfig#DEFAULT_DELAY_RETRY} are
         * recommended for typical recovery scenarios, this builder allows for fine-tuned control
         * when specific requirements necessitate a different approach.
         */
        @ExperimentalRetryPolicy
        public static final class Builder {

            private boolean mShouldRetry = true;
            private long mTimeoutInMillis = RetryConfig.getDefaultRetryDelayInMillis();

            /**
             * Specifies whether a retry should be attempted.
             *
             * @param shouldRetry  If true (the default), initialization will be retried.
             *                     If false, initialization will not be retried.
             * @return {@code this} for method chaining.
             */
            @NonNull
            public Builder setShouldRetry(boolean shouldRetry) {
                mShouldRetry = shouldRetry;
                return this;
            }

            /**
             * Sets the retry delay in milliseconds.
             *
             * <p>If set, the initialization will be retried after the specified delay. For optimal
             * results, the delay should be within the range of 100 to 2000 milliseconds. This
             * aligns with lab testing, which suggests this range provides sufficient recovery
             * time for most common camera issues while minimizing latency.
             *
             * @param timeoutInMillis The delay in milliseconds.
             * @return {@code this} for method chaining.
             */
            @NonNull
            public Builder setRetryDelayInMillis(
                    @IntRange(from = 100, to = 2000) long timeoutInMillis) {
                mTimeoutInMillis = timeoutInMillis;
                return this;
            }

            /**
             * Builds the customized {@link RetryConfig} object.
             *
             * @return The configured RetryConfig.
             */
            @NonNull
            public RetryConfig build() {
                return new RetryConfig(mShouldRetry, mTimeoutInMillis);
            }
        }
    }
}