Merge "Added an (optional) session id to Autofill id."
diff --git a/core/java/android/view/autofill/AutofillId.java b/core/java/android/view/autofill/AutofillId.java
index cb1d89c..9c935af 100644
--- a/core/java/android/view/autofill/AutofillId.java
+++ b/core/java/android/view/autofill/AutofillId.java
@@ -15,6 +15,7 @@
  */
 package android.view.autofill;
 
+import android.annotation.NonNull;
 import android.annotation.TestApi;
 import android.os.Parcel;
 import android.os.Parcelable;
@@ -25,33 +26,47 @@
  */
 public final class AutofillId implements Parcelable {
 
+    /** @hide */
+    public static final int NO_SESSION = 0;
+
+    private static final int FLAG_IS_VIRTUAL = 0x1;
+    private static final int FLAG_HAS_SESSION = 0x2;
+
     private final int mViewId;
-    private final boolean mVirtual;
+    private final int mFlags;
     private final int mVirtualId;
+    private final int mSessionId;
 
     /** @hide */
     @TestApi
     public AutofillId(int id) {
-        mVirtual = false;
-        mViewId = id;
-        mVirtualId = View.NO_ID;
+        this(/* flags= */ 0, id, View.NO_ID, NO_SESSION);
     }
 
     /** @hide */
     @TestApi
-    public AutofillId(AutofillId parent, int virtualChildId) {
-        mVirtual = true;
-        mViewId = parent.mViewId;
-        mVirtualId = virtualChildId;
+    public AutofillId(@NonNull AutofillId parent, int virtualChildId) {
+        this(FLAG_IS_VIRTUAL, parent.mViewId, virtualChildId, NO_SESSION);
     }
 
     /** @hide */
     public AutofillId(int parentId, int virtualChildId) {
-        mVirtual = true;
+        this(FLAG_IS_VIRTUAL, parentId, virtualChildId, NO_SESSION);
+    }
+
+    /** @hide */
+    public AutofillId(@NonNull AutofillId parent, int virtualChildId, int sessionId) {
+        this(FLAG_IS_VIRTUAL | FLAG_HAS_SESSION, parent.mViewId, virtualChildId, sessionId);
+    }
+
+    private AutofillId(int flags, int parentId, int virtualChildId, int sessionId) {
+        mFlags = flags;
         mViewId = parentId;
         mVirtualId = virtualChildId;
+        mSessionId = sessionId;
     }
 
+
     /** @hide */
     public int getViewId() {
         return mViewId;
@@ -64,7 +79,16 @@
 
     /** @hide */
     public boolean isVirtual() {
-        return mVirtual;
+        return (mFlags & FLAG_IS_VIRTUAL) != 0;
+    }
+
+    private boolean hasSession() {
+        return (mFlags & FLAG_HAS_SESSION) != 0;
+    }
+
+    /** @hide */
+    public int getSessionId() {
+        return mSessionId;
     }
 
     /////////////////////////////////
@@ -77,6 +101,7 @@
         int result = 1;
         result = prime * result + mViewId;
         result = prime * result + mVirtualId;
+        result = prime * result + mSessionId;
         return result;
     }
 
@@ -88,15 +113,19 @@
         final AutofillId other = (AutofillId) obj;
         if (mViewId != other.mViewId) return false;
         if (mVirtualId != other.mVirtualId) return false;
+        if (mSessionId != other.mSessionId) return false;
         return true;
     }
 
     @Override
     public String toString() {
         final StringBuilder builder = new StringBuilder().append(mViewId);
-        if (mVirtual) {
+        if (isVirtual()) {
             builder.append(':').append(mVirtualId);
         }
+        if (hasSession()) {
+            builder.append('@').append(mSessionId);
+        }
         return builder.toString();
     }
 
@@ -108,21 +137,24 @@
     @Override
     public void writeToParcel(Parcel parcel, int flags) {
         parcel.writeInt(mViewId);
-        parcel.writeInt(mVirtual ? 1 : 0);
-        parcel.writeInt(mVirtualId);
-    }
-
-    private AutofillId(Parcel parcel) {
-        mViewId = parcel.readInt();
-        mVirtual = parcel.readInt() == 1;
-        mVirtualId = parcel.readInt();
+        parcel.writeInt(mFlags);
+        if (isVirtual()) {
+            parcel.writeInt(mVirtualId);
+        }
+        if (hasSession()) {
+            parcel.writeInt(mSessionId);
+        }
     }
 
     public static final Parcelable.Creator<AutofillId> CREATOR =
             new Parcelable.Creator<AutofillId>() {
         @Override
         public AutofillId createFromParcel(Parcel source) {
-            return new AutofillId(source);
+            final int viewId = source.readInt();
+            final int flags = source.readInt();
+            final int virtualId = (flags & FLAG_IS_VIRTUAL) != 0 ? source.readInt() : View.NO_ID;
+            final int sessionId = (flags & FLAG_HAS_SESSION) != 0 ? source.readInt() : NO_SESSION;
+            return new AutofillId(flags, viewId, virtualId, sessionId);
         }
 
         @Override
diff --git a/core/java/android/view/contentcapture/ContentCaptureSession.java b/core/java/android/view/contentcapture/ContentCaptureSession.java
index 6890beaf..d9a8416 100644
--- a/core/java/android/view/contentcapture/ContentCaptureSession.java
+++ b/core/java/android/view/contentcapture/ContentCaptureSession.java
@@ -28,6 +28,7 @@
 import android.view.contentcapture.ViewNode.ViewStructureImpl;
 
 import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.Preconditions;
 
 import dalvik.system.CloseGuard;
@@ -107,7 +108,7 @@
 
     /** @hide */
     @Nullable
-    protected final String mId = UUID.randomUUID().toString();
+    protected final String mId;
 
     private int mState = STATE_UNKNOWN;
 
@@ -123,6 +124,13 @@
 
     /** @hide */
     protected ContentCaptureSession() {
+        this(UUID.randomUUID().toString());
+    }
+
+    /** @hide */
+    @VisibleForTesting
+    public ContentCaptureSession(@NonNull String id) {
+        mId = Preconditions.checkNotNull(id);
         mCloseGuard.open("destroy");
     }
 
@@ -140,6 +148,13 @@
         return mContentCaptureSessionId;
     }
 
+    /** @hide */
+    @VisibleForTesting
+    public int getIdAsInt() {
+        // TODO(b/121197119): use sessionId instead of hashcode once it's changed to int
+        return mId.hashCode();
+    }
+
     /**
      * Creates a new {@link ContentCaptureSession}.
      *
@@ -315,9 +330,7 @@
     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);
+        return new AutofillId(parentId, virtualChildId, getIdAsInt());
     }
 
     /**
@@ -333,8 +346,7 @@
     @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);
+        return new ViewNode.ViewStructureImpl(parentId, virtualId, getIdAsInt());
     }
 
     boolean isContentCaptureEnabled() {
diff --git a/core/java/android/view/contentcapture/ViewNode.java b/core/java/android/view/contentcapture/ViewNode.java
index b7a486a..ddfecb0 100644
--- a/core/java/android/view/contentcapture/ViewNode.java
+++ b/core/java/android/view/contentcapture/ViewNode.java
@@ -672,9 +672,9 @@
         }
 
         @VisibleForTesting // Must be public to be accessed from FrameworkCoreTests' apk.
-        public ViewStructureImpl(@NonNull AutofillId parentId, int virtualId) {
+        public ViewStructureImpl(@NonNull AutofillId parentId, int virtualId, int sessionId) {
             mNode.mParentAutofillId = Preconditions.checkNotNull(parentId);
-            mNode.mAutofillId = new AutofillId(parentId, virtualId);
+            mNode.mAutofillId = new AutofillId(parentId, virtualId, sessionId);
         }
 
         @VisibleForTesting // Must be public to be accessed from FrameworkCoreTests' apk.
diff --git a/core/tests/coretests/src/android/view/autofill/AutofillIdTest.java b/core/tests/coretests/src/android/view/autofill/AutofillIdTest.java
new file mode 100644
index 0000000..7619af2
--- /dev/null
+++ b/core/tests/coretests/src/android/view/autofill/AutofillIdTest.java
@@ -0,0 +1,150 @@
+/*
+ * 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.autofill;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.testng.Assert.assertThrows;
+
+import android.os.Parcel;
+import android.view.View;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class AutofillIdTest {
+
+    @Test
+    public void testNonVirtual() {
+        final AutofillId id = new AutofillId(42);
+        assertThat(id.getViewId()).isEqualTo(42);
+        assertThat(id.isVirtual()).isFalse();
+        assertThat(id.getVirtualChildId()).isEqualTo(View.NO_ID);
+
+        final AutofillId clone = cloneThroughParcel(id);
+        assertThat(clone.getViewId()).isEqualTo(42);
+        assertThat(clone.isVirtual()).isFalse();
+        assertThat(clone.getVirtualChildId()).isEqualTo(View.NO_ID);
+    }
+
+    @Test
+    public void testVirtual() {
+        final AutofillId id = new AutofillId(42, 108);
+        assertThat(id.getViewId()).isEqualTo(42);
+        assertThat(id.isVirtual()).isTrue();
+        assertThat(id.getVirtualChildId()).isEqualTo(108);
+
+        final AutofillId clone = cloneThroughParcel(id);
+        assertThat(clone.getViewId()).isEqualTo(42);
+        assertThat(clone.isVirtual()).isTrue();
+        assertThat(clone.getVirtualChildId()).isEqualTo(108);
+    }
+
+    @Test
+    public void testVirtual_parentObjectConstructor() {
+        assertThrows(NullPointerException.class, () -> new AutofillId(null, 108));
+
+        final AutofillId id = new AutofillId(new AutofillId(42), 108);
+        assertThat(id.getViewId()).isEqualTo(42);
+        assertThat(id.isVirtual()).isTrue();
+        assertThat(id.getVirtualChildId()).isEqualTo(108);
+
+        final AutofillId clone = cloneThroughParcel(id);
+        assertThat(clone.getViewId()).isEqualTo(42);
+        assertThat(clone.isVirtual()).isTrue();
+        assertThat(clone.getVirtualChildId()).isEqualTo(108);
+    }
+
+    @Test
+    public void testVirtual_withSession() {
+        final AutofillId id = new AutofillId(new AutofillId(42), 108, 666);
+        assertThat(id.getViewId()).isEqualTo(42);
+        assertThat(id.isVirtual()).isTrue();
+        assertThat(id.getVirtualChildId()).isEqualTo(108);
+        assertThat(id.getSessionId()).isEqualTo(666);
+
+        final AutofillId clone = cloneThroughParcel(id);
+        assertThat(clone.getViewId()).isEqualTo(42);
+        assertThat(clone.isVirtual()).isTrue();
+        assertThat(clone.getVirtualChildId()).isEqualTo(108);
+        assertThat(clone.getSessionId()).isEqualTo(666);
+    }
+
+    @Test
+    public void testEqualsHashCode() {
+        final AutofillId realId = new AutofillId(42);
+        final AutofillId realIdSame = new AutofillId(42);
+        assertThat(realId).isEqualTo(realIdSame);
+        assertThat(realIdSame).isEqualTo(realId);
+        assertThat(realId.hashCode()).isEqualTo(realIdSame.hashCode());
+
+        final AutofillId realIdDifferent = new AutofillId(108);
+        assertThat(realId).isNotEqualTo(realIdDifferent);
+        assertThat(realIdDifferent).isNotEqualTo(realId);
+
+        final AutofillId virtualId = new AutofillId(42, 1);
+        final AutofillId virtualIdSame = new AutofillId(42, 1);
+        assertThat(virtualId).isEqualTo(virtualIdSame);
+        assertThat(virtualIdSame).isEqualTo(virtualId);
+        assertThat(virtualId.hashCode()).isEqualTo(virtualIdSame.hashCode());
+        assertThat(virtualId).isNotEqualTo(realId);
+        assertThat(realId).isNotEqualTo(virtualId);
+
+        final AutofillId virtualIdDifferentChild = new AutofillId(42, 2);
+        assertThat(virtualIdDifferentChild).isNotEqualTo(virtualId);
+        assertThat(virtualId).isNotEqualTo(virtualIdDifferentChild);
+        assertThat(virtualIdDifferentChild).isNotEqualTo(realId);
+        assertThat(realId).isNotEqualTo(virtualIdDifferentChild);
+
+        final AutofillId virtualIdDifferentParent = new AutofillId(108, 1);
+        assertThat(virtualIdDifferentParent).isNotEqualTo(virtualId);
+        assertThat(virtualId).isNotEqualTo(virtualIdDifferentParent);
+        assertThat(virtualIdDifferentParent).isNotEqualTo(virtualIdDifferentChild);
+        assertThat(virtualIdDifferentChild).isNotEqualTo(virtualIdDifferentParent);
+
+        final AutofillId virtualIdDifferentSession = new AutofillId(new AutofillId(42), 1, 108);
+        assertThat(virtualIdDifferentSession).isNotEqualTo(virtualId);
+        assertThat(virtualId).isNotEqualTo(virtualIdDifferentSession);
+        assertThat(virtualIdDifferentSession).isNotEqualTo(realId);
+        assertThat(realId).isNotEqualTo(virtualIdDifferentSession);
+
+        final AutofillId sameVirtualIdDifferentSession = new AutofillId(new AutofillId(42), 1, 108);
+        assertThat(sameVirtualIdDifferentSession).isEqualTo(virtualIdDifferentSession);
+        assertThat(virtualIdDifferentSession).isEqualTo(sameVirtualIdDifferentSession);
+        assertThat(sameVirtualIdDifferentSession.hashCode())
+                .isEqualTo(virtualIdDifferentSession.hashCode());
+    }
+
+    private AutofillId cloneThroughParcel(AutofillId id) {
+        Parcel parcel = Parcel.obtain();
+
+        try {
+            // Write to parcel
+            parcel.setDataPosition(0); // Sanity / paranoid check
+            id.writeToParcel(parcel, 0);
+
+            // Read from parcel
+            parcel.setDataPosition(0);
+            AutofillId clone = AutofillId.CREATOR.createFromParcel(parcel);
+            assertThat(clone).isNotNull();
+            return clone;
+        } finally {
+            parcel.recycle();
+        }
+    }
+}
diff --git a/core/tests/coretests/src/android/view/contentcapture/ContentCaptureSessionTest.java b/core/tests/coretests/src/android/view/contentcapture/ContentCaptureSessionTest.java
index 59f3a4c..73cceae 100644
--- a/core/tests/coretests/src/android/view/contentcapture/ContentCaptureSessionTest.java
+++ b/core/tests/coretests/src/android/view/contentcapture/ContentCaptureSessionTest.java
@@ -19,11 +19,14 @@
 
 import static org.testng.Assert.assertThrows;
 
+import android.view.View;
+import android.view.ViewStructure;
 import android.view.autofill.AutofillId;
+import android.view.contentcapture.ViewNode.ViewStructureImpl;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.mockito.Spy;
+import org.mockito.Mock;
 import org.mockito.junit.MockitoJUnitRunner;
 
 /**
@@ -35,34 +38,104 @@
 @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;
+    private ContentCaptureSession mSession1 = new MyContentCaptureSession("111");
+
+    private ContentCaptureSession mSession2 = new MyContentCaptureSession("2222");
+
+    @Mock
+    private View mMockView;
 
     @Test
     public void testNewAutofillId_invalid() {
-        assertThrows(NullPointerException.class, () -> mMockSession.newAutofillId(null, 42));
+        assertThrows(NullPointerException.class, () -> mSession1.newAutofillId(null, 42));
         assertThrows(IllegalArgumentException.class,
-                () -> mMockSession.newAutofillId(new AutofillId(42, 42), 42));
+                () -> mSession1.newAutofillId(new AutofillId(42, 42), 42));
     }
 
     @Test
     public void testNewAutofillId_valid() {
         final AutofillId parentId = new AutofillId(42);
-        final AutofillId childId = mMockSession.newAutofillId(parentId, 108);
+        final AutofillId childId = mSession1.newAutofillId(parentId, 108);
         assertThat(childId.getViewId()).isEqualTo(42);
         assertThat(childId.getVirtualChildId()).isEqualTo(108);
-        // TODO(b/121197119): assert session id
+        assertThat(childId.getSessionId()).isEqualTo(mSession1.getIdAsInt());
+    }
+
+    @Test
+    public void testNewAutofillId_differentSessions() {
+        assertThat(mSession1.getIdAsInt()).isNotSameAs(mSession2.getIdAsInt()); //sanity check
+        final AutofillId parentId = new AutofillId(42);
+        final AutofillId childId1 = mSession1.newAutofillId(parentId, 108);
+        final AutofillId childId2 = mSession2.newAutofillId(parentId, 108);
+        assertThat(childId1).isNotEqualTo(childId2);
+        assertThat(childId2).isNotEqualTo(childId1);
     }
 
     @Test
     public void testNotifyXXX_null() {
-        assertThrows(NullPointerException.class, () -> mMockSession.notifyViewAppeared(null));
-        assertThrows(NullPointerException.class, () -> mMockSession.notifyViewDisappeared(null));
+        assertThrows(NullPointerException.class, () -> mSession1.notifyViewAppeared(null));
+        assertThrows(NullPointerException.class, () -> mSession1.notifyViewDisappeared(null));
         assertThrows(NullPointerException.class,
-                () -> mMockSession.notifyViewTextChanged(null, "whatever", 0));
+                () -> mSession1.notifyViewTextChanged(null, "whatever", 0));
+    }
+
+    @Test
+    public void testNewViewStructure() {
+        assertThat(mMockView.getAutofillId()).isNotNull(); // sanity check
+        final ViewStructure structure = mSession1.newViewStructure(mMockView);
+        assertThat(structure).isNotNull();
+        assertThat(structure.getAutofillId()).isEqualTo(mMockView.getAutofillId());
+    }
+
+    @Test
+    public void testNewVirtualViewStructure() {
+        final AutofillId parentId = new AutofillId(42);
+        final ViewStructure structure = mSession1.newVirtualViewStructure(parentId, 108);
+        assertThat(structure).isNotNull();
+        final AutofillId childId = mSession1.newAutofillId(parentId, 108);
+        assertThat(structure.getAutofillId()).isEqualTo(childId);
+    }
+
+    // Cannot use @Spy because we need to pass the session id on constructor
+    private class MyContentCaptureSession extends ContentCaptureSession {
+
+        private MyContentCaptureSession(String id) {
+            super(id);
+        }
+
+        @Override
+        MainContentCaptureSession getMainCaptureSession() {
+            throw new UnsupportedOperationException("should not have been called");
+        }
+
+        @Override
+        ContentCaptureSession newChild(ContentCaptureContext context) {
+            throw new UnsupportedOperationException("should not have been called");
+        }
+
+        @Override
+        void flush() {
+            throw new UnsupportedOperationException("should not have been called");
+        }
+
+        @Override
+        void onDestroy() {
+            throw new UnsupportedOperationException("should not have been called");
+        }
+
+        @Override
+        void internalNotifyViewAppeared(ViewStructureImpl node) {
+            throw new UnsupportedOperationException("should not have been called");
+        }
+
+        @Override
+        void internalNotifyViewDisappeared(AutofillId id) {
+            throw new UnsupportedOperationException("should not have been called");
+        }
+
+        @Override
+        void internalNotifyViewTextChanged(AutofillId id, CharSequence text, int flags) {
+            throw new UnsupportedOperationException("should not have been called");
+        }
     }
 }
diff --git a/core/tests/coretests/src/android/view/contentcapture/ViewNodeTest.java b/core/tests/coretests/src/android/view/contentcapture/ViewNodeTest.java
index 995946b..bbfe01c 100644
--- a/core/tests/coretests/src/android/view/contentcapture/ViewNodeTest.java
+++ b/core/tests/coretests/src/android/view/contentcapture/ViewNodeTest.java
@@ -110,10 +110,10 @@
     @Test
     public void testAutofillIdMethods_explicitIdsConstructor() {
         AutofillId initialParentId = new AutofillId(42);
-        ViewStructureImpl structure = new ViewStructureImpl(initialParentId, 108);
+        ViewStructureImpl structure = new ViewStructureImpl(initialParentId, 108, 666);
         ViewNode node = structure.getNode();
 
-        assertThat(node.getAutofillId()).isEqualTo(new AutofillId(initialParentId, 108));
+        assertThat(node.getAutofillId()).isEqualTo(new AutofillId(initialParentId, 108, 666));
         assertThat(node.getParentAutofillId()).isEqualTo(initialParentId);
 
         AutofillId newChildId = new AutofillId(108);