Merge changes from topic "cc_virtual_id"

* changes:
  New, minor ContentCapture APIs for virtual views management.
  Implemented missing methods on android.view.contentcapture.ViewNode
diff --git a/api/current.txt b/api/current.txt
index 72dd139..7846f72 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -52323,6 +52323,8 @@
     method public final android.view.contentcapture.ContentCaptureSession createContentCaptureSession(android.view.contentcapture.ContentCaptureContext);
     method public final void destroy();
     method public final android.view.contentcapture.ContentCaptureSessionId getContentCaptureSessionId();
+    method public android.view.autofill.AutofillId newAutofillId(android.view.autofill.AutofillId, int);
+    method public final android.view.ViewStructure newVirtualViewStructure(android.view.autofill.AutofillId, int);
     method public final void notifyViewAppeared(android.view.ViewStructure);
     method public final void notifyViewDisappeared(android.view.autofill.AutofillId);
     method public final void notifyViewTextChanged(android.view.autofill.AutofillId, java.lang.CharSequence, int);
diff --git a/core/java/android/app/assist/AssistStructure.java b/core/java/android/app/assist/AssistStructure.java
index cc4d4b1a..7d03f00 100644
--- a/core/java/android/app/assist/AssistStructure.java
+++ b/core/java/android/app/assist/AssistStructure.java
@@ -1699,7 +1699,8 @@
 
         @Override
         public void setVisibility(int visibility) {
-            mNode.mFlags = (mNode.mFlags&~ViewNode.FLAGS_VISIBILITY_MASK) | visibility;
+            mNode.mFlags = (mNode.mFlags & ~ViewNode.FLAGS_VISIBILITY_MASK)
+                    | (visibility & ViewNode.FLAGS_VISIBILITY_MASK);
         }
 
         @Override
diff --git a/core/java/android/service/contentcapture/ContentCaptureService.java b/core/java/android/service/contentcapture/ContentCaptureService.java
index 1b280fa..e5e028d 100644
--- a/core/java/android/service/contentcapture/ContentCaptureService.java
+++ b/core/java/android/service/contentcapture/ContentCaptureService.java
@@ -166,7 +166,7 @@
      */
     public final void setContentCaptureWhitelist(@Nullable List<String> packages,
             @Nullable List<ComponentName> activities) {
-        //TODO(b/111276913): implement
+        //TODO(b/122595322): implement
     }
 
     /**
@@ -177,7 +177,7 @@
      */
     public final void setActivityContentCaptureEnabled(@NonNull ComponentName activity,
             boolean enabled) {
-        //TODO(b/111276913): implement
+        //TODO(b/122595322): implement
     }
 
     /**
@@ -188,7 +188,7 @@
      */
     public final void setPackageContentCaptureEnabled(@NonNull String packageName,
             boolean enabled) {
-        //TODO(b/111276913): implement
+        //TODO(b/122595322): implement
     }
 
     /**
@@ -197,7 +197,7 @@
      */
     @NonNull
     public final Set<ComponentName> getContentCaptureDisabledActivities() {
-        //TODO(b/111276913): implement
+        //TODO(b/122595322): implement
         return null;
     }
 
@@ -207,7 +207,7 @@
      */
     @NonNull
     public final Set<String> getContentCaptureDisabledPackages() {
-        //TODO(b/111276913): implement
+        //TODO(b/122595322): implement
         return null;
     }
 
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index cb2c40e..cd0e579 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -8199,6 +8199,19 @@
      * <p>The populated structure is then passed to the service through
      * {@link ContentCaptureSession#notifyViewAppeared(ViewStructure)}.
      *
+     * <p><b>Note: </b>views that manage a virtual structure under this view must populate just
+     * the node representing this view and return right away, then asynchronously report (not
+     * necessarily in the UI thread) when the children nodes appear, disappear or have their text
+     * changed by calling
+     * {@link ContentCaptureSession#notifyViewAppeared(ViewStructure)},
+     * {@link ContentCaptureSession#notifyViewDisappeared(AutofillId)}, and
+     * {@link ContentCaptureSession#notifyViewTextChanged(AutofillId, CharSequence, int)}
+     * respectively. The structure for the a child must be created using
+     * {@link ContentCaptureSession#newVirtualViewStructure(AutofillId, int)}, and the
+     * {@code autofillId} for a child can be obtained either through
+     * {@code childStructure.getAutofillId()} or
+     * {@link ContentCaptureSession#newAutofillId(AutofillId, int)}.
+     *
      * <p><b>Note: </b>the following methods of the {@code structure} will be ignored:
      * <ul>
      *   <li>{@link ViewStructure#setChildCount(int)}
@@ -8235,10 +8248,6 @@
         } else {
             structure.setId(id, null, null, null);
         }
-        if (viewFor == VIEW_STRUCTURE_FOR_CONTENT_CAPTURE) {
-            //TODO(b/111276913): STOPSHIP - don't set it if not needed
-            structure.setDataIsSensitive(false);
-        }
 
         if (viewFor == VIEW_STRUCTURE_FOR_AUTOFILL
                 || viewFor == VIEW_STRUCTURE_FOR_CONTENT_CAPTURE) {
diff --git a/core/java/android/view/contentcapture/ContentCaptureSession.java b/core/java/android/view/contentcapture/ContentCaptureSession.java
index 2119073..6890beaf 100644
--- a/core/java/android/view/contentcapture/ContentCaptureSession.java
+++ b/core/java/android/view/contentcapture/ContentCaptureSession.java
@@ -46,8 +46,7 @@
 
     /**
      * Used on {@link #notifyViewTextChanged(AutofillId, CharSequence, int)} to indicate that the
-     *
-     * thext change was caused by user input (for example, through IME).
+     * text change was caused by user input (for example, through IME).
      */
     public static final int FLAG_USER_INPUT = 0x1;
 
@@ -185,7 +184,7 @@
 
             mCloseGuard.close();
 
-            //TODO(b/111276913): check state (for example, how to handle if it's waiting for remote
+            // TODO(b/111276913): check state (for example, how to handle if it's waiting for remote
             // id) and send it to the cache of batched commands
             if (VERBOSE) {
                 Log.v(TAG, "destroy(): state=" + getStateAsString(mState) + ", mId=" + mId);
@@ -302,6 +301,26 @@
     }
 
     /**
+     * Creates a new {@link AutofillId} for a virtual child, so it can be used to uniquely identify
+     * the children in the session.
+     *
+     * @param parentId id of the virtual view parent (it can be obtained by calling
+     * {@link ViewStructure#getAutofillId()} on the parent).
+     * @param virtualChildId id of the virtual child, relative to the parent.
+     *
+     * @return if for the virtual child
+     *
+     * @throws IllegalArgumentException if the {@code parentId} is a virtual child id.
+     */
+    public @NonNull AutofillId newAutofillId(@NonNull AutofillId parentId, int virtualChildId) {
+        Preconditions.checkNotNull(parentId);
+        Preconditions.checkArgument(!parentId.isVirtual(), "virtual ids cannot have children");
+        // TODO(b/121197119): we need to add the session id to the AutofillId to make them unique
+        // per session
+        return new AutofillId(parentId, virtualChildId);
+    }
+
+    /**
      * Creates a {@link ViewStructure} for a "virtual" view, so it can be passed to
      * {@link #notifyViewAppeared(ViewStructure)} by the view managing the virtual view hierarchy.
      *
@@ -310,12 +329,11 @@
      * @param virtualId id of the virtual child, relative to the parent.
      *
      * @return a new {@link ViewStructure} that can be used for Content Capture purposes.
-     *
-     * @hide
      */
     @NonNull
     public final ViewStructure newVirtualViewStructure(@NonNull AutofillId parentId,
             int virtualId) {
+        // TODO(b/121197119): use the constructor that takes a session id / assert on unit test.
         return new ViewNode.ViewStructureImpl(parentId, virtualId);
     }
 
diff --git a/core/java/android/view/contentcapture/ViewNode.java b/core/java/android/view/contentcapture/ViewNode.java
index 86b89adb..b7a486a 100644
--- a/core/java/android/view/contentcapture/ViewNode.java
+++ b/core/java/android/view/contentcapture/ViewNode.java
@@ -24,6 +24,7 @@
 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;
@@ -32,30 +33,188 @@
 import android.view.autofill.AutofillId;
 import android.view.autofill.AutofillValue;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.Preconditions;
 
-//TODO(b/111276913): add javadocs / implement Parcelable / implement
-//TODO(b/111276913): for now it's extending ViewNode directly as it needs most of its properties,
+//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";
+    private static final String TAG = ViewNode.class.getSimpleName();
+    private static final boolean VERBOSE = false;
+
+    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_MATRIX = 1L << 23;
+    private static final long FLAGS_HAS_ELEVATION = 1L << 24;
+    private static final long FLAGS_HAS_ALPHA = 1L << 25;
+    private static final long FLAGS_HAS_CONTENT_DESCRIPTION = 1L << 26;
+    private static final long FLAGS_HAS_EXTRAS = 1L << 27;
+    private static final long FLAGS_HAS_LOCALE_LIST = 1L << 28;
+    private static final long FLAGS_HAS_INPUT_TYPE = 1L << 29;
+    private static final long FLAGS_HAS_MIN_TEXT_EMS = 1L << 30;
+    private static final long FLAGS_HAS_MAX_TEXT_EMS = 1L << 31;
+    private static final long FLAGS_HAS_MAX_TEXT_LENGTH = 1L << 32;
+    private static final long FLAGS_HAS_TEXT_ID_ENTRY = 1L << 33;
+    private static final long FLAGS_HAS_AUTOFILL_TYPE = 1L << 34;
+    private static final long FLAGS_HAS_AUTOFILL_VALUE = 1L << 35;
+    private static final long FLAGS_HAS_AUTOFILL_HINTS = 1L << 36;
+    private static final long FLAGS_HAS_AUTOFILL_OPTIONS = 1L << 37;
+
+    /** Flags used to optimize what's written to the parcel */
+    private long mFlags;
 
     private AutofillId mParentAutofillId;
 
-    // TODO(b/111276913): temporarily setting some fields here while they're not accessible from the
-    // superclass
     private AutofillId mAutofillId;
-    private CharSequence mText;
+    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 Matrix mMatrix;
+    private float mElevation;
+    private float mAlpha = 1.0f;
+    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 @View.AutofillType int mAutofillType = View.AUTOFILL_TYPE_NONE;
+    private String[] mAutofillHints;
+    private AutofillValue mAutofillValue;
+    private CharSequence[] mAutofillOptions;
 
     /** @hide */
     public ViewNode() {
     }
 
+    private ViewNode(long nodeFlags, @NonNull Parcel parcel) {
+        mFlags = nodeFlags;
+
+        if ((nodeFlags & FLAGS_HAS_AUTOFILL_ID) != 0) {
+            mAutofillId = parcel.readParcelable(null);
+        }
+        if ((nodeFlags & FLAGS_HAS_AUTOFILL_PARENT_ID) != 0) {
+            mParentAutofillId = parcel.readParcelable(null);
+        }
+        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_MATRIX) != 0) {
+            mMatrix = new Matrix();
+            final float[] tmpMatrix = new float[9];
+            parcel.readFloatArray(tmpMatrix);
+            mMatrix.setValues(tmpMatrix);
+        }
+        if ((nodeFlags & FLAGS_HAS_ELEVATION) != 0) {
+            mElevation = parcel.readFloat();
+        }
+        if ((nodeFlags & FLAGS_HAS_ALPHA) != 0) {
+            mAlpha = parcel.readFloat();
+        }
+        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);
+        }
+        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);
+        }
+        if ((nodeFlags & FLAGS_HAS_AUTOFILL_OPTIONS) != 0) {
+            mAutofillOptions = parcel.readCharSequenceArray();
+        }
+    }
+
     /**
      * Returns the {@link AutofillId} of this view's parent, if the parent is also part of the
      * screen observation tree.
@@ -65,53 +224,446 @@
         return mParentAutofillId;
     }
 
-    // TODO(b/111276913): temporarily overwriting some methods
     @Override
     public AutofillId getAutofillId() {
         return mAutofillId;
     }
+
     @Override
     public CharSequence getText() {
-        return mText;
+        return mText != null ? mText.mText : null;
     }
+
     @Override
     public String getClassName() {
         return mClassName;
     }
 
+    @Override
+    public int getId() {
+        return mId;
+    }
+
+    @Override
+    public String getIdPackage() {
+        return mIdPackage;
+    }
+
+    @Override
+    public String getIdType() {
+        return mIdType;
+    }
+
+    @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;
+    }
+
+    @Override
+    public Matrix getTransformation() {
+        return mMatrix;
+    }
+
+    @Override
+    public float getElevation() {
+        return mElevation;
+    }
+
+    @Override
+    public float getAlpha() {
+        return mAlpha;
+    }
+
+    @Override
+    public CharSequence getContentDescription() {
+        return mContentDescription;
+    }
+
+    @Override
+    public Bundle getExtras() {
+        return mExtras;
+    }
+
+    @Override
+    public String getHint() {
+        return mText != null ? mText.mHint : null;
+    }
+
+    @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;
+    }
+
+    @Override
+    public int[] getTextLineCharOffsets() {
+        return mText != null ? mText.mLineCharOffsets : null;
+    }
+
+    @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;
+    }
+
+    @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
+    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 (mMatrix != null) {
+            nodeFlags |= FLAGS_HAS_MATRIX;
+        }
+        if (mElevation != 0) {
+            nodeFlags |= FLAGS_HAS_ELEVATION;
+        }
+        if (mAlpha != 1.0f) {
+            nodeFlags |= FLAGS_HAS_ALPHA;
+        }
+        if (mContentDescription != null) {
+            nodeFlags |= FLAGS_HAS_CONTENT_DESCRIPTION;
+        }
+        if (mExtras != null) {
+            nodeFlags |= FLAGS_HAS_EXTRAS;
+        }
+        if (mLocaleList != null) {
+            nodeFlags |= FLAGS_HAS_LOCALE_LIST;
+        }
+        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;
+        }
+        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_MATRIX) != 0) {
+            //TODO(b/122484602): use a singleton tmpMatrix (if logic is not moved to superclass)
+            final float[] tmpMatrix = new float[9];
+            mMatrix.getValues(tmpMatrix);
+            parcel.writeFloatArray(tmpMatrix);
+        }
+        if ((nodeFlags & FLAGS_HAS_ELEVATION) != 0) {
+            parcel.writeFloat(mElevation);
+        }
+        if ((nodeFlags & FLAGS_HAS_ALPHA) != 0) {
+            parcel.writeFloat(mAlpha);
+        }
+        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_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);
+        }
+    }
+
     /** @hide */
     public static void writeToParcel(@NonNull Parcel parcel, @Nullable ViewNode node, int flags) {
         if (node == null) {
-            parcel.writeParcelable(null, flags);
-            return;
+            parcel.writeLong(0);
+        } else {
+            node.writeSelfToParcel(parcel, flags);
         }
-        parcel.writeParcelable(node.mAutofillId, flags);
-        parcel.writeParcelable(node.mParentAutofillId, flags);
-        parcel.writeCharSequence(node.mText);
-        parcel.writeString(node.mClassName);
     }
 
     /** @hide */
     public static @Nullable ViewNode readFromParcel(@NonNull Parcel parcel) {
-        final AutofillId id = parcel.readParcelable(null);
-        if (id == null) return null;
-
-        final ViewNode node = new ViewNode();
-
-        node.mAutofillId = id;
-        node.mParentAutofillId = parcel.readParcelable(null);
-        node.mText = parcel.readCharSequence();
-        node.mClassName = parcel.readString();
-
-        return node;
+        final long nodeFlags = parcel.readLong();
+        return nodeFlags == 0 ? new ViewNode() : new ViewNode(nodeFlags, parcel);
     }
 
     /** @hide */
-    static final class ViewStructureImpl extends ViewStructure {
+    @VisibleForTesting // Must be public to be accessed from FrameworkCoreTests' apk.
+    public static final class ViewStructureImpl extends ViewStructure {
 
         final ViewNode mNode = new ViewNode();
 
-        ViewStructureImpl(@NonNull View view) {
+        @VisibleForTesting // Must be public to be accessed from FrameworkCoreTests' apk.
+        public ViewStructureImpl(@NonNull View view) {
             mNode.mAutofillId = Preconditions.checkNotNull(view).getAutofillId();
             final ViewParent parent = view.getParent();
             if (parent instanceof View) {
@@ -119,179 +671,212 @@
             }
         }
 
-        ViewStructureImpl(@NonNull AutofillId parentId, int virtualId) {
+        @VisibleForTesting // Must be public to be accessed from FrameworkCoreTests' apk.
+        public ViewStructureImpl(@NonNull AutofillId parentId, int virtualId) {
             mNode.mParentAutofillId = Preconditions.checkNotNull(parentId);
             mNode.mAutofillId = new AutofillId(parentId, virtualId);
         }
 
+        @VisibleForTesting // Must be public to be accessed from FrameworkCoreTests' apk.
+        public ViewNode getNode() {
+            return mNode;
+        }
+
         @Override
         public void setId(int id, String packageName, String typeName, String entryName) {
-            // TODO(b/111276913): implement or move to superclass
+            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) {
-            // TODO(b/111276913): implement or move to superclass
+            mNode.mX = left;
+            mNode.mY = top;
+            mNode.mScrollX = scrollX;
+            mNode.mScrollY = scrollY;
+            mNode.mWidth = width;
+            mNode.mHeight = height;
         }
 
         @Override
         public void setTransformation(Matrix matrix) {
-            // TODO(b/111276913): implement or move to superclass
+            if (matrix == null) {
+                mNode.mMatrix = null;
+            } else {
+                mNode.mMatrix = new Matrix(matrix);
+            }
         }
 
         @Override
         public void setElevation(float elevation) {
-            // TODO(b/111276913): implement or move to superclass
+            mNode.mElevation = elevation;
         }
 
         @Override
         public void setAlpha(float alpha) {
-            // TODO(b/111276913): implement or move to superclass
+            mNode.mAlpha = alpha;
         }
 
         @Override
         public void setVisibility(int visibility) {
-            // TODO(b/111276913): implement or move to superclass
+            mNode.mFlags = (mNode.mFlags & ~FLAGS_VISIBILITY_MASK)
+                    | (visibility & FLAGS_VISIBILITY_MASK);
         }
 
         @Override
         public void setAssistBlocked(boolean state) {
-            // TODO(b/111276913): implement or move to superclass
+            mNode.mFlags = (mNode.mFlags & ~FLAGS_ASSIST_BLOCKED)
+                    | (state ? FLAGS_ASSIST_BLOCKED : 0);
         }
 
         @Override
         public void setEnabled(boolean state) {
-            // TODO(b/111276913): implement or move to superclass
+            mNode.mFlags = (mNode.mFlags & ~FLAGS_DISABLED) | (state ? 0 : FLAGS_DISABLED);
         }
 
         @Override
         public void setClickable(boolean state) {
-            // TODO(b/111276913): implement or move to superclass
+            mNode.mFlags = (mNode.mFlags & ~FLAGS_CLICKABLE) | (state ? FLAGS_CLICKABLE : 0);
         }
 
         @Override
         public void setLongClickable(boolean state) {
-            // TODO(b/111276913): implement or move to superclass
+            mNode.mFlags = (mNode.mFlags & ~FLAGS_LONG_CLICKABLE)
+                    | (state ? FLAGS_LONG_CLICKABLE : 0);
         }
 
         @Override
         public void setContextClickable(boolean state) {
-            // TODO(b/111276913): implement or move to superclass
+            mNode.mFlags = (mNode.mFlags & ~FLAGS_CONTEXT_CLICKABLE)
+                    | (state ? FLAGS_CONTEXT_CLICKABLE : 0);
         }
 
         @Override
         public void setFocusable(boolean state) {
-            // TODO(b/111276913): implement or move to superclass
+            mNode.mFlags = (mNode.mFlags & ~FLAGS_FOCUSABLE) | (state ? FLAGS_FOCUSABLE : 0);
         }
 
         @Override
         public void setFocused(boolean state) {
-            // TODO(b/111276913): implement or move to superclass
+            mNode.mFlags = (mNode.mFlags & ~FLAGS_FOCUSED) | (state ? FLAGS_FOCUSED : 0);
         }
 
         @Override
         public void setAccessibilityFocused(boolean state) {
-            // TODO(b/111276913): implement or move to superclass
+            mNode.mFlags = (mNode.mFlags & ~FLAGS_ACCESSIBILITY_FOCUSED)
+                    | (state ? FLAGS_ACCESSIBILITY_FOCUSED : 0);
         }
 
         @Override
         public void setCheckable(boolean state) {
-            // TODO(b/111276913): implement or move to superclass
+            mNode.mFlags = (mNode.mFlags & ~FLAGS_CHECKABLE) | (state ? FLAGS_CHECKABLE : 0);
         }
 
         @Override
         public void setChecked(boolean state) {
-            // TODO(b/111276913): implement or move to superclass
+            mNode.mFlags = (mNode.mFlags & ~FLAGS_CHECKED) | (state ? FLAGS_CHECKED : 0);
         }
 
         @Override
         public void setSelected(boolean state) {
-            // TODO(b/111276913): implement or move to superclass
+            mNode.mFlags = (mNode.mFlags & ~FLAGS_SELECTED) | (state ? FLAGS_SELECTED : 0);
         }
 
         @Override
         public void setActivated(boolean state) {
-            // TODO(b/111276913): implement or move to superclass
+            mNode.mFlags = (mNode.mFlags & ~FLAGS_ACTIVATED) | (state ? FLAGS_ACTIVATED : 0);
         }
 
         @Override
         public void setOpaque(boolean opaque) {
-            // TODO(b/111276913): implement or move to superclass
+            mNode.mFlags = (mNode.mFlags & ~FLAGS_OPAQUE) | (opaque ? FLAGS_OPAQUE : 0);
         }
 
         @Override
         public void setClassName(String className) {
-            // TODO(b/111276913): temporarily setting directly; should be done on superclass instead
             mNode.mClassName = className;
         }
 
         @Override
         public void setContentDescription(CharSequence contentDescription) {
-            // TODO(b/111276913): implement or move to superclass
+            mNode.mContentDescription = contentDescription;
         }
 
         @Override
         public void setText(CharSequence text) {
-            // TODO(b/111276913): temporarily setting directly; should be done on superclass instead
-            mNode.mText = 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) {
-            // TODO(b/111276913): temporarily setting directly; should be done on superclass instead
-            mNode.mText = text;
-            // TODO(b/111276913): implement or move to superclass
+            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) {
-            // TODO(b/111276913): implement or move to superclass
+            final ViewNodeText t = getNodeText();
+            t.mTextColor = fgColor;
+            t.mTextBackgroundColor = bgColor;
+            t.mTextSize = size;
+            t.mTextStyle = style;
         }
 
         @Override
         public void setTextLines(int[] charOffsets, int[] baselines) {
-            // TODO(b/111276913): implement or move to superclass
+            final ViewNodeText t = getNodeText();
+            t.mLineCharOffsets = charOffsets;
+            t.mLineBaselines = baselines;
+        }
+
+        @Override
+        public void setTextIdEntry(String entryName) {
+            mNode.mTextIdEntry = Preconditions.checkNotNull(entryName);
         }
 
         @Override
         public void setHint(CharSequence hint) {
-            // TODO(b/111276913): implement or move to superclass
+            getNodeText().mHint = hint != null ? hint.toString() : null;
         }
 
         @Override
         public CharSequence getText() {
-            // TODO(b/111276913): temporarily getting directly; should be done on superclass instead
-            return mNode.mText;
+            return mNode.getText();
         }
 
         @Override
         public int getTextSelectionStart() {
-            // TODO(b/111276913): implement or move to superclass
-            return 0;
+            return mNode.getTextSelectionStart();
         }
 
         @Override
         public int getTextSelectionEnd() {
-            // TODO(b/111276913): implement or move to superclass
-            return 0;
+            return mNode.getTextSelectionEnd();
         }
 
         @Override
         public CharSequence getHint() {
-            // TODO(b/111276913): implement or move to superclass
-            return null;
+            return mNode.getHint();
         }
 
         @Override
         public Bundle getExtras() {
-            // TODO(b/111276913): implement or move to superclass
-            return null;
+            if (mNode.mExtras != null) {
+                return mNode.mExtras;
+            }
+            mNode.mExtras = new Bundle();
+            return mNode.mExtras;
         }
 
         @Override
         public boolean hasExtras() {
-            // TODO(b/111276913): implement or move to superclass
-            return false;
+            return mNode.mExtras != null;
         }
 
         @Override
@@ -325,50 +910,64 @@
 
         @Override
         public AutofillId getAutofillId() {
-            // TODO(b/111276913): temporarily getting directly; should be done on superclass instead
             return mNode.mAutofillId;
         }
 
         @Override
         public void setAutofillId(AutofillId id) {
-            // TODO(b/111276913): temporarily setting directly; should be done on superclass instead
-            mNode.mAutofillId = id;
+            mNode.mAutofillId = Preconditions.checkNotNull(id);
         }
 
+
         @Override
         public void setAutofillId(AutofillId parentId, int virtualId) {
-            // TODO(b/111276913): temporarily setting directly; should be done on superclass instead
+            mNode.mParentAutofillId = Preconditions.checkNotNull(parentId);
             mNode.mAutofillId = new AutofillId(parentId, virtualId);
         }
 
         @Override
-        public void setAutofillType(int type) {
-            // TODO(b/111276913): implement or move to superclass
+        public void setAutofillType(@View.AutofillType int type) {
+            mNode.mAutofillType = type;
         }
 
         @Override
-        public void setAutofillHints(String[] hint) {
-            // TODO(b/111276913): implement or move to superclass
+        public void setAutofillHints(String[] hints) {
+            mNode.mAutofillHints = hints;
         }
 
         @Override
         public void setAutofillValue(AutofillValue value) {
-            // TODO(b/111276913): implement or move to superclass
+            mNode.mAutofillValue = value;
         }
 
         @Override
         public void setAutofillOptions(CharSequence[] options) {
-            // TODO(b/111276913): implement or move to superclass
+            mNode.mAutofillOptions = options;
         }
 
         @Override
         public void setInputType(int inputType) {
-            // TODO(b/111276913): implement or move to superclass
+            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) {
-            // TODO(b/111276913): implement or move to superclass
+            Log.w(TAG, "setDataIsSensitive() is not supported");
         }
 
         @Override
@@ -378,7 +977,7 @@
 
         @Override
         public Rect getTempRect() {
-            // TODO(b/111276913): implement or move to superclass
+            Log.w(TAG, "getTempRect() is not supported");
             return null;
         }
 
@@ -389,7 +988,7 @@
 
         @Override
         public void setLocaleList(LocaleList localeList) {
-            // TODO(b/111276913): implement or move to superclass
+            mNode.mLocaleList = localeList;
         }
 
         @Override
@@ -402,5 +1001,66 @@
         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);
+            }
+        }
     }
 }
diff --git a/core/tests/coretests/Android.mk b/core/tests/coretests/Android.mk
index 041fb7e..74943c7 100644
--- a/core/tests/coretests/Android.mk
+++ b/core/tests/coretests/Android.mk
@@ -43,7 +43,8 @@
     platform-test-annotations \
     compatibility-device-util \
     truth-prebuilt \
-    print-test-util-lib
+    print-test-util-lib \
+    testng # TODO: remove once Android migrates to JUnit 4.12, which provide assertThrows
 
 LOCAL_JAVA_LIBRARIES := \
     android.test.runner \
diff --git a/core/tests/coretests/src/android/view/contentcapture/ContentCaptureSessionTest.java b/core/tests/coretests/src/android/view/contentcapture/ContentCaptureSessionTest.java
new file mode 100644
index 0000000..59f3a4c
--- /dev/null
+++ b/core/tests/coretests/src/android/view/contentcapture/ContentCaptureSessionTest.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2019 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 static com.google.common.truth.Truth.assertThat;
+
+import static org.testng.Assert.assertThrows;
+
+import android.view.autofill.AutofillId;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Spy;
+import org.mockito.junit.MockitoJUnitRunner;
+
+/**
+ * Unit test for {@link ContentCaptureSessionTest}.
+ *
+ * <p>To run it:
+ * {@code atest FrameworksCoreTests:android.view.contentcapture.ContentCaptureSessionTest}
+ */
+@RunWith(MockitoJUnitRunner.class)
+public class ContentCaptureSessionTest {
+
+    /**
+     * Uses a spy as ContentCaptureSession is abstract but (so far) we're testing its concrete
+     * methods.
+     */
+    @Spy
+    private ContentCaptureSession mMockSession;
+
+    @Test
+    public void testNewAutofillId_invalid() {
+        assertThrows(NullPointerException.class, () -> mMockSession.newAutofillId(null, 42));
+        assertThrows(IllegalArgumentException.class,
+                () -> mMockSession.newAutofillId(new AutofillId(42, 42), 42));
+    }
+
+    @Test
+    public void testNewAutofillId_valid() {
+        final AutofillId parentId = new AutofillId(42);
+        final AutofillId childId = mMockSession.newAutofillId(parentId, 108);
+        assertThat(childId.getViewId()).isEqualTo(42);
+        assertThat(childId.getVirtualChildId()).isEqualTo(108);
+        // TODO(b/121197119): assert session id
+    }
+
+    @Test
+    public void testNotifyXXX_null() {
+        assertThrows(NullPointerException.class, () -> mMockSession.notifyViewAppeared(null));
+        assertThrows(NullPointerException.class, () -> mMockSession.notifyViewDisappeared(null));
+        assertThrows(NullPointerException.class,
+                () -> mMockSession.notifyViewTextChanged(null, "whatever", 0));
+    }
+}
diff --git a/core/tests/coretests/src/android/view/contentcapture/ViewNodeTest.java b/core/tests/coretests/src/android/view/contentcapture/ViewNodeTest.java
new file mode 100644
index 0000000..995946b
--- /dev/null
+++ b/core/tests/coretests/src/android/view/contentcapture/ViewNodeTest.java
@@ -0,0 +1,462 @@
+/*
+ * Copyright (C) 2019 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 static com.google.common.truth.Truth.assertThat;
+
+import static org.testng.Assert.assertThrows;
+
+import android.content.Context;
+import android.graphics.Matrix;
+import android.os.Bundle;
+import android.os.LocaleList;
+import android.os.Parcel;
+import android.support.test.InstrumentationRegistry;
+import android.view.View;
+import android.view.ViewStructure.HtmlInfo;
+import android.view.autofill.AutofillId;
+import android.view.autofill.AutofillValue;
+import android.view.contentcapture.ViewNode.ViewStructureImpl;
+import android.widget.FrameLayout;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+import java.util.Locale;
+
+/**
+ * Unit test for {@link ViewNode}.
+ *
+ * <p>To run it: {@code atest FrameworksCoreTests:android.view.contentcapture.ViewNodeTest}
+ */
+@RunWith(MockitoJUnitRunner.class)
+public class ViewNodeTest {
+
+    private final Context mContext = InstrumentationRegistry.getTargetContext();
+
+    @Mock
+    private HtmlInfo mHtmlInfoMock;
+
+    @Test
+    public void testAutofillIdMethods_orphanView() {
+        View view = new View(mContext);
+        AutofillId initialId = new AutofillId(42);
+        view.setAutofillId(initialId);
+
+        ViewStructureImpl structure = new ViewStructureImpl(view);
+        ViewNode node = structure.getNode();
+
+        assertThat(node.getAutofillId()).isEqualTo(initialId);
+        assertThat(node.getParentAutofillId()).isNull();
+
+        AutofillId newId = new AutofillId(108);
+        structure.setAutofillId(newId);
+        assertThat(node.getAutofillId()).isEqualTo(newId);
+        assertThat(node.getParentAutofillId()).isNull();
+
+        structure.setAutofillId(new AutofillId(66), 6);
+        assertThat(node.getAutofillId()).isEqualTo(new AutofillId(66, 6));
+        assertThat(node.getParentAutofillId()).isEqualTo(new AutofillId(66));
+    }
+
+    @Test
+    public void testAutofillIdMethods_parentedView() {
+        FrameLayout parent = new FrameLayout(mContext);
+        AutofillId initialParentId = new AutofillId(48);
+        parent.setAutofillId(initialParentId);
+
+        View child = new View(mContext);
+        AutofillId initialChildId = new AutofillId(42);
+        child.setAutofillId(initialChildId);
+
+        parent.addView(child);
+
+        ViewStructureImpl structure = new ViewStructureImpl(child);
+        ViewNode node = structure.getNode();
+
+        assertThat(node.getAutofillId()).isEqualTo(initialChildId);
+        assertThat(node.getParentAutofillId()).isEqualTo(initialParentId);
+
+        AutofillId newChildId = new AutofillId(108);
+        structure.setAutofillId(newChildId);
+        assertThat(node.getAutofillId()).isEqualTo(newChildId);
+        assertThat(node.getParentAutofillId()).isEqualTo(initialParentId);
+
+        AutofillId newParentId = new AutofillId(15162342);
+        parent.setAutofillId(newParentId);
+        assertThat(node.getAutofillId()).isEqualTo(newChildId);
+        assertThat(node.getParentAutofillId()).isEqualTo(initialParentId);
+
+        structure.setAutofillId(new AutofillId(66), 6);
+        assertThat(node.getAutofillId()).isEqualTo(new AutofillId(66, 6));
+        assertThat(node.getParentAutofillId()).isEqualTo(new AutofillId(66));
+    }
+
+    @Test
+    public void testAutofillIdMethods_explicitIdsConstructor() {
+        AutofillId initialParentId = new AutofillId(42);
+        ViewStructureImpl structure = new ViewStructureImpl(initialParentId, 108);
+        ViewNode node = structure.getNode();
+
+        assertThat(node.getAutofillId()).isEqualTo(new AutofillId(initialParentId, 108));
+        assertThat(node.getParentAutofillId()).isEqualTo(initialParentId);
+
+        AutofillId newChildId = new AutofillId(108);
+        structure.setAutofillId(newChildId);
+        assertThat(node.getAutofillId()).isEqualTo(newChildId);
+        assertThat(node.getParentAutofillId()).isEqualTo(initialParentId);
+
+        structure.setAutofillId(new AutofillId(66), 6);
+        assertThat(node.getAutofillId()).isEqualTo(new AutofillId(66, 6));
+        assertThat(node.getParentAutofillId()).isEqualTo(new AutofillId(66));
+    }
+
+    @Test
+    public void testInvalidSetters() {
+        View view = new View(mContext);
+        AutofillId initialId = new AutofillId(42);
+        view.setAutofillId(initialId);
+
+        ViewStructureImpl structure = new ViewStructureImpl(view);
+        ViewNode node = structure.getNode();
+        assertThat(node.getAutofillId()).isEqualTo(initialId); // sanity check
+
+        assertThrows(NullPointerException.class, () -> structure.setAutofillId(null));
+        assertThat(node.getAutofillId()).isEqualTo(initialId); // invariant
+
+        assertThrows(NullPointerException.class, () -> structure.setAutofillId(null, 666));
+        assertThat(node.getAutofillId()).isEqualTo(initialId); // invariant
+
+        assertThrows(NullPointerException.class, () -> structure.setTextIdEntry(null));
+        assertThat(node.getTextIdEntry()).isNull();
+    }
+
+    @Test
+    public void testUnsupportedProperties() {
+        View view = new View(mContext);
+
+        ViewStructureImpl structure = new ViewStructureImpl(view);
+        ViewNode node = structure.getNode();
+
+        structure.setChildCount(1);
+        assertThat(node.getChildCount()).isEqualTo(0);
+
+        structure.addChildCount(1);
+        assertThat(node.getChildCount()).isEqualTo(0);
+
+        assertThat(structure.newChild(0)).isNull();
+        assertThat(node.getChildCount()).isEqualTo(0);
+
+        assertThat(structure.asyncNewChild(0)).isNull();
+        assertThat(node.getChildCount()).isEqualTo(0);
+
+        structure.asyncCommit();
+        assertThat(node.getChildCount()).isEqualTo(0);
+
+        structure.setWebDomain("Y U NO SET?");
+        assertThat(node.getWebDomain()).isNull();
+
+        assertThat(structure.newHtmlInfoBuilder("WHATEVER")).isNull();
+
+        structure.setHtmlInfo(mHtmlInfoMock);
+        assertThat(node.getHtmlInfo()).isNull();
+
+        structure.setDataIsSensitive(true);
+
+        assertThat(structure.getTempRect()).isNull();
+    }
+
+    @Test
+    public void testValidProperties_directly() {
+        ViewStructureImpl structure = newSimpleStructure();
+        assertSimpleStructure(structure);
+        assertSimpleNode(structure.getNode());
+    }
+
+    @Test
+    public void testValidProperties_throughParcel() {
+        ViewStructureImpl structure = newSimpleStructure();
+        final ViewNode node = structure.getNode();
+        assertSimpleNode(node); // sanity check
+
+        final ViewNode clone = cloneThroughParcel(node);
+        assertSimpleNode(clone);
+    }
+
+    @Test
+    public void testComplexText_directly() {
+        ViewStructureImpl structure = newStructureWithComplexText();
+        assertStructureWithComplexText(structure);
+        assertNodeWithComplexText(structure.getNode());
+    }
+
+    @Test
+    public void testComplexText_throughParcel() {
+        ViewStructureImpl structure = newStructureWithComplexText();
+        final ViewNode node = structure.getNode();
+        assertNodeWithComplexText(node); // sanity check
+
+        ViewNode clone = cloneThroughParcel(node);
+        assertNodeWithComplexText(clone);
+    }
+
+    @Test
+    public void testVisibility() {
+        // Visibility is a special case becase it use flag masks, so we want to make sure it works
+        // fine
+        View view = new View(mContext);
+        ViewStructureImpl structure = new ViewStructureImpl(view);
+        ViewNode node = structure.getNode();
+
+        structure.setVisibility(View.VISIBLE);
+        assertThat(node.getVisibility()).isEqualTo(View.VISIBLE);
+        assertThat(cloneThroughParcel(node).getVisibility()).isEqualTo(View.VISIBLE);
+
+        structure.setVisibility(View.GONE);
+        assertThat(node.getVisibility()).isEqualTo(View.GONE);
+        assertThat(cloneThroughParcel(node).getVisibility()).isEqualTo(View.GONE);
+
+        structure.setVisibility(View.VISIBLE);
+        assertThat(node.getVisibility()).isEqualTo(View.VISIBLE);
+        assertThat(cloneThroughParcel(node).getVisibility()).isEqualTo(View.VISIBLE);
+
+        structure.setVisibility(View.INVISIBLE);
+        assertThat(node.getVisibility()).isEqualTo(View.INVISIBLE);
+        assertThat(cloneThroughParcel(node).getVisibility()).isEqualTo(View.INVISIBLE);
+
+        structure.setVisibility(View.INVISIBLE | View.GONE);
+        assertThat(node.getVisibility()).isEqualTo(View.INVISIBLE | View.GONE);
+        assertThat(cloneThroughParcel(node).getVisibility()).isEqualTo(View.INVISIBLE | View.GONE);
+
+
+        final int invalidValue = Math.max(Math.max(View.VISIBLE, View.INVISIBLE), View.GONE) * 2;
+        structure.setVisibility(View.VISIBLE);
+        structure.setVisibility(invalidValue); // should be ignored
+        assertThat(node.getVisibility()).isEqualTo(View.VISIBLE);
+        assertThat(cloneThroughParcel(node).getVisibility()).isEqualTo(View.VISIBLE);
+
+        structure.setVisibility(View.GONE | invalidValue);
+        assertThat(node.getVisibility()).isEqualTo(View.GONE);
+        assertThat(cloneThroughParcel(node).getVisibility()).isEqualTo(View.GONE);
+    }
+
+    /**
+     * Creates a {@link ViewStructureImpl} that can be asserted through
+     * {@link #assertSimpleNode(ViewNode)}.
+     */
+    private ViewStructureImpl newSimpleStructure() {
+        View view = new View(mContext);
+        view.setAutofillId(new AutofillId(42));
+
+        ViewStructureImpl structure = new ViewStructureImpl(view);
+
+        // Basic properties
+        structure.setText("Text is set!");
+        structure.setClassName("Classy!");
+        structure.setContentDescription("Described I am!");
+        structure.setVisibility(View.INVISIBLE);
+
+        // Autofill properties
+        structure.setAutofillType(View.AUTOFILL_TYPE_TEXT);
+        structure.setAutofillHints(new String[] { "Auto", "Man" });
+        structure.setAutofillOptions(new String[] { "Maybe" });
+        structure.setAutofillValue(AutofillValue.forText("Malkovich"));
+
+        // Graphic properties
+        structure.setElevation(6.66f);
+        structure.setAlpha(66.6f);
+        structure.setTransformation(Matrix.IDENTITY_MATRIX);
+
+        // Extra text properties
+        structure.setMinTextEms(6);
+        structure.setMaxTextLength(66);
+        structure.setMaxTextEms(666);
+        structure.setInputType(42);
+        structure.setTextIdEntry("TEXT, Y U NO ENTRY?");
+        structure.setLocaleList(new LocaleList(Locale.US, Locale.ENGLISH));
+
+        // Resource id
+        structure.setId(16, "package.name", "type.name", "entry.name");
+
+        // Dimensions
+        structure.setDimens(4, 8, 15, 16, 23, 42);
+
+        // Boolean properties
+        structure.setAssistBlocked(true);
+        structure.setEnabled(true);
+        structure.setClickable(true);
+        structure.setLongClickable(true);
+        structure.setContextClickable(true);
+        structure.setFocusable(true);
+        structure.setFocused(true);
+        structure.setAccessibilityFocused(true);
+        structure.setChecked(true);
+        structure.setActivated(true);
+        structure.setOpaque(true);
+
+        // Bundle
+        assertThat(structure.hasExtras()).isFalse();
+        final Bundle bundle = structure.getExtras();
+        assertThat(bundle).isNotNull();
+        bundle.putString("Marlon", "Bundle");
+        assertThat(structure.hasExtras()).isTrue();
+        return structure;
+    }
+
+    /**
+     * Asserts the properties of a {@link ViewNode} that was created by
+     * {@link #newSimpleStructure()}.
+     */
+    private void assertSimpleNode(ViewNode node) {
+
+        // Basic properties
+        assertThat(node.getAutofillId()).isEqualTo(new AutofillId(42));
+        assertThat(node.getParentAutofillId()).isNull();
+        assertThat(node.getText()).isEqualTo("Text is set!");
+        assertThat(node.getClassName()).isEqualTo("Classy!");
+        assertThat(node.getContentDescription().toString()).isEqualTo("Described I am!");
+        assertThat(node.getVisibility()).isEqualTo(View.INVISIBLE);
+
+        // Autofill properties
+        assertThat(node.getAutofillType()).isEqualTo(View.AUTOFILL_TYPE_TEXT);
+        assertThat(node.getAutofillHints()).asList().containsExactly("Auto", "Man").inOrder();
+        assertThat(node.getAutofillOptions()).asList().containsExactly("Maybe").inOrder();
+        assertThat(node.getAutofillValue().getTextValue()).isEqualTo("Malkovich");
+
+        // Graphic properties
+        assertThat(node.getElevation()).isWithin(1.0e-10f).of(6.66f);
+        assertThat(node.getAlpha()).isWithin(1.0e-10f).of(66.6f);
+        assertThat(node.getTransformation()).isEqualTo(Matrix.IDENTITY_MATRIX);
+
+        // Extra text properties
+        assertThat(node.getMinTextEms()).isEqualTo(6);
+        assertThat(node.getMaxTextLength()).isEqualTo(66);
+        assertThat(node.getMaxTextEms()).isEqualTo(666);
+        assertThat(node.getInputType()).isEqualTo(42);
+        assertThat(node.getTextIdEntry()).isEqualTo("TEXT, Y U NO ENTRY?");
+        assertThat(node.getLocaleList()).isEqualTo(new LocaleList(Locale.US, Locale.ENGLISH));
+
+        // Resource id
+        assertThat(node.getId()).isEqualTo(16);
+        assertThat(node.getIdPackage()).isEqualTo("package.name");
+        assertThat(node.getIdType()).isEqualTo("type.name");
+        assertThat(node.getIdEntry()).isEqualTo("entry.name");
+
+        // Dimensions
+        assertThat(node.getLeft()).isEqualTo(4);
+        assertThat(node.getTop()).isEqualTo(8);
+        assertThat(node.getScrollX()).isEqualTo(15);
+        assertThat(node.getScrollY()).isEqualTo(16);
+        assertThat(node.getWidth()).isEqualTo(23);
+        assertThat(node.getHeight()).isEqualTo(42);
+
+        // Boolean properties
+        assertThat(node.isAssistBlocked()).isTrue();
+        assertThat(node.isEnabled()).isTrue();
+        assertThat(node.isClickable()).isTrue();
+        assertThat(node.isLongClickable()).isTrue();
+        assertThat(node.isContextClickable()).isTrue();
+        assertThat(node.isFocusable()).isTrue();
+        assertThat(node.isFocused()).isTrue();
+        assertThat(node.isAccessibilityFocused()).isTrue();
+        assertThat(node.isChecked()).isTrue();
+        assertThat(node.isActivated()).isTrue();
+        assertThat(node.isOpaque()).isTrue();
+
+        // Bundle
+        final Bundle bundle = node.getExtras();
+        assertThat(bundle).isNotNull();
+        assertThat(bundle.size()).isEqualTo(1);
+        assertThat(bundle.getString("Marlon")).isEqualTo("Bundle");
+    }
+
+    /**
+     * Asserts the properties of a {@link ViewStructureImpl} that was created by
+     * {@link #newSimpleStructure()}.
+     */
+    private void assertSimpleStructure(ViewStructureImpl structure) {
+        assertThat(structure.getAutofillId()).isEqualTo(new AutofillId(42));
+        assertThat(structure.getText()).isEqualTo("Text is set!");
+
+        // Bundle
+        final Bundle bundle = structure.getExtras();
+        assertThat(bundle.size()).isEqualTo(1);
+        assertThat(bundle.getString("Marlon")).isEqualTo("Bundle");
+    }
+
+    /**
+     * Creates a {@link ViewStructureImpl} with "complex" text properties (such as selection); it
+     * can be asserted through {@link #assertNodeWithComplexText(ViewNode)}.
+     */
+    private ViewStructureImpl newStructureWithComplexText() {
+        View view = new View(mContext);
+        ViewStructureImpl structure = new ViewStructureImpl(view);
+        structure.setText("IGNORE ME!");
+        structure.setText("Now we're talking!", 4, 8);
+        structure.setHint("Soylent Green is SPOILER ALERT");
+        structure.setTextStyle(15.0f, 16, 23, 42);
+        structure.setTextLines(new int[] {4,  8, 15} , new int[] {16, 23, 42});
+        return structure;
+    }
+
+    /**
+     * Asserts the properties of a {@link ViewNode} that was created by
+     * {@link #newStructureWithComplexText()}.
+     */
+    private void assertNodeWithComplexText(ViewNode node) {
+        assertThat(node.getText()).isEqualTo("Now we're talking!");
+        assertThat(node.getTextSelectionStart()).isEqualTo(4);
+        assertThat(node.getTextSelectionEnd()).isEqualTo(8);
+        assertThat(node.getHint()).isEqualTo("Soylent Green is SPOILER ALERT");
+        assertThat(node.getTextSize()).isWithin(1.0e-10f).of(15.0f);
+        assertThat(node.getTextColor()).isEqualTo(16);
+        assertThat(node.getTextBackgroundColor()).isEqualTo(23);
+        assertThat(node.getTextStyle()).isEqualTo(42);
+        assertThat(node.getTextLineCharOffsets()).asList().containsExactly(4, 8, 15).inOrder();
+        assertThat(node.getTextLineBaselines()).asList().containsExactly(16, 23, 42).inOrder();
+    }
+
+    /**
+     * Asserts the properties of a {@link ViewStructureImpl} that was created by
+     * {@link #newStructureWithComplexText()}.
+     */
+    private void assertStructureWithComplexText(ViewStructureImpl structure) {
+        assertThat(structure.getText()).isEqualTo("Now we're talking!");
+        assertThat(structure.getTextSelectionStart()).isEqualTo(4);
+        assertThat(structure.getTextSelectionEnd()).isEqualTo(8);
+        assertThat(structure.getHint()).isEqualTo("Soylent Green is SPOILER ALERT");
+    }
+
+    private ViewNode cloneThroughParcel(ViewNode node) {
+        Parcel parcel = Parcel.obtain();
+
+        try {
+            // Write to parcel
+            parcel.setDataPosition(0); // Sanity / paranoid check
+            ViewNode.writeToParcel(parcel, node, 0);
+
+            // Read from parcel
+            parcel.setDataPosition(0);
+            ViewNode clone = ViewNode.readFromParcel(parcel);
+            assertThat(clone).isNotNull();
+            return clone;
+        } finally {
+            parcel.recycle();
+        }
+    }
+}