Merge "Add test for autoFinishing sessions." into oc-dev
diff --git a/tests/autofillservice/AndroidManifest.xml b/tests/autofillservice/AndroidManifest.xml
index 6e3d44992..32bf5a5 100644
--- a/tests/autofillservice/AndroidManifest.xml
+++ b/tests/autofillservice/AndroidManifest.xml
@@ -54,6 +54,7 @@
         <activity android:name=".DummyActivity"/>
         <activity android:name=".OutOfProcessLoginActivity"
             android:process="android.autofillservice.cts.outside"/>
+        <activity android:name=".FragmentContainerActivity" />
 
         <service
             android:name=".InstrumentedAutoFillService"
diff --git a/tests/autofillservice/res/layout/fragment_container.xml b/tests/autofillservice/res/layout/fragment_container.xml
new file mode 100644
index 0000000..156efad
--- /dev/null
+++ b/tests/autofillservice/res/layout/fragment_container.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  * Copyright (C) 2017 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.
+  -->
+
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:orientation="vertical"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:id="@+id/rootContainer" />
diff --git a/tests/autofillservice/res/layout/fragment_with_edittext.xml b/tests/autofillservice/res/layout/fragment_with_edittext.xml
new file mode 100644
index 0000000..e0d4584
--- /dev/null
+++ b/tests/autofillservice/res/layout/fragment_with_edittext.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  * Copyright (C) 2017 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.
+  -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:orientation="vertical"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <EditText android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:id="@+id/editText1" />
+
+    <EditText android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:id="@+id/editText2" />
+</LinearLayout>
diff --git a/tests/autofillservice/src/android/autofillservice/cts/AutoFinishSessionTest.java b/tests/autofillservice/src/android/autofillservice/cts/AutoFinishSessionTest.java
new file mode 100644
index 0000000..e4f081b
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/AutoFinishSessionTest.java
@@ -0,0 +1,298 @@
+/*
+ * Copyright (C) 2017 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.autofillservice.cts;
+
+import static android.autofillservice.cts.FragmentContainerActivity.FRAGMENT_TAG;
+import static android.autofillservice.cts.Helper.FILL_TIMEOUT_MS;
+import static android.autofillservice.cts.Helper.eventually;
+import static android.autofillservice.cts.Helper.findNodeByResourceId;
+import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_GENERIC;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.app.Fragment;
+import android.autofillservice.cts.InstrumentedAutoFillService.SaveRequest;
+import android.content.Intent;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.test.rule.ActivityTestRule;
+import android.view.ViewGroup;
+import android.widget.EditText;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+
+/**
+ * Tests that the session finishes when the views and fragments go away
+ */
+public class AutoFinishSessionTest extends AutoFillServiceTestCase {
+    @Rule
+    public final ActivityTestRule<FragmentContainerActivity> mActivityRule =
+            new ActivityTestRule<>(FragmentContainerActivity.class);
+    private FragmentContainerActivity mActivity;
+    private EditText mEditText1;
+    private EditText mEditText2;
+    private Fragment mFragment;
+    private ViewGroup mParent;
+
+    @Before
+    public void initViews() {
+        mActivity = mActivityRule.getActivity();
+        mEditText1 = mActivity.findViewById(R.id.editText1);
+        mEditText2 = mActivity.findViewById(R.id.editText2);
+        mFragment = mActivity.getFragmentManager().findFragmentByTag(FRAGMENT_TAG);
+        mParent = ((ViewGroup) mEditText1.getParent());
+
+        assertThat(mFragment).isNotNull();
+    }
+
+    private void removeViewsBaseTest(@NonNull Runnable firstRemove, @Nullable Runnable firstCheck,
+            @Nullable Runnable secondRemove, String... viewsToSave)
+            throws Exception {
+        enableService();
+        try {
+            // Set expectations.
+            sReplier.addResponse(new CannedFillResponse.Builder()
+                    .setSaveOnAllViewsInvisible(true)
+                    .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, viewsToSave).build());
+
+            // Trigger autofill
+            eventually(() -> {
+                mActivity.syncRunOnUiThread(() -> mEditText2.requestFocus());
+                mActivity.syncRunOnUiThread(() -> mEditText1.requestFocus());
+
+                try {
+                    sReplier.getNextFillRequest();
+                } catch (InterruptedException e) {
+                    throw new RuntimeException(e);
+                }
+            }, (int) (FILL_TIMEOUT_MS * 2));
+
+            sUiBot.assertNoDatasets();
+
+            // remove first set of views
+            mActivity.syncRunOnUiThread(() -> {
+                mEditText1.setText("editText1-filled");
+                mEditText2.setText("editText2-filled");
+            });
+            firstRemove.run();
+
+            // Check state between remove operations
+            if (firstCheck != null) {
+                firstCheck.run();
+            }
+
+            // remove second set of views
+            if (secondRemove != null) {
+                secondRemove.run();
+            }
+
+            // Save should be shows after all remove operations were executed
+            sUiBot.saveForAutofill(true, SAVE_DATA_TYPE_GENERIC);
+
+            SaveRequest saveRequest = sReplier.getNextSaveRequest();
+            for (String view : viewsToSave) {
+                assertThat(findNodeByResourceId(saveRequest.structure, view)
+                        .getAutofillValue().getTextValue().toString()).isEqualTo(view + "-filled");
+            }
+        } finally {
+            disableService();
+        }
+    }
+
+    @Test
+    public void removeBothViewsToFinishSession() throws Exception {
+        removeViewsBaseTest(
+                () -> mActivity.syncRunOnUiThread(
+                        () -> ((ViewGroup) mEditText1.getParent()).removeView(mEditText1)),
+                () -> sUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_GENERIC),
+                () -> mActivity.syncRunOnUiThread(
+                        () -> ((ViewGroup) mEditText2.getParent()).removeView(mEditText2)),
+                "editText1", "editText2");
+    }
+
+    @Test
+    public void removeOneViewToFinishSession() throws Exception {
+        removeViewsBaseTest(
+                () -> mActivity.syncRunOnUiThread(() -> {
+                    // Do not trigger new partition when switching to editText2
+                    mEditText2.setFocusable(false);
+
+                    mParent.removeView(mEditText1);
+                }),
+                null,
+                null,
+                "editText1");
+    }
+
+    @Test
+    public void hideOneViewToFinishSession() throws Exception {
+        removeViewsBaseTest(
+                () -> mActivity.syncRunOnUiThread(
+                        () -> mEditText1.setVisibility(ViewGroup.INVISIBLE)),
+                null,
+                null,
+                "editText1");
+    }
+
+    @Test
+    public void removeFragmentToFinishSession() throws Exception {
+        removeViewsBaseTest(
+                () -> mActivity.syncRunOnUiThread(
+                        () -> mActivity.getFragmentManager().beginTransaction().remove(
+                                mFragment).commitNow()),
+                null,
+                null,
+                "editText1", "editText2");
+    }
+
+    @Test
+    public void removeParentToFinishSession() throws Exception {
+        removeViewsBaseTest(
+                () -> mActivity.syncRunOnUiThread(
+                        () -> ((ViewGroup) mParent.getParent()).removeView(mParent)),
+                null,
+                null,
+                "editText1", "editText2");
+    }
+
+    @Test
+    public void hideParentToFinishSession() throws Exception {
+        removeViewsBaseTest(
+                () -> mActivity.syncRunOnUiThread(() -> mParent.setVisibility(ViewGroup.INVISIBLE)),
+                null,
+                null,
+                "editText1", "editText2");
+    }
+
+    /**
+     * An activity that is currently getting autofilled might go into the background. While the
+     * tracked views are not visible on the screen anymore, this should not trigger a save.
+     */
+    public void activityToBackgroundShouldNotTriggerSave(@Nullable Runnable removeInBackGround,
+            @Nullable Runnable removeInForeGroup) throws Exception {
+        enableService();
+        try {
+            // Set expectations.
+            sReplier.addResponse(new CannedFillResponse.Builder()
+                    .setSaveOnAllViewsInvisible(true)
+                    .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, "editText1").build());
+
+            // Trigger autofill
+            eventually(() -> {
+                mActivity.syncRunOnUiThread(() -> mEditText2.requestFocus());
+                mActivity.syncRunOnUiThread(() -> mEditText1.requestFocus());
+
+                try {
+                    sReplier.getNextFillRequest();
+                } catch (InterruptedException e) {
+                    throw new RuntimeException(e);
+                }
+            }, (int) (FILL_TIMEOUT_MS * 2));
+
+            sUiBot.assertNoDatasets();
+
+            mActivity.syncRunOnUiThread(() -> {
+                mEditText1.setText("editText1-filled");
+                mEditText2.setText("editText2-filled");
+            });
+
+            // Start activity on top
+            mActivity.startActivity(new Intent(getContext(),
+                    ManualAuthenticationActivity.class));
+            mActivity.waitUntilStopped();
+
+            if (removeInBackGround != null) {
+                removeInBackGround.run();
+            }
+
+            sUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_GENERIC);
+
+            // Remove previously started activity from top
+            sUiBot.selectById("android.autofillservice.cts:id/button");
+            mActivity.waitUntilResumed();
+
+            if (removeInForeGroup != null) {
+                sUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_GENERIC);
+
+                removeInForeGroup.run();
+            }
+
+            // Save should be shows after all remove operations were executed
+            sUiBot.saveForAutofill(true, SAVE_DATA_TYPE_GENERIC);
+
+            SaveRequest saveRequest = sReplier.getNextSaveRequest();
+            assertThat(findNodeByResourceId(saveRequest.structure, "editText1")
+                    .getAutofillValue().getTextValue().toString()).isEqualTo("editText1-filled");
+        } finally {
+            disableService();
+        }
+    }
+
+    @Test
+    public void removeViewInBackground() throws Exception {
+        activityToBackgroundShouldNotTriggerSave(
+                () -> mActivity.syncRunOnUiThread(() -> {
+                    // Do not trigger new partition when switching to editText2
+                    mEditText2.setFocusable(false);
+
+                    mParent.removeView(mEditText1);
+                }),
+                null);
+    }
+
+    @Test
+    public void hideViewInBackground() throws Exception {
+        activityToBackgroundShouldNotTriggerSave(
+                () -> mActivity.syncRunOnUiThread(() -> {
+                    // Do not trigger new partition when switching to editText2
+                    mEditText2.setFocusable(false);
+
+                    mEditText1.setVisibility(ViewGroup.INVISIBLE);
+                }),
+                null);
+    }
+
+    @Test
+    public void hideParentInBackground() throws Exception {
+        activityToBackgroundShouldNotTriggerSave(
+                () -> mActivity.syncRunOnUiThread(() -> mParent.setVisibility(ViewGroup.INVISIBLE)),
+                null);
+    }
+
+    @Test
+    public void removeParentInBackground() throws Exception {
+        activityToBackgroundShouldNotTriggerSave(
+                () -> mActivity.syncRunOnUiThread(
+                        () -> ((ViewGroup) mParent.getParent()).removeView(mParent)),
+                null);
+    }
+
+    @Test
+    public void removeViewAfterBackground() throws Exception {
+        activityToBackgroundShouldNotTriggerSave(
+                () -> mActivity.syncRunOnUiThread(() -> {
+                    // Do not trigger new fill request when closing activity
+                    mEditText1.setFocusable(false);
+                    mEditText2.setFocusable(false);
+                }),
+                () -> mActivity.syncRunOnUiThread(() -> {
+                    mParent.removeView(mEditText1);
+                }));
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/CannedFillResponse.java b/tests/autofillservice/src/android/autofillservice/cts/CannedFillResponse.java
index 2fa29ce..0ceab53 100644
--- a/tests/autofillservice/src/android/autofillservice/cts/CannedFillResponse.java
+++ b/tests/autofillservice/src/android/autofillservice/cts/CannedFillResponse.java
@@ -66,6 +66,7 @@
     private final String[] mAuthenticationIds;
     private final CharSequence mNegativeActionLabel;
     private final IntentSender mNegativeActionListener;
+    private final boolean mSaveOnAllViewsInvisible;
 
     private CannedFillResponse(Builder builder) {
         mDatasets = builder.mDatasets;
@@ -79,6 +80,7 @@
         mAuthenticationIds = builder.mAuthenticationIds;
         mNegativeActionLabel = builder.mNegativeActionLabel;
         mNegativeActionListener = builder.mNegativeActionListener;
+        mSaveOnAllViewsInvisible = builder.mSaveOnAllViewsInvisible;
     }
 
     /**
@@ -100,9 +102,18 @@
                 builder.addDataset(dataset);
             }
         }
-        if (mRequiredSavableIds != null) {
-            final SaveInfo.Builder saveInfo = new SaveInfo.Builder(mSaveType,
-                    getAutofillIds(structure, mRequiredSavableIds));
+        if (mRequiredSavableIds != null || mSaveOnAllViewsInvisible) {
+            final SaveInfo.Builder saveInfo;
+
+            if (mRequiredSavableIds == null) {
+                saveInfo = new SaveInfo.Builder(mSaveType, null);
+            } else {
+                saveInfo = new SaveInfo.Builder(mSaveType,
+                        getAutofillIds(structure, mRequiredSavableIds));
+            }
+
+            saveInfo.setSaveOnAllViewsInvisible(mSaveOnAllViewsInvisible);
+
             if (mOptionalSavableIds != null) {
                 saveInfo.setOptionalIds(getAutofillIds(structure, mOptionalSavableIds));
             }
@@ -126,6 +137,7 @@
         return "CannedFillResponse: [datasets=" + mDatasets
                 + ", requiredSavableIds=" + Arrays.toString(mRequiredSavableIds)
                 + ", optionalSavableIds=" + Arrays.toString(mOptionalSavableIds)
+                + ", saveOnAllViewsInvisible=" + mSaveOnAllViewsInvisible
                 + ", saveDescription=" + mSaveDescription
                 + ", hasPresentation=" + (mPresentation != null)
                 + ", hasAuthentication=" + (mAuthentication != null)
@@ -145,6 +157,7 @@
         private String[] mAuthenticationIds;
         private CharSequence mNegativeActionLabel;
         private IntentSender mNegativeActionListener;
+        private boolean mSaveOnAllViewsInvisible;
 
         public Builder addDataset(CannedDataset dataset) {
             mDatasets.add(dataset);
@@ -161,6 +174,14 @@
         }
 
         /**
+         * Sets the saveOnAllViewsInvisible flag
+         */
+        public Builder setSaveOnAllViewsInvisible(boolean saveOnAllViewsInvisible) {
+            mSaveOnAllViewsInvisible = saveOnAllViewsInvisible;
+            return this;
+        }
+
+        /**
          * Sets the optional savable ids based on they {@code resourceId}.
          */
         public Builder setOptionalSavableIds(String... ids) {
diff --git a/tests/autofillservice/src/android/autofillservice/cts/FragmentContainerActivity.java b/tests/autofillservice/src/android/autofillservice/cts/FragmentContainerActivity.java
new file mode 100644
index 0000000..7be4496
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/FragmentContainerActivity.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2017 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.autofillservice.cts;
+
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Activity containing an fragment
+ */
+public class FragmentContainerActivity extends AbstractAutoFillActivity {
+    static final String FRAGMENT_TAG =
+            FragmentContainerActivity.class.getName() + "#FRAGMENT_TAG";
+    private CountDownLatch mResumed = new CountDownLatch(1);
+    private CountDownLatch mStopped = new CountDownLatch(0);
+
+    @Override
+    protected void onCreate(@Nullable Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        setContentView(R.layout.fragment_container);
+
+        // have to manually add fragment as we cannot remove it otherwise
+        getFragmentManager().beginTransaction().add(R.id.rootContainer,
+                new FragmentWithEditText(), FRAGMENT_TAG).commitNow();
+    }
+
+    @Override
+    protected void onStart() {
+        super.onStart();
+
+        mStopped = new CountDownLatch(1);
+    }
+
+    @Override
+    protected void onResume() {
+        super.onResume();
+
+        mResumed.countDown();
+    }
+
+    @Override
+    protected void onPause() {
+        super.onPause();
+
+        mResumed = new CountDownLatch(1);
+    }
+
+    @Override
+    protected void onStop() {
+        super.onStop();
+
+        mStopped.countDown();
+    }
+
+    public boolean waitUntilResumed() throws InterruptedException {
+        return mResumed.await(Helper.UI_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+    }
+
+    public boolean waitUntilStopped() throws InterruptedException {
+        return mStopped.await(Helper.UI_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/FragmentWithEditText.java b/tests/autofillservice/src/android/autofillservice/cts/FragmentWithEditText.java
new file mode 100644
index 0000000..963daff
--- /dev/null
+++ b/tests/autofillservice/src/android/autofillservice/cts/FragmentWithEditText.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2017 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.autofillservice.cts;
+
+import android.support.annotation.Nullable;
+import android.app.Fragment;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.EditText;
+
+/**
+ * A fragment with containing a single {@link EditText}
+ */
+public class FragmentWithEditText extends Fragment {
+    @Override
+    @Nullable public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
+            Bundle savedInstanceState) {
+        return inflater.inflate(R.layout.fragment_with_edittext, null);
+    }
+}
diff --git a/tests/autofillservice/src/android/autofillservice/cts/ManualAuthenticationActivity.java b/tests/autofillservice/src/android/autofillservice/cts/ManualAuthenticationActivity.java
index c2af4cc1..32844d4 100644
--- a/tests/autofillservice/src/android/autofillservice/cts/ManualAuthenticationActivity.java
+++ b/tests/autofillservice/src/android/autofillservice/cts/ManualAuthenticationActivity.java
@@ -49,25 +49,24 @@
         setContentView(R.layout.single_button_activity);
 
         findViewById(R.id.button).setOnClickListener((v) -> {
-            // We should get the assist structure
             AssistStructure structure = getIntent().getParcelableExtra(
                     AutofillManager.EXTRA_ASSIST_STRUCTURE);
-            assertWithMessage("structure not called").that(structure).isNotNull();
+            if (structure != null) {
+                Parcelable result;
+                if (sResponse != null) {
+                    result = sResponse.asFillResponse(structure);
+                } else if (sDataset != null) {
+                    result = sDataset.asDataset(structure);
+                } else {
+                    throw new IllegalStateException("no dataset or response");
+                }
 
-            Parcelable result = null;
-            if (sResponse != null) {
-                result = sResponse.asFillResponse(structure);
-            } else if (sDataset != null) {
-                result = sDataset.asDataset(structure);
-            } else {
-                throw new IllegalStateException("no dataset or response");
+                // Pass on the auth result
+                Intent intent = new Intent();
+                intent.putExtra(AutofillManager.EXTRA_AUTHENTICATION_RESULT, result);
+                setResult(RESULT_OK, intent);
             }
 
-            // Pass on the auth result
-            Intent intent = new Intent();
-            intent.putExtra(AutofillManager.EXTRA_AUTHENTICATION_RESULT, result);
-            setResult(RESULT_OK, intent);
-
             // Done
             finish();
         });