GuidedAction.java

/*
 * Copyright (C) 2015 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.leanback.widget;

import android.content.Context;
import android.content.Intent;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.text.InputType;

import androidx.annotation.DrawableRes;
import androidx.annotation.StringRes;
import androidx.core.content.ContextCompat;
import androidx.leanback.R;

import java.util.List;

/**
 * A data class which represents an action within a {@link
 * androidx.leanback.app.GuidedStepFragment}. GuidedActions contain at minimum a title
 * and a description, and typically also an icon.
 * <p>
 * A GuidedAction typically represents a single action a user may take, but may also represent a
 * possible choice out of a group of mutually exclusive choices (similar to radio buttons), or an
 * information-only label (in which case the item cannot be clicked).
 * <p>
 * GuidedActions may optionally be checked. They may also indicate that they will request further
 * user input on selection, in which case they will be displayed with a chevron indicator.
 * <p>
 * GuidedAction recommends to use {@link Builder}. When application subclass GuidedAction, it
 * can subclass {@link BuilderBase}, implement its own builder() method where it should
 * call {@link BuilderBase#applyValues(GuidedAction)}.
 */
public class GuidedAction extends Action {

    private static final String TAG = "GuidedAction";

    /**
     * Special check set Id that is neither checkbox nor radio.
     */
    public static final int NO_CHECK_SET = 0;
    /**
     * Default checkset Id for radio.
     */
    public static final int DEFAULT_CHECK_SET_ID = 1;
    /**
     * Checkset Id for checkbox.
     */
    public static final int CHECKBOX_CHECK_SET_ID = -1;

    /**
     * When finishing editing, goes to next action.
     */
    public static final long ACTION_ID_NEXT = -2;
    /**
     * When finishing editing, stay on current action.
     */
    public static final long ACTION_ID_CURRENT = -3;

    /**
     * Id of standard OK action.
     */
    public static final long ACTION_ID_OK = -4;

    /**
     * Id of standard Cancel action.
     */
    public static final long ACTION_ID_CANCEL = -5;

    /**
     * Id of standard Finish action.
     */
    public static final long ACTION_ID_FINISH = -6;

    /**
     * Id of standard Finish action.
     */
    public static final long ACTION_ID_CONTINUE = -7;

    /**
     * Id of standard Yes action.
     */
    public static final long ACTION_ID_YES = -8;

    /**
     * Id of standard No action.
     */
    public static final long ACTION_ID_NO = -9;

    static final int EDITING_NONE = 0;
    static final int EDITING_TITLE = 1;
    static final int EDITING_DESCRIPTION = 2;
    static final int EDITING_ACTIVATOR_VIEW = 3;

    /**
     * Base builder class to build a {@link GuidedAction} object.  When subclass GuidedAction, you
     * can override this BuilderBase class, implements your build() method which should call
     * {@link #applyValues(GuidedAction)}.  When using GuidedAction directly, use {@link Builder}.
     */
    public abstract static class BuilderBase<B extends BuilderBase> {
        private Context mContext;
        private long mId;
        private CharSequence mTitle;
        private CharSequence mEditTitle;
        private CharSequence mDescription;
        private CharSequence mEditDescription;
        private String[] mAutofillHints;
        private Drawable mIcon;
        /**
         * The mActionFlags holds various action states such as whether title or description are
         * editable, or the action is focusable.
         *
         */
        private int mActionFlags;

        private int mEditable = EDITING_NONE;
        private int mInputType = InputType.TYPE_CLASS_TEXT
                | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS;
        private int mDescriptionInputType = InputType.TYPE_CLASS_TEXT
                | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS;
        private int mEditInputType = InputType.TYPE_CLASS_TEXT;
        private int mDescriptionEditInputType = InputType.TYPE_CLASS_TEXT;
        private int mCheckSetId = NO_CHECK_SET;
        private List<GuidedAction> mSubActions;
        private Intent mIntent;

        /**
         * Creates a BuilderBase for GuidedAction or its subclass.
         * @param context Context object used to build the GuidedAction.
         */
        public BuilderBase(Context context) {
            mContext = context;
            mActionFlags = PF_ENABLED | PF_FOCUSABLE | PF_AUTORESTORE;
        }

        /**
         * Returns Context of this Builder.
         * @return Context of this Builder.
         */
        public Context getContext() {
            return mContext;
        }

        private void setFlags(int flag, int mask) {
            mActionFlags = (mActionFlags & ~mask) | (flag & mask);
        }

        /**
         * Subclass of BuilderBase should call this function to apply values.
         * @param action GuidedAction to apply BuilderBase values.
         */
        protected final void applyValues(GuidedAction action) {
            // Base Action values
            action.setId(mId);
            action.setLabel1(mTitle);
            action.setEditTitle(mEditTitle);
            action.setLabel2(mDescription);
            action.setEditDescription(mEditDescription);
            action.setIcon(mIcon);

            // Subclass values
            action.mIntent = mIntent;
            action.mEditable = mEditable;
            action.mInputType = mInputType;
            action.mDescriptionInputType = mDescriptionInputType;
            action.mAutofillHints = mAutofillHints;
            action.mEditInputType = mEditInputType;
            action.mDescriptionEditInputType = mDescriptionEditInputType;
            action.mActionFlags = mActionFlags;
            action.mCheckSetId = mCheckSetId;
            action.mSubActions = mSubActions;
        }

        /**
         * Construct a clickable action with associated id and auto assign pre-defined title for the
         * action. If the id is not supported, the method simply does nothing.
         * @param id One of {@link GuidedAction#ACTION_ID_OK} {@link GuidedAction#ACTION_ID_CANCEL}
         * {@link GuidedAction#ACTION_ID_FINISH} {@link GuidedAction#ACTION_ID_CONTINUE}
         * {@link GuidedAction#ACTION_ID_YES} {@link GuidedAction#ACTION_ID_NO}.
         * @return The same BuilderBase object.
         */
        public B clickAction(long id) {
            if (id == ACTION_ID_OK) {
                mId = ACTION_ID_OK;
                mTitle = mContext.getString(android.R.string.ok);
            } else if (id == ACTION_ID_CANCEL) {
                mId = ACTION_ID_CANCEL;
                mTitle = mContext.getString(android.R.string.cancel);
            } else if (id == ACTION_ID_FINISH) {
                mId = ACTION_ID_FINISH;
                mTitle = mContext.getString(R.string.lb_guidedaction_finish_title);
            } else if (id == ACTION_ID_CONTINUE) {
                mId = ACTION_ID_CONTINUE;
                mTitle = mContext.getString(R.string.lb_guidedaction_continue_title);
            } else if (id == ACTION_ID_YES) {
                mId = ACTION_ID_YES;
                mTitle = mContext.getString(android.R.string.ok);
            } else if (id == ACTION_ID_NO) {
                mId = ACTION_ID_NO;
                mTitle = mContext.getString(android.R.string.cancel);
            }
            return (B) this;
        }

        /**
         * Sets the ID associated with this action.  The ID can be any value the client wishes;
         * it is typically used to determine what to do when an action is clicked.
         * @param id The ID to associate with this action.
         */
        public B id(long id) {
            mId = id;
            return (B) this;
        }

        /**
         * Sets the title for this action.  The title is typically a short string indicating the
         * action to be taken on click, e.g. "Continue" or "Cancel".
         * @param title The title for this action.
         */
        public B title(CharSequence title) {
            mTitle = title;
            return (B) this;
        }

        /**
         * Sets the title for this action.  The title is typically a short string indicating the
         * action to be taken on click, e.g. "Continue" or "Cancel".
         * @param titleResourceId The resource id of title for this action.
         */
        public B title(@StringRes int titleResourceId) {
            mTitle = getContext().getString(titleResourceId);
            return (B) this;
        }

        /**
         * Sets the optional title text to edit.  When TextView is activated, the edit title
         * replaces the string of title.
         * @param editTitle The optional title text to edit when TextView is activated.
         */
        public B editTitle(CharSequence editTitle) {
            mEditTitle = editTitle;
            return (B) this;
        }

        /**
         * Sets the optional title text to edit.  When TextView is activated, the edit title
         * replaces the string of title.
         * @param editTitleResourceId String resource id of the optional title text to edit when
         * TextView is activated.
         */
        public B editTitle(@StringRes int editTitleResourceId) {
            mEditTitle = getContext().getString(editTitleResourceId);
            return (B) this;
        }

        /**
         * Sets the description for this action.  The description is typically a longer string
         * providing extra information on what the action will do.
         * @param description The description for this action.
         */
        public B description(CharSequence description) {
            mDescription = description;
            return (B) this;
        }

        /**
         * Sets the description for this action.  The description is typically a longer string
         * providing extra information on what the action will do.
         * @param descriptionResourceId String resource id of the description for this action.
         */
        public B description(@StringRes int descriptionResourceId) {
            mDescription = getContext().getString(descriptionResourceId);
            return (B) this;
        }

        /**
         * Sets the optional description text to edit.  When TextView is activated, the edit
         * description replaces the string of description.
         * @param description The description to edit for this action.
         */
        public B editDescription(CharSequence description) {
            mEditDescription = description;
            return (B) this;
        }

        /**
         * Sets the optional description text to edit.  When TextView is activated, the edit
         * description replaces the string of description.
         * @param descriptionResourceId String resource id of the description to edit for this
         * action.
         */
        public B editDescription(@StringRes int descriptionResourceId) {
            mEditDescription = getContext().getString(descriptionResourceId);
            return (B) this;
        }

        /**
         * Sets the intent associated with this action.  Clients would typically fire this intent
         * directly when the action is clicked.
         * @param intent The intent associated with this action.
         */
        public B intent(Intent intent) {
            mIntent = intent;
            return (B) this;
        }

        /**
         * Sets the action's icon drawable.
         * @param icon The drawable for the icon associated with this action.
         */
        public B icon(Drawable icon) {
            mIcon = icon;
            return (B) this;
        }

        /**
         * Sets the action's icon drawable by retrieving it by resource ID from the specified
         * context. This is a convenience function that simply looks up the drawable and calls
         * {@link #icon(Drawable)}.
         * @param iconResourceId The resource ID for the icon associated with this action.
         * @param context The context whose resource ID should be retrieved.
         * @deprecated Use {@link #icon(int)}.
         */
        @Deprecated
        public B iconResourceId(@DrawableRes int iconResourceId, Context context) {
            return icon(ContextCompat.getDrawable(context, iconResourceId));
        }

        /**
         * Sets the action's icon drawable by retrieving it by resource ID from Builder's
         * context. This is a convenience function that simply looks up the drawable and calls
         * {@link #icon(Drawable)}.
         * @param iconResourceId The resource ID for the icon associated with this action.
         */
        public B icon(@DrawableRes int iconResourceId) {
            return icon(ContextCompat.getDrawable(getContext(), iconResourceId));
        }

        /**
         * Indicates whether this action title is editable. Note: Editable actions cannot also be
         * checked, or belong to a check set.
         * @param editable Whether this action is editable.
         */
        public B editable(boolean editable) {
            if (!editable) {
                if (mEditable == EDITING_TITLE) {
                    mEditable = EDITING_NONE;
                }
                return (B) this;
            }
            mEditable = EDITING_TITLE;
            if (isChecked() || mCheckSetId != NO_CHECK_SET) {
                throw new IllegalArgumentException("Editable actions cannot also be checked");
            }
            return (B) this;
        }

        /**
         * Indicates whether this action's description is editable
         * @param editable Whether this action description is editable.
         */
        public B descriptionEditable(boolean editable) {
            if (!editable) {
                if (mEditable == EDITING_DESCRIPTION) {
                    mEditable = EDITING_NONE;
                }
                return (B) this;
            }
            mEditable = EDITING_DESCRIPTION;
            if (isChecked() || mCheckSetId != NO_CHECK_SET) {
                throw new IllegalArgumentException("Editable actions cannot also be checked");
            }
            return (B) this;
        }

        /**
         * Indicates whether this action has a view can be activated to edit, e.g. a DatePicker.
         * @param editable Whether this action has view can be activated to edit.
         */
        public B hasEditableActivatorView(boolean editable) {
            if (!editable) {
                if (mEditable == EDITING_ACTIVATOR_VIEW) {
                    mEditable = EDITING_NONE;
                }
                return (B) this;
            }
            mEditable = EDITING_ACTIVATOR_VIEW;
            if (isChecked() || mCheckSetId != NO_CHECK_SET) {
                throw new IllegalArgumentException("Editable actions cannot also be checked");
            }
            return (B) this;
        }

        /**
         * Sets {@link InputType} of this action title not in editing.
         *
         * @param inputType InputType for the action title not in editing.
         */
        public B inputType(int inputType) {
            mInputType = inputType;
            return (B) this;
        }

        /**
         * Sets {@link InputType} of this action description not in editing.
         *
         * @param inputType InputType for the action description not in editing.
         */
        public B descriptionInputType(int inputType) {
            mDescriptionInputType = inputType;
            return (B) this;
        }


        /**
         * Sets {@link InputType} of this action title in editing.
         *
         * @param inputType InputType for the action title in editing.
         */
        public B editInputType(int inputType) {
            mEditInputType = inputType;
            return (B) this;
        }

        /**
         * Sets {@link InputType} of this action description in editing.
         *
         * @param inputType InputType for the action description in editing.
         */
        public B descriptionEditInputType(int inputType) {
            mDescriptionEditInputType = inputType;
            return (B) this;
        }


        private boolean isChecked() {
            return (mActionFlags & PF_CHECKED) == PF_CHECKED;
        }
        /**
         * Indicates whether this action is initially checked.
         * @param checked Whether this action is checked.
         */
        public B checked(boolean checked) {
            setFlags(checked ? PF_CHECKED : 0, PF_CHECKED);
            if (mEditable != EDITING_NONE) {
                throw new IllegalArgumentException("Editable actions cannot also be checked");
            }
            return (B) this;
        }

        /**
         * Indicates whether this action is part of a single-select group similar to radio buttons
         * or this action is a checkbox. When one item in a check set is checked, all others with
         * the same check set ID will be checked automatically.
         * @param checkSetId The check set ID, or {@link GuidedAction#NO_CHECK_SET} to indicate not
         * radio or checkbox, or {@link GuidedAction#CHECKBOX_CHECK_SET_ID} to indicate a checkbox.
         */
        public B checkSetId(int checkSetId) {
            mCheckSetId = checkSetId;
            if (mEditable != EDITING_NONE) {
                throw new IllegalArgumentException("Editable actions cannot also be in check sets");
            }
            return (B) this;
        }

        /**
         * Indicates whether the title and description are long, and should be displayed
         * appropriately.
         * @param multilineDescription Whether this action has a multiline description.
         */
        public B multilineDescription(boolean multilineDescription) {
            setFlags(multilineDescription ? PF_MULTI_LINE_DESCRIPTION : 0,
                PF_MULTI_LINE_DESCRIPTION);
            return (B) this;
        }

        /**
         * Indicates whether this action has a next state and should display a chevron.
         * @param hasNext Whether this action has a next state.
         */
        public B hasNext(boolean hasNext) {
            setFlags(hasNext ? PF_HAS_NEXT : 0, PF_HAS_NEXT);
            return (B) this;
        }

        /**
         * Indicates whether this action is for information purposes only and cannot be clicked.
         * @param infoOnly Whether this action has a next state.
         */
        public B infoOnly(boolean infoOnly) {
            setFlags(infoOnly ? PF_INFO_ONLY : 0, PF_INFO_ONLY);
            return (B) this;
        }

        /**
         * Indicates whether this action is enabled.  If not enabled, an action cannot be clicked.
         * @param enabled Whether the action is enabled.
         */
        public B enabled(boolean enabled) {
            setFlags(enabled ? PF_ENABLED : 0, PF_ENABLED);
            return (B) this;
        }

        /**
         * Indicates whether this action can take focus.
         * @param focusable
         * @return The same BuilderBase object.
         */
        public B focusable(boolean focusable) {
            setFlags(focusable ? PF_FOCUSABLE : 0, PF_FOCUSABLE);
            return (B) this;
        }

        /**
         * Sets sub actions list.
         * @param subActions
         * @return The same BuilderBase object.
         */
        public B subActions(List<GuidedAction> subActions) {
            mSubActions = subActions;
            return (B) this;
        }

        /**
         * Explicitly sets auto restore feature on the GuidedAction.  It's by default true.
         * @param autoSaveRestoreEnabled True if turn on auto save/restore of GuidedAction content,
         *                                false otherwise.
         * @return The same BuilderBase object.
         * @see GuidedAction#isAutoSaveRestoreEnabled()
         */
        public B autoSaveRestoreEnabled(boolean autoSaveRestoreEnabled) {
            setFlags(autoSaveRestoreEnabled ? PF_AUTORESTORE : 0, PF_AUTORESTORE);
            return (B) this;
        }

        /**
         * Sets autofill hints. See {@link android.view.View#setAutofillHints}
         * @param hints List of hints for autofill.
         * @return The same BuilderBase object.
         */
        public B autofillHints(String... hints) {
            mAutofillHints = hints;
            return (B) this;
        }
    }

    /**
     * Builds a {@link GuidedAction} object.
     */
    public static class Builder extends BuilderBase<Builder> {

        /**
         * @deprecated Use {@link GuidedAction.Builder#GuidedAction.Builder(Context)}.
         */
        @Deprecated
        public Builder() {
            super(null);
        }

        /**
         * Creates a Builder for GuidedAction.
         * @param context Context to build GuidedAction.
         */
        public Builder(Context context) {
            super(context);
        }

        /**
         * Builds the GuidedAction corresponding to this Builder.
         * @return The GuidedAction as configured through this Builder.
         */
        public GuidedAction build() {
            GuidedAction action = new GuidedAction();
            applyValues(action);
            return action;
        }

    }

    static final int PF_CHECKED = 0x00000001;
    static final int PF_MULTI_LINE_DESCRIPTION = 0x00000002;
    static final int PF_HAS_NEXT = 0x00000004;
    static final int PF_INFO_ONLY = 0x00000008;
    static final int PF_ENABLED = 0x00000010;
    static final int PF_FOCUSABLE = 0x00000020;
    static final int PF_AUTORESTORE = 0x00000040;
    int mActionFlags;

    private CharSequence mEditTitle;
    private CharSequence mEditDescription;
    int mEditable;
    int mInputType;
    int mDescriptionInputType;
    int mEditInputType;
    int mDescriptionEditInputType;
    String[] mAutofillHints;

    int mCheckSetId;

    List<GuidedAction> mSubActions;

    Intent mIntent;

    protected GuidedAction() {
        super(0);
    }

    private void setFlags(int flag, int mask) {
        mActionFlags = (mActionFlags & ~mask) | (flag & mask);
    }

    /**
     * Returns the title of this action.
     * @return The title set when this action was built.
     */
    public CharSequence getTitle() {
        return getLabel1();
    }

    /**
     * Sets the title of this action.
     * @param title The title set when this action was built.
     */
    public void setTitle(CharSequence title) {
        setLabel1(title);
    }

    /**
     * Returns the optional title text to edit.  When not null, it is being edited instead of
     * {@link #getTitle()}.
     * @return Optional title text to edit instead of {@link #getTitle()}.
     */
    public CharSequence getEditTitle() {
        return mEditTitle;
    }

    /**
     * Sets the optional title text to edit instead of {@link #setTitle(CharSequence)}.
     * @param editTitle Optional title text to edit instead of {@link #setTitle(CharSequence)}.
     */
    public void setEditTitle(CharSequence editTitle) {
        mEditTitle = editTitle;
    }

    /**
     * Returns the optional description text to edit.  When not null, it is being edited instead of
     * {@link #getDescription()}.
     * @return Optional description text to edit instead of {@link #getDescription()}.
     */
    public CharSequence getEditDescription() {
        return mEditDescription;
    }

    /**
     * Sets the optional description text to edit instead of {@link #setDescription(CharSequence)}.
     * @param editDescription Optional description text to edit instead of
     * {@link #setDescription(CharSequence)}.
     */
    public void setEditDescription(CharSequence editDescription) {
        mEditDescription = editDescription;
    }

    /**
     * Returns true if {@link #getEditTitle()} is not null.  When true, the {@link #getEditTitle()}
     * is being edited instead of {@link #getTitle()}.
     * @return true if {@link #getEditTitle()} is not null.
     */
    public boolean isEditTitleUsed() {
        return mEditTitle != null;
    }

    /**
     * Returns the description of this action.
     * @return The description of this action.
     */
    public CharSequence getDescription() {
        return getLabel2();
    }

    /**
     * Sets the description of this action.
     * @param description The description of the action.
     */
    public void setDescription(CharSequence description) {
        setLabel2(description);
    }

    /**
     * Returns the intent associated with this action.
     * @return The intent set when this action was built.
     */
    public Intent getIntent() {
        return mIntent;
    }

    /**
     * Sets the intent of this action.
     * @param intent New intent to set on this action.
     */
    public void setIntent(Intent intent) {
        mIntent = intent;
    }

    /**
     * Returns whether this action title is editable.
     * @return true if the action title is editable, false otherwise.
     */
    public boolean isEditable() {
        return mEditable == EDITING_TITLE;
    }

    /**
     * Returns whether this action description is editable.
     * @return true if the action description is editable, false otherwise.
     */
    public boolean isDescriptionEditable() {
        return mEditable == EDITING_DESCRIPTION;
    }

    /**
     * Returns if this action has editable title or editable description.
     * @return True if this action has editable title or editable description, false otherwise.
     */
    public boolean hasTextEditable() {
        return mEditable == EDITING_TITLE || mEditable == EDITING_DESCRIPTION;
    }

    /**
     * Returns whether this action can be activated to edit, e.g. a DatePicker.
     * @return true if the action can be activated to edit.
     */
    public boolean hasEditableActivatorView() {
        return mEditable == EDITING_ACTIVATOR_VIEW;
    }

    /**
     * Returns InputType of action title in editing; only valid when {@link #isEditable()} is true.
     * @return InputType of action title in editing.
     */
    public int getEditInputType() {
        return mEditInputType;
    }

    /**
     * Returns InputType of action description in editing; only valid when
     * {@link #isDescriptionEditable()} is true.
     * @return InputType of action description in editing.
     */
    public int getDescriptionEditInputType() {
        return mDescriptionEditInputType;
    }

    /**
     * Returns InputType of action title not in editing.
     * @return InputType of action title not in editing.
     */
    public int getInputType() {
        return mInputType;
    }

    /**
     * Returns InputType of action description not in editing.
     * @return InputType of action description not in editing.
     */
    public int getDescriptionInputType() {
        return mDescriptionInputType;
    }

    /**
     * Returns whether this action is checked.
     * @return true if the action is currently checked, false otherwise.
     */
    public boolean isChecked() {
        return (mActionFlags & PF_CHECKED) == PF_CHECKED;
    }

    /**
     * Sets whether this action is checked.
     * @param checked Whether this action should be checked.
     */
    public void setChecked(boolean checked) {
        setFlags(checked ? PF_CHECKED : 0, PF_CHECKED);
    }

    /**
     * Returns the check set id this action is a part of. All actions in the same list with the same
     * check set id are considered linked. When one of the actions within that set is selected, that
     * action becomes checked, while all the other actions become unchecked.
     *
     * @return an integer representing the check set this action is a part of, or
     *         {@link #CHECKBOX_CHECK_SET_ID} if this is a checkbox, or {@link #NO_CHECK_SET} if
     *         this action is not a checkbox or radiobutton.
     */
    public int getCheckSetId() {
        return mCheckSetId;
    }

    /**
     * Returns whether this action is has a multiline description.
     * @return true if the action was constructed as having a multiline description, false
     * otherwise.
     */
    public boolean hasMultilineDescription() {
        return (mActionFlags & PF_MULTI_LINE_DESCRIPTION) == PF_MULTI_LINE_DESCRIPTION;
    }

    /**
     * Returns whether this action is enabled.
     * @return true if the action is currently enabled, false otherwise.
     */
    public boolean isEnabled() {
        return (mActionFlags & PF_ENABLED) == PF_ENABLED;
    }

    /**
     * Sets whether this action is enabled.
     * @param enabled Whether this action should be enabled.
     */
    public void setEnabled(boolean enabled) {
        setFlags(enabled ? PF_ENABLED : 0, PF_ENABLED);
    }

    /**
     * Returns whether this action is focusable.
     * @return true if the action is currently focusable, false otherwise.
     */
    public boolean isFocusable() {
        return (mActionFlags & PF_FOCUSABLE) == PF_FOCUSABLE;
    }

    /**
     * Sets whether this action is focusable.
     * @param focusable Whether this action should be focusable.
     */
    public void setFocusable(boolean focusable) {
        setFlags(focusable ? PF_FOCUSABLE : 0, PF_FOCUSABLE);
    }

    /**
     * Returns autofill hints, see {@link android.view.View#setAutofillHints(String...)}.
     */
    public String[] getAutofillHints() {
        return mAutofillHints;
    }

    /**
     * Returns whether this action will request further user input when selected, such as showing
     * another GuidedStepFragment or launching a new activity. Configured during construction.
     * @return true if the action will request further user input when selected, false otherwise.
     */
    public boolean hasNext() {
        return (mActionFlags & PF_HAS_NEXT) == PF_HAS_NEXT;
    }

    /**
     * Returns whether the action will only display information and is thus not clickable. If both
     * this and {@link #hasNext()} are true, infoOnly takes precedence. The default is false. For
     * example, this might represent e.g. the amount of storage a document uses, or the cost of an
     * app.
     * @return true if will only display information, false otherwise.
     */
    public boolean infoOnly() {
        return (mActionFlags & PF_INFO_ONLY) == PF_INFO_ONLY;
    }

    /**
     * Change sub actions list.
     * @param actions Sub actions list to set on this action.  Sets null to disable sub actions.
     */
    public void setSubActions(List<GuidedAction> actions) {
        mSubActions = actions;
    }

    /**
     * @return List of sub actions or null if sub actions list is not enabled.
     */
    public List<GuidedAction> getSubActions() {
        return mSubActions;
    }

    /**
     * @return True if has sub actions list, even it's currently empty.
     */
    public boolean hasSubActions() {
        return mSubActions != null;
    }

    /**
     * Returns true if Action will be saved to instanceState and restored later, false otherwise.
     * The default value is true.  When isAutoSaveRestoreEnabled() is true and {@link #getId()} is
     * not {@link #NO_ID}:
     * <li>{@link #isEditable()} is true: save text of {@link #getTitle()}</li>
     * <li>{@link #isDescriptionEditable()} is true: save text of {@link #getDescription()}</li>
     * <li>{@link #getCheckSetId()} is not {@link #NO_CHECK_SET}: save {@link #isChecked()}}</li>
     * <li>{@link GuidedDatePickerAction} will be saved</li>
     * App may explicitly disable auto restore and handle by itself. App should override Fragment
     * onSaveInstanceState() and onCreateActions()
     * @return True if Action will be saved to instanceState and restored later, false otherwise.
     */
    public final boolean isAutoSaveRestoreEnabled() {
        return (mActionFlags & PF_AUTORESTORE) == PF_AUTORESTORE;
    }

    /**
     * Save action into a bundle using a given key. When isAutoRestoreEna() is true:
     * <li>{@link #isEditable()} is true: save text of {@link #getTitle()}</li>
     * <li>{@link #isDescriptionEditable()} is true: save text of {@link #getDescription()}</li>
     * <li>{@link #getCheckSetId()} is not {@link #NO_CHECK_SET}: save {@link #isChecked()}}</li>
     * <li>{@link GuidedDatePickerAction} will be saved</li>
     * Subclass may override this method.
     * @param bundle  Bundle to save the Action.
     * @param key Key used to save the Action.
     */
    public void onSaveInstanceState(Bundle bundle, String key) {
        if (needAutoSaveTitle() && getTitle() != null) {
            bundle.putString(key, getTitle().toString());
        } else if (needAutoSaveDescription() && getDescription() != null) {
            bundle.putString(key, getDescription().toString());
        } else if (getCheckSetId() != NO_CHECK_SET) {
            bundle.putBoolean(key, isChecked());
        }
    }

    /**
     * Restore action from a bundle using a given key. When isAutoRestore() is true:
     * <li>{@link #isEditable()} is true: save text of {@link #getTitle()}</li>
     * <li>{@link #isDescriptionEditable()} is true: save text of {@link #getDescription()}</li>
     * <li>{@link #getCheckSetId()} is not {@link #NO_CHECK_SET}: save {@link #isChecked()}}</li>
     * <li>{@link GuidedDatePickerAction} will be saved</li>
     * Subclass may override this method.
     * @param bundle  Bundle to restore the Action from.
     * @param key Key used to restore the Action.
     */
    public void onRestoreInstanceState(Bundle bundle, String key) {
        if (needAutoSaveTitle()) {
            String title = bundle.getString(key);
            if (title != null) {
                setTitle(title);
            }
        } else if (needAutoSaveDescription()) {
            String description = bundle.getString(key);
            if (description != null) {
                setDescription(description);
            }
        } else if (getCheckSetId() != NO_CHECK_SET) {
            setChecked(bundle.getBoolean(key, isChecked()));
        }
    }

    static boolean isPasswordVariant(int inputType) {
        final int variation = inputType & InputType.TYPE_MASK_VARIATION;
        return variation == InputType.TYPE_TEXT_VARIATION_PASSWORD
                || variation == InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD
                || variation == InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD;
    }

    final boolean needAutoSaveTitle() {
        return isEditable() && !isPasswordVariant(getEditInputType());
    }

    final boolean needAutoSaveDescription() {
        return isDescriptionEditable() && !isPasswordVariant(getDescriptionEditInputType());
    }

}