/*
 * Copyright (C) 2018 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 android.view.contentcapture;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SystemApi;
import android.annotation.TestApi;
import android.app.assist.AssistStructure;
import android.graphics.Matrix;
import android.graphics.Rect;
import android.os.Bundle;
import android.os.LocaleList;
import android.os.Parcel;
import android.text.TextUtils;
import android.util.Log;
import android.view.View;
import android.view.ViewParent;
import android.view.ViewStructure;
import android.view.ViewStructure.HtmlInfo.Builder;
import android.view.autofill.AutofillId;
import android.view.autofill.AutofillValue;

import java.util.Objects;

//TODO(b/122484602): add javadocs / implement Parcelable / implement
//TODO(b/122484602): for now it's extending ViewNode directly as it needs most of its properties,
// but it might be better to create a common, abstract android.view.ViewNode class that both extend
// instead
/** @hide */
@SystemApi
public final class ViewNode extends AssistStructure.ViewNode {

    private static final String TAG = ViewNode.class.getSimpleName();

    private static final long FLAGS_HAS_TEXT = 1L << 0;
    private static final long FLAGS_HAS_COMPLEX_TEXT = 1L << 1;
    private static final long FLAGS_VISIBILITY_MASK = View.VISIBLE | View.INVISIBLE | View.GONE;
    private static final long FLAGS_HAS_CLASSNAME = 1L << 4;
    private static final long FLAGS_HAS_AUTOFILL_ID = 1L << 5;
    private static final long FLAGS_HAS_AUTOFILL_PARENT_ID = 1L << 6;
    private static final long FLAGS_HAS_ID = 1L << 7;
    private static final long FLAGS_HAS_LARGE_COORDS = 1L << 8;
    private static final long FLAGS_HAS_SCROLL = 1L << 9;
    private static final long FLAGS_ASSIST_BLOCKED = 1L << 10;
    private static final long FLAGS_DISABLED = 1L << 11;
    private static final long FLAGS_CLICKABLE = 1L << 12;
    private static final long FLAGS_LONG_CLICKABLE = 1L << 13;
    private static final long FLAGS_CONTEXT_CLICKABLE = 1L << 14;
    private static final long FLAGS_FOCUSABLE = 1L << 15;
    private static final long FLAGS_FOCUSED = 1L << 16;
    private static final long FLAGS_ACCESSIBILITY_FOCUSED = 1L << 17;
    private static final long FLAGS_CHECKABLE = 1L << 18;
    private static final long FLAGS_CHECKED = 1L << 19;
    private static final long FLAGS_SELECTED = 1L << 20;
    private static final long FLAGS_ACTIVATED = 1L << 21;
    private static final long FLAGS_OPAQUE = 1L << 22;
    private static final long FLAGS_HAS_CONTENT_DESCRIPTION = 1L << 23;
    private static final long FLAGS_HAS_EXTRAS = 1L << 24;
    private static final long FLAGS_HAS_LOCALE_LIST = 1L << 25;
    private static final long FLAGS_HAS_INPUT_TYPE = 1L << 26;
    private static final long FLAGS_HAS_MIN_TEXT_EMS = 1L << 27;
    private static final long FLAGS_HAS_MAX_TEXT_EMS = 1L << 28;
    private static final long FLAGS_HAS_MAX_TEXT_LENGTH = 1L << 29;
    private static final long FLAGS_HAS_TEXT_ID_ENTRY = 1L << 30;
    private static final long FLAGS_HAS_AUTOFILL_TYPE = 1L << 31;
    private static final long FLAGS_HAS_AUTOFILL_VALUE = 1L << 32;
    private static final long FLAGS_HAS_AUTOFILL_HINTS = 1L << 33;
    private static final long FLAGS_HAS_AUTOFILL_OPTIONS = 1L << 34;
    private static final long FLAGS_HAS_HINT_ID_ENTRY = 1L << 35;
    private static final long FLAGS_HAS_MIME_TYPES = 1L << 36;

    /** Flags used to optimize what's written to the parcel */
    private long mFlags;

    private AutofillId mParentAutofillId;

    private AutofillId mAutofillId;
    private ViewNodeText mText;
    private String mClassName;
    private int mId = View.NO_ID;
    private String mIdPackage;
    private String mIdType;
    private String mIdEntry;
    private int mX;
    private int mY;
    private int mScrollX;
    private int mScrollY;
    private int mWidth;
    private int mHeight;
    private CharSequence mContentDescription;
    private Bundle mExtras;
    private LocaleList mLocaleList;
    private int mInputType;
    private int mMinEms = -1;
    private int mMaxEms = -1;
    private int mMaxLength = -1;
    private String mTextIdEntry;
    private String mHintIdEntry;
    private @View.AutofillType int mAutofillType = View.AUTOFILL_TYPE_NONE;
    private String[] mAutofillHints;
    private AutofillValue mAutofillValue;
    private CharSequence[] mAutofillOptions;
    private String[] mReceiveContentMimeTypes;

    /** @hide */
    public ViewNode() {
    }

    private ViewNode(long nodeFlags, @NonNull Parcel parcel) {
        mFlags = nodeFlags;

        if ((nodeFlags & FLAGS_HAS_AUTOFILL_ID) != 0) {
            mAutofillId = parcel.readParcelable(null, android.view.autofill.AutofillId.class);
        }
        if ((nodeFlags & FLAGS_HAS_AUTOFILL_PARENT_ID) != 0) {
            mParentAutofillId = parcel.readParcelable(null, android.view.autofill.AutofillId.class);
        }
        if ((nodeFlags & FLAGS_HAS_TEXT) != 0) {
            mText = new ViewNodeText(parcel, (nodeFlags & FLAGS_HAS_COMPLEX_TEXT) == 0);
        }
        if ((nodeFlags & FLAGS_HAS_CLASSNAME) != 0) {
            mClassName = parcel.readString();
        }
        if ((nodeFlags & FLAGS_HAS_ID) != 0) {
            mId = parcel.readInt();
            if (mId != View.NO_ID) {
                mIdEntry = parcel.readString();
                if (mIdEntry != null) {
                    mIdType = parcel.readString();
                    mIdPackage = parcel.readString();
                }
            }
        }
        if ((nodeFlags & FLAGS_HAS_LARGE_COORDS) != 0) {
            mX = parcel.readInt();
            mY = parcel.readInt();
            mWidth = parcel.readInt();
            mHeight = parcel.readInt();
        } else {
            int val = parcel.readInt();
            mX = val & 0x7fff;
            mY = (val >> 16) & 0x7fff;
            val = parcel.readInt();
            mWidth = val & 0x7fff;
            mHeight = (val >> 16) & 0x7fff;
        }
        if ((nodeFlags & FLAGS_HAS_SCROLL) != 0) {
            mScrollX = parcel.readInt();
            mScrollY = parcel.readInt();
        }
        if ((nodeFlags & FLAGS_HAS_CONTENT_DESCRIPTION) != 0) {
            mContentDescription = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(parcel);
        }
        if ((nodeFlags & FLAGS_HAS_EXTRAS) != 0) {
            mExtras = parcel.readBundle();
        }
        if ((nodeFlags & FLAGS_HAS_LOCALE_LIST) != 0) {
            mLocaleList = parcel.readParcelable(null, android.os.LocaleList.class);
        }
        if ((nodeFlags & FLAGS_HAS_MIME_TYPES) != 0) {
            mReceiveContentMimeTypes = parcel.readStringArray();
        }
        if ((nodeFlags & FLAGS_HAS_INPUT_TYPE) != 0) {
            mInputType = parcel.readInt();
        }
        if ((nodeFlags & FLAGS_HAS_MIN_TEXT_EMS) != 0) {
            mMinEms = parcel.readInt();
        }
        if ((nodeFlags & FLAGS_HAS_MAX_TEXT_EMS) != 0) {
            mMaxEms = parcel.readInt();
        }
        if ((nodeFlags & FLAGS_HAS_MAX_TEXT_LENGTH) != 0) {
            mMaxLength = parcel.readInt();
        }
        if ((nodeFlags & FLAGS_HAS_TEXT_ID_ENTRY) != 0) {
            mTextIdEntry = parcel.readString();
        }
        if ((nodeFlags & FLAGS_HAS_AUTOFILL_TYPE) != 0) {
            mAutofillType = parcel.readInt();
        }
        if ((nodeFlags & FLAGS_HAS_AUTOFILL_HINTS) != 0) {
            mAutofillHints = parcel.readStringArray();
        }
        if ((nodeFlags & FLAGS_HAS_AUTOFILL_VALUE) != 0) {
            mAutofillValue = parcel.readParcelable(null, android.view.autofill.AutofillValue.class);
        }
        if ((nodeFlags & FLAGS_HAS_AUTOFILL_OPTIONS) != 0) {
            mAutofillOptions = parcel.readCharSequenceArray();
        }
        if ((nodeFlags & FLAGS_HAS_HINT_ID_ENTRY) != 0) {
            mHintIdEntry = parcel.readString();
        }
    }

    /**
     * Returns the {@link AutofillId} of this view's parent, if the parent is also part of the
     * screen observation tree.
     */
    @Nullable
    public AutofillId getParentAutofillId() {
        return mParentAutofillId;
    }

    @Nullable
    @Override
    public AutofillId getAutofillId() {
        return mAutofillId;
    }

    @Nullable
    @Override
    public CharSequence getText() {
        return mText != null ? mText.mText : null;
    }

    @Nullable
    @Override
    public String getClassName() {
        return mClassName;
    }

    @Override
    public int getId() {
        return mId;
    }

    @Nullable
    @Override
    public String getIdPackage() {
        return mIdPackage;
    }

    @Nullable
    @Override
    public String getIdType() {
        return mIdType;
    }

    @Nullable
    @Override
    public String getIdEntry() {
        return mIdEntry;
    }

    @Override
    public int getLeft() {
        return mX;
    }

    @Override
    public int getTop() {
        return mY;
    }

    @Override
    public int getScrollX() {
        return mScrollX;
    }

    @Override
    public int getScrollY() {
        return mScrollY;
    }

    @Override
    public int getWidth() {
        return mWidth;
    }

    @Override
    public int getHeight() {
        return mHeight;
    }

    @Override
    public boolean isAssistBlocked() {
        return (mFlags & FLAGS_ASSIST_BLOCKED) != 0;
    }

    @Override
    public boolean isEnabled() {
        return (mFlags & FLAGS_DISABLED) == 0;
    }

    @Override
    public boolean isClickable() {
        return (mFlags & FLAGS_CLICKABLE) != 0;
    }

    @Override
    public boolean isLongClickable() {
        return (mFlags & FLAGS_LONG_CLICKABLE) != 0;
    }

    @Override
    public boolean isContextClickable() {
        return (mFlags & FLAGS_CONTEXT_CLICKABLE) != 0;
    }

    @Override
    public boolean isFocusable() {
        return (mFlags & FLAGS_FOCUSABLE) != 0;
    }

    @Override
    public boolean isFocused() {
        return (mFlags & FLAGS_FOCUSED) != 0;
    }

    @Override
    public boolean isAccessibilityFocused() {
        return (mFlags & FLAGS_ACCESSIBILITY_FOCUSED) != 0;
    }

    @Override
    public boolean isCheckable() {
        return (mFlags & FLAGS_CHECKABLE) != 0;
    }

    @Override
    public boolean isChecked() {
        return (mFlags & FLAGS_CHECKED) != 0;
    }

    @Override
    public boolean isSelected() {
        return (mFlags & FLAGS_SELECTED) != 0;
    }

    @Override
    public boolean isActivated() {
        return (mFlags & FLAGS_ACTIVATED) != 0;
    }

    @Override
    public boolean isOpaque() {
        return (mFlags & FLAGS_OPAQUE) != 0;
    }

    @Nullable
    @Override
    public CharSequence getContentDescription() {
        return mContentDescription;
    }

    @Nullable
    @Override
    public Bundle getExtras() {
        return mExtras;
    }

    @Nullable
    @Override
    public String getHint() {
        return mText != null ? mText.mHint : null;
    }

    @Nullable
    @Override
    public String getHintIdEntry() {
        return mHintIdEntry;
    }

    @Override
    public int getTextSelectionStart() {
        return mText != null ? mText.mTextSelectionStart : -1;
    }

    @Override
    public int getTextSelectionEnd() {
        return mText != null ? mText.mTextSelectionEnd : -1;
    }

    @Override
    public int getTextColor() {
        return mText != null ? mText.mTextColor : TEXT_COLOR_UNDEFINED;
    }

    @Override
    public int getTextBackgroundColor() {
        return mText != null ? mText.mTextBackgroundColor : TEXT_COLOR_UNDEFINED;
    }

    @Override
    public float getTextSize() {
        return mText != null ? mText.mTextSize : 0;
    }

    @Override
    public int getTextStyle() {
        return mText != null ? mText.mTextStyle : 0;
    }

    @Nullable
    @Override
    public int[] getTextLineCharOffsets() {
        return mText != null ? mText.mLineCharOffsets : null;
    }

    @Nullable
    @Override
    public int[] getTextLineBaselines() {
        return mText != null ? mText.mLineBaselines : null;
    }

    @Override
    public int getVisibility() {
        return (int) (mFlags & FLAGS_VISIBILITY_MASK);
    }

    @Override
    public int getInputType() {
        return mInputType;
    }

    @Override
    public int getMinTextEms() {
        return mMinEms;
    }

    @Override
    public int getMaxTextEms() {
        return mMaxEms;
    }

    @Override
    public int getMaxTextLength() {
        return mMaxLength;
    }

    @Nullable
    @Override
    public String getTextIdEntry() {
        return mTextIdEntry;
    }

    @Override
    public @View.AutofillType int getAutofillType() {
        return mAutofillType;
    }

    @Override
    @Nullable public String[] getAutofillHints() {
        return mAutofillHints;
    }

    @Override
    @Nullable public AutofillValue getAutofillValue() {
        return mAutofillValue;
    }

    @Override
    @Nullable public CharSequence[] getAutofillOptions() {
        return mAutofillOptions;
    }

    @Override
    @Nullable
    public String[] getReceiveContentMimeTypes() {
        return mReceiveContentMimeTypes;
    }

    @Nullable
    @Override
    public LocaleList getLocaleList() {
        return mLocaleList;
    }

    private void writeSelfToParcel(@NonNull Parcel parcel, int parcelFlags) {
        long nodeFlags = mFlags;

        if (mAutofillId != null) {
            nodeFlags |= FLAGS_HAS_AUTOFILL_ID;
        }

        if (mParentAutofillId != null) {
            nodeFlags |= FLAGS_HAS_AUTOFILL_PARENT_ID;
        }

        if (mText != null) {
            nodeFlags |= FLAGS_HAS_TEXT;
            if (!mText.isSimple()) {
                nodeFlags |= FLAGS_HAS_COMPLEX_TEXT;
            }
        }
        if (mClassName != null) {
            nodeFlags |= FLAGS_HAS_CLASSNAME;
        }
        if (mId != View.NO_ID) {
            nodeFlags |= FLAGS_HAS_ID;
        }
        if ((mX & ~0x7fff) != 0 || (mY & ~0x7fff) != 0
                || (mWidth & ~0x7fff) != 0 | (mHeight & ~0x7fff) != 0) {
            nodeFlags |= FLAGS_HAS_LARGE_COORDS;
        }
        if (mScrollX != 0 || mScrollY != 0) {
            nodeFlags |= FLAGS_HAS_SCROLL;
        }
        if (mContentDescription != null) {
            nodeFlags |= FLAGS_HAS_CONTENT_DESCRIPTION;
        }
        if (mExtras != null) {
            nodeFlags |= FLAGS_HAS_EXTRAS;
        }
        if (mLocaleList != null) {
            nodeFlags |= FLAGS_HAS_LOCALE_LIST;
        }
        if (mReceiveContentMimeTypes != null) {
            nodeFlags |= FLAGS_HAS_MIME_TYPES;
        }
        if (mInputType != 0) {
            nodeFlags |= FLAGS_HAS_INPUT_TYPE;
        }
        if (mMinEms > -1) {
            nodeFlags |= FLAGS_HAS_MIN_TEXT_EMS;
        }
        if (mMaxEms > -1) {
            nodeFlags |= FLAGS_HAS_MAX_TEXT_EMS;
        }
        if (mMaxLength > -1) {
            nodeFlags |= FLAGS_HAS_MAX_TEXT_LENGTH;
        }
        if (mTextIdEntry != null) {
            nodeFlags |= FLAGS_HAS_TEXT_ID_ENTRY;
        }
        if (mAutofillValue != null) {
            nodeFlags |= FLAGS_HAS_AUTOFILL_VALUE;
        }
        if (mAutofillType != View.AUTOFILL_TYPE_NONE) {
            nodeFlags |= FLAGS_HAS_AUTOFILL_TYPE;
        }
        if (mAutofillHints != null) {
            nodeFlags |= FLAGS_HAS_AUTOFILL_HINTS;
        }
        if (mAutofillOptions != null) {
            nodeFlags |= FLAGS_HAS_AUTOFILL_OPTIONS;
        }
        if (mHintIdEntry != null) {
            nodeFlags |= FLAGS_HAS_HINT_ID_ENTRY;
        }
        parcel.writeLong(nodeFlags);

        if ((nodeFlags & FLAGS_HAS_AUTOFILL_ID) != 0) {
            parcel.writeParcelable(mAutofillId, parcelFlags);
        }
        if ((nodeFlags & FLAGS_HAS_AUTOFILL_PARENT_ID) != 0) {
            parcel.writeParcelable(mParentAutofillId, parcelFlags);
        }
        if ((nodeFlags & FLAGS_HAS_TEXT) != 0) {
            mText.writeToParcel(parcel, (nodeFlags & FLAGS_HAS_COMPLEX_TEXT) == 0);
        }
        if ((nodeFlags & FLAGS_HAS_CLASSNAME) != 0) {
            parcel.writeString(mClassName);
        }
        if ((nodeFlags & FLAGS_HAS_ID) != 0) {
            parcel.writeInt(mId);
            if (mId != View.NO_ID) {
                parcel.writeString(mIdEntry);
                if (mIdEntry != null) {
                    parcel.writeString(mIdType);
                    parcel.writeString(mIdPackage);
                }
            }
        }
        if ((nodeFlags & FLAGS_HAS_LARGE_COORDS) != 0) {
            parcel.writeInt(mX);
            parcel.writeInt(mY);
            parcel.writeInt(mWidth);
            parcel.writeInt(mHeight);
        } else {
            parcel.writeInt((mY << 16) | mX);
            parcel.writeInt((mHeight << 16) | mWidth);
        }
        if ((nodeFlags & FLAGS_HAS_SCROLL) != 0) {
            parcel.writeInt(mScrollX);
            parcel.writeInt(mScrollY);
        }
        if ((nodeFlags & FLAGS_HAS_CONTENT_DESCRIPTION) != 0) {
            TextUtils.writeToParcel(mContentDescription, parcel, 0);
        }
        if ((nodeFlags & FLAGS_HAS_EXTRAS) != 0) {
            parcel.writeBundle(mExtras);
        }
        if ((nodeFlags & FLAGS_HAS_LOCALE_LIST) != 0) {
            parcel.writeParcelable(mLocaleList, 0);
        }
        if ((nodeFlags & FLAGS_HAS_MIME_TYPES) != 0) {
            parcel.writeStringArray(mReceiveContentMimeTypes);
        }
        if ((nodeFlags & FLAGS_HAS_INPUT_TYPE) != 0) {
            parcel.writeInt(mInputType);
        }
        if ((nodeFlags & FLAGS_HAS_MIN_TEXT_EMS) != 0) {
            parcel.writeInt(mMinEms);
        }
        if ((nodeFlags & FLAGS_HAS_MAX_TEXT_EMS) != 0) {
            parcel.writeInt(mMaxEms);
        }
        if ((nodeFlags & FLAGS_HAS_MAX_TEXT_LENGTH) != 0) {
            parcel.writeInt(mMaxLength);
        }
        if ((nodeFlags & FLAGS_HAS_TEXT_ID_ENTRY) != 0) {
            parcel.writeString(mTextIdEntry);
        }
        if ((nodeFlags & FLAGS_HAS_AUTOFILL_TYPE) != 0) {
            parcel.writeInt(mAutofillType);
        }
        if ((nodeFlags & FLAGS_HAS_AUTOFILL_HINTS) != 0) {
            parcel.writeStringArray(mAutofillHints);
        }
        if ((nodeFlags & FLAGS_HAS_AUTOFILL_VALUE) != 0) {
            parcel.writeParcelable(mAutofillValue, 0);
        }
        if ((nodeFlags & FLAGS_HAS_AUTOFILL_OPTIONS) != 0) {
            parcel.writeCharSequenceArray(mAutofillOptions);
        }
        if ((nodeFlags & FLAGS_HAS_HINT_ID_ENTRY) != 0) {
            parcel.writeString(mHintIdEntry);
        }
    }

    /** @hide */
    @TestApi
    public static void writeToParcel(@NonNull Parcel parcel, @Nullable ViewNode node, int flags) {
        if (node == null) {
            parcel.writeLong(0);
        } else {
            node.writeSelfToParcel(parcel, flags);
        }
    }

    /** @hide */
    @TestApi
    public static @Nullable ViewNode readFromParcel(@NonNull Parcel parcel) {
        final long nodeFlags = parcel.readLong();
        return nodeFlags == 0 ? null : new ViewNode(nodeFlags, parcel);
    }

    /** @hide */
    @TestApi
    public static final class ViewStructureImpl extends ViewStructure {

        final ViewNode mNode = new ViewNode();

        /** @hide */
        @TestApi
        public ViewStructureImpl(@NonNull View view) {
            mNode.mAutofillId = Objects.requireNonNull(view).getAutofillId();
            final ViewParent parent = view.getParent();
            if (parent instanceof View) {
                mNode.mParentAutofillId = ((View) parent).getAutofillId();
            }
        }

        /** @hide */
        @TestApi
        public ViewStructureImpl(@NonNull AutofillId parentId, long virtualId, int sessionId) {
            mNode.mParentAutofillId = Objects.requireNonNull(parentId);
            mNode.mAutofillId = new AutofillId(parentId, virtualId, sessionId);
        }

        /** @hide */
        @TestApi
        public ViewNode getNode() {
            return mNode;
        }

        @Override
        public void setId(int id, String packageName, String typeName, String entryName) {
            mNode.mId = id;
            mNode.mIdPackage = packageName;
            mNode.mIdType = typeName;
            mNode.mIdEntry = entryName;
        }

        @Override
        public void setDimens(int left, int top, int scrollX, int scrollY, int width, int height) {
            mNode.mX = left;
            mNode.mY = top;
            mNode.mScrollX = scrollX;
            mNode.mScrollY = scrollY;
            mNode.mWidth = width;
            mNode.mHeight = height;
        }

        @Override
        public void setTransformation(Matrix matrix) {
            Log.w(TAG, "setTransformation() is not supported");
        }

        @Override
        public void setElevation(float elevation) {
            Log.w(TAG, "setElevation() is not supported");
        }

        @Override
        public void setAlpha(float alpha) {
            Log.w(TAG, "setAlpha() is not supported");
        }

        @Override
        public void setVisibility(int visibility) {
            mNode.mFlags = (mNode.mFlags & ~FLAGS_VISIBILITY_MASK)
                    | (visibility & FLAGS_VISIBILITY_MASK);
        }

        @Override
        public void setAssistBlocked(boolean state) {
            mNode.mFlags = (mNode.mFlags & ~FLAGS_ASSIST_BLOCKED)
                    | (state ? FLAGS_ASSIST_BLOCKED : 0);
        }

        @Override
        public void setEnabled(boolean state) {
            mNode.mFlags = (mNode.mFlags & ~FLAGS_DISABLED) | (state ? 0 : FLAGS_DISABLED);
        }

        @Override
        public void setClickable(boolean state) {
            mNode.mFlags = (mNode.mFlags & ~FLAGS_CLICKABLE) | (state ? FLAGS_CLICKABLE : 0);
        }

        @Override
        public void setLongClickable(boolean state) {
            mNode.mFlags = (mNode.mFlags & ~FLAGS_LONG_CLICKABLE)
                    | (state ? FLAGS_LONG_CLICKABLE : 0);
        }

        @Override
        public void setContextClickable(boolean state) {
            mNode.mFlags = (mNode.mFlags & ~FLAGS_CONTEXT_CLICKABLE)
                    | (state ? FLAGS_CONTEXT_CLICKABLE : 0);
        }

        @Override
        public void setFocusable(boolean state) {
            mNode.mFlags = (mNode.mFlags & ~FLAGS_FOCUSABLE) | (state ? FLAGS_FOCUSABLE : 0);
        }

        @Override
        public void setFocused(boolean state) {
            mNode.mFlags = (mNode.mFlags & ~FLAGS_FOCUSED) | (state ? FLAGS_FOCUSED : 0);
        }

        @Override
        public void setAccessibilityFocused(boolean state) {
            mNode.mFlags = (mNode.mFlags & ~FLAGS_ACCESSIBILITY_FOCUSED)
                    | (state ? FLAGS_ACCESSIBILITY_FOCUSED : 0);
        }

        @Override
        public void setCheckable(boolean state) {
            mNode.mFlags = (mNode.mFlags & ~FLAGS_CHECKABLE) | (state ? FLAGS_CHECKABLE : 0);
        }

        @Override
        public void setChecked(boolean state) {
            mNode.mFlags = (mNode.mFlags & ~FLAGS_CHECKED) | (state ? FLAGS_CHECKED : 0);
        }

        @Override
        public void setSelected(boolean state) {
            mNode.mFlags = (mNode.mFlags & ~FLAGS_SELECTED) | (state ? FLAGS_SELECTED : 0);
        }

        @Override
        public void setActivated(boolean state) {
            mNode.mFlags = (mNode.mFlags & ~FLAGS_ACTIVATED) | (state ? FLAGS_ACTIVATED : 0);
        }

        @Override
        public void setOpaque(boolean opaque) {
            mNode.mFlags = (mNode.mFlags & ~FLAGS_OPAQUE) | (opaque ? FLAGS_OPAQUE : 0);
        }

        @Override
        public void setClassName(String className) {
            mNode.mClassName = className;
        }

        @Override
        public void setContentDescription(CharSequence contentDescription) {
            mNode.mContentDescription = contentDescription;
        }

        @Override
        public void setText(CharSequence text) {
            final ViewNodeText t = getNodeText();
            t.mText = TextUtils.trimNoCopySpans(text);
            t.mTextSelectionStart = t.mTextSelectionEnd = -1;
        }

        @Override
        public void setText(CharSequence text, int selectionStart, int selectionEnd) {
            final ViewNodeText t = getNodeText();
            t.mText = TextUtils.trimNoCopySpans(text);
            t.mTextSelectionStart = selectionStart;
            t.mTextSelectionEnd = selectionEnd;
        }

        @Override
        public void setTextStyle(float size, int fgColor, int bgColor, int style) {
            final ViewNodeText t = getNodeText();
            t.mTextColor = fgColor;
            t.mTextBackgroundColor = bgColor;
            t.mTextSize = size;
            t.mTextStyle = style;
        }

        @Override
        public void setTextLines(int[] charOffsets, int[] baselines) {
            final ViewNodeText t = getNodeText();
            t.mLineCharOffsets = charOffsets;
            t.mLineBaselines = baselines;
        }

        @Override
        public void setTextIdEntry(@NonNull String entryName) {
            mNode.mTextIdEntry = Objects.requireNonNull(entryName);
        }

        @Override
        public void setHint(CharSequence hint) {
            getNodeText().mHint = hint != null ? hint.toString() : null;
        }

        @Override
        public void setHintIdEntry(String entryName) {
            mNode.mHintIdEntry = Objects.requireNonNull(entryName);
        }

        @Override
        public CharSequence getText() {
            return mNode.getText();
        }

        @Override
        public int getTextSelectionStart() {
            return mNode.getTextSelectionStart();
        }

        @Override
        public int getTextSelectionEnd() {
            return mNode.getTextSelectionEnd();
        }

        @Override
        public CharSequence getHint() {
            return mNode.getHint();
        }

        @Override
        public Bundle getExtras() {
            if (mNode.mExtras != null) {
                return mNode.mExtras;
            }
            mNode.mExtras = new Bundle();
            return mNode.mExtras;
        }

        @Override
        public boolean hasExtras() {
            return mNode.mExtras != null;
        }

        @Override
        public void setChildCount(int num) {
            Log.w(TAG, "setChildCount() is not supported");
        }

        @Override
        public int addChildCount(int num) {
            Log.w(TAG, "addChildCount() is not supported");
            return 0;
        }

        @Override
        public int getChildCount() {
            Log.w(TAG, "getChildCount() is not supported");
            return 0;
        }

        @Override
        public ViewStructure newChild(int index) {
            Log.w(TAG, "newChild() is not supported");
            return null;
        }

        @Override
        public ViewStructure asyncNewChild(int index) {
            Log.w(TAG, "asyncNewChild() is not supported");
            return null;
        }

        @Override
        public AutofillId getAutofillId() {
            return mNode.mAutofillId;
        }

        @Override
        public void setAutofillId(AutofillId id) {
            mNode.mAutofillId = Objects.requireNonNull(id);
        }


        @Override
        public void setAutofillId(AutofillId parentId, int virtualId) {
            mNode.mParentAutofillId = Objects.requireNonNull(parentId);
            mNode.mAutofillId = new AutofillId(parentId, virtualId);
        }

        @Override
        public void setAutofillType(@View.AutofillType int type) {
            mNode.mAutofillType = type;
        }

        @Override
        public void setReceiveContentMimeTypes(@Nullable String[] mimeTypes) {
            mNode.mReceiveContentMimeTypes = mimeTypes;
        }

        @Override
        public void setAutofillHints(String[] hints) {
            mNode.mAutofillHints = hints;
        }

        @Override
        public void setAutofillValue(AutofillValue value) {
            mNode.mAutofillValue = value;
        }

        @Override
        public void setAutofillOptions(CharSequence[] options) {
            mNode.mAutofillOptions = options;
        }

        @Override
        public void setInputType(int inputType) {
            mNode.mInputType = inputType;
        }

        @Override
        public void setMinTextEms(int minEms) {
            mNode.mMinEms = minEms;
        }

        @Override
        public void setMaxTextEms(int maxEms) {
            mNode.mMaxEms = maxEms;
        }

        @Override
        public void setMaxTextLength(int maxLength) {
            mNode.mMaxLength = maxLength;
        }

        @Override
        public void setDataIsSensitive(boolean sensitive) {
            Log.w(TAG, "setDataIsSensitive() is not supported");
        }

        @Override
        public void asyncCommit() {
            Log.w(TAG, "asyncCommit() is not supported");
        }

        @Override
        public Rect getTempRect() {
            Log.w(TAG, "getTempRect() is not supported");
            return null;
        }

        @Override
        public void setWebDomain(String domain) {
            Log.w(TAG, "setWebDomain() is not supported");
        }

        @Override
        public void setLocaleList(LocaleList localeList) {
            mNode.mLocaleList = localeList;
        }

        @Override
        public Builder newHtmlInfoBuilder(String tagName) {
            Log.w(TAG, "newHtmlInfoBuilder() is not supported");
            return null;
        }

        @Override
        public void setHtmlInfo(HtmlInfo htmlInfo) {
            Log.w(TAG, "setHtmlInfo() is not supported");
        }

        private ViewNodeText getNodeText() {
            if (mNode.mText != null) {
                return mNode.mText;
            }
            mNode.mText = new ViewNodeText();
            return mNode.mText;
        }
    }

    //TODO(b/122484602): copied 'as-is' from AssistStructure, except for writeSensitive
    static final class ViewNodeText {
        CharSequence mText;
        float mTextSize;
        int mTextStyle;
        int mTextColor = ViewNode.TEXT_COLOR_UNDEFINED;
        int mTextBackgroundColor = ViewNode.TEXT_COLOR_UNDEFINED;
        int mTextSelectionStart;
        int mTextSelectionEnd;
        int[] mLineCharOffsets;
        int[] mLineBaselines;
        String mHint;

        ViewNodeText() {
        }

        boolean isSimple() {
            return mTextBackgroundColor == ViewNode.TEXT_COLOR_UNDEFINED
                    && mTextSelectionStart == 0 && mTextSelectionEnd == 0
                    && mLineCharOffsets == null && mLineBaselines == null && mHint == null;
        }

        ViewNodeText(Parcel in, boolean simple) {
            mText = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
            mTextSize = in.readFloat();
            mTextStyle = in.readInt();
            mTextColor = in.readInt();
            if (!simple) {
                mTextBackgroundColor = in.readInt();
                mTextSelectionStart = in.readInt();
                mTextSelectionEnd = in.readInt();
                mLineCharOffsets = in.createIntArray();
                mLineBaselines = in.createIntArray();
                mHint = in.readString();
            }
        }

        void writeToParcel(Parcel out, boolean simple) {
            TextUtils.writeToParcel(mText, out, 0);
            out.writeFloat(mTextSize);
            out.writeInt(mTextStyle);
            out.writeInt(mTextColor);
            if (!simple) {
                out.writeInt(mTextBackgroundColor);
                out.writeInt(mTextSelectionStart);
                out.writeInt(mTextSelectionEnd);
                out.writeIntArray(mLineCharOffsets);
                out.writeIntArray(mLineBaselines);
                out.writeString(mHint);
            }
        }
    }
}
