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();
});