chromium/chrome/browser/download/internal/android/java/src/org/chromium/chrome/browser/download/home/list/view/CircularProgressView.java

// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.chrome.browser.download.home.list.view;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.view.View;
import android.widget.ImageView;

import androidx.annotation.IntDef;
import androidx.annotation.StringRes;

import org.chromium.base.MathUtils;
import org.chromium.chrome.browser.download.internal.R;
import org.chromium.components.browser_ui.widget.async_image.AutoAnimatorDrawable;
import org.chromium.components.browser_ui.widget.async_image.ForegroundDrawableCompat;
import org.chromium.ui.UiUtils;
import org.chromium.ui.widget.ChromeImageButton;

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

/**
 * A representation of a progress bar that supports (1) an indeterminate state, (2) a determinate
 * state, and (3) running, paused, and retry states.
 *
 * The determinate {@link Drawable} will have it's level set via {@link Drawable#setLevel(int)}
 * based on the progress (0 - 10,000).
 *
 * The indeterminate and determinate {@link Drawable}s support {@link Animatable} drawables and the
 * animation will be started/stopped when shown/hidden respectively.
 */
public class CircularProgressView extends ChromeImageButton {
    /**
     * The value to use with {@link #setProgress(int)} to specify that the indeterminate
     * {@link Drawable} should be used.
     */
    public static final int INDETERMINATE = -1;

    /** The various states this {@link CircularProgressView} can be in. */
    @IntDef({UiState.RUNNING, UiState.PAUSED, UiState.RETRY})
    @Retention(RetentionPolicy.SOURCE)
    public @interface UiState {
        /** This progress bar will look like it is actively running based on the XML drawable. */
        int RUNNING = 0;

        /** This progress bar will look like it is paused based on the XML drawable. */
        int PAUSED = 1;

        /** This progress bar will look like it is able to be retried based on the XML drawable. */
        int RETRY = 2;
    }

    private static final int MAX_LEVEL = 10000;

    private final Drawable mIndeterminateProgress;
    private final Drawable mDeterminateProgress;
    private final Drawable mResumeButtonSrc;
    private final Drawable mPauseButtonSrc;
    private final Drawable mRetryButtonSrc;

    private final ForegroundDrawableCompat mForegroundHelper;

    /**
     * Creates an instance of a {@link CircularProgressView}.
     * @param context  The {@link Context} to use.
     * @param attrs    An {@link AttributeSet} instance.
     */
    public CircularProgressView(Context context, AttributeSet attrs) {
        super(context, attrs);

        mForegroundHelper = new ForegroundDrawableCompat(this);
        mForegroundHelper.setScaleType(ImageView.ScaleType.FIT_XY);

        TypedArray types =
                attrs == null
                        ? null
                        : context.obtainStyledAttributes(
                                attrs, R.styleable.CircularProgressView, 0, 0);

        mIndeterminateProgress =
                AutoAnimatorDrawable.wrap(
                        UiUtils.getDrawable(
                                context,
                                types,
                                R.styleable.CircularProgressView_indeterminateProgress));
        mDeterminateProgress =
                AutoAnimatorDrawable.wrap(
                        UiUtils.getDrawable(
                                context,
                                types,
                                R.styleable.CircularProgressView_determinateProgress));
        mResumeButtonSrc =
                UiUtils.getDrawable(context, types, R.styleable.CircularProgressView_resumeSrc);
        mPauseButtonSrc =
                UiUtils.getDrawable(context, types, R.styleable.CircularProgressView_pauseSrc);
        mRetryButtonSrc =
                UiUtils.getDrawable(context, types, R.styleable.CircularProgressView_retrySrc);

        if (types != null) types.recycle();
    }

    /**
     * Sets the progress of this {@link CircularProgressView} to {@code progress}.  If {@code
     * progress} is {@link #INDETERMINATE} an indeterminate {@link Drawable} will be used.
     * Otherwise the value will be clamped between 0 and 100 and a determinate {@link Drawable} will
     * be used and have it's level set via {@link Drawable#setLevel(int)}.
     *
     * @param progress The progress value (0 to 100 or {@link #INDETERMINATE}) to show.
     */
    public void setProgress(int progress) {
        if (progress == INDETERMINATE) {
            mForegroundHelper.setDrawable(mIndeterminateProgress);
        } else {
            if (mDeterminateProgress != null) {
                progress = MathUtils.clamp(progress, 0, 100);
                mDeterminateProgress.setLevel(progress * MAX_LEVEL / 100);
            }
            mForegroundHelper.setDrawable(mDeterminateProgress);
        }
    }

    /**
     * The state this {@link CircularProgressView} should show.  This can be one of the three
     * UiStates defined above.  This will determine what the action drawable is in the view.
     * @param state The UiState to use.
     */
    public void setState(@UiState int state) {
        Drawable imageDrawable;
        @StringRes int contentDescription;
        switch (state) {
            case UiState.RUNNING:
                imageDrawable = mPauseButtonSrc;
                contentDescription = R.string.download_notification_pause_button;
                break;
            case UiState.PAUSED:
                imageDrawable = mResumeButtonSrc;
                contentDescription = R.string.download_notification_resume_button;
                break;
            case UiState.RETRY:
            default:
                imageDrawable = mRetryButtonSrc;
                contentDescription = R.string.download_notification_resume_button;
                break;
        }

        setImageDrawable(imageDrawable);
        setContentDescription(getContext().getText(contentDescription));
    }

    // AppCompatImageButton implementation.
    @Override
    public void draw(Canvas canvas) {
        super.draw(canvas);
        mForegroundHelper.draw(canvas);
    }

    @Override
    protected void onVisibilityChanged(View changedView, int visibility) {
        super.onVisibilityChanged(changedView, visibility);
        mForegroundHelper.onVisibilityChanged(changedView, visibility);
    }

    @Override
    protected void drawableStateChanged() {
        super.drawableStateChanged();
        mForegroundHelper.drawableStateChanged();
    }

    @Override
    protected boolean verifyDrawable(Drawable dr) {
        return super.verifyDrawable(dr) || mForegroundHelper.verifyDrawable(dr);
    }
}