1 /* 2 * Copyright (C) 2017 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.autofillservice.cts; 18 19 import static android.autofillservice.cts.FragmentContainerActivity.FRAGMENT_TAG; 20 import static android.autofillservice.cts.Helper.findNodeByResourceId; 21 import static android.autofillservice.cts.Helper.getContext; 22 import static android.service.autofill.SaveInfo.SAVE_DATA_TYPE_GENERIC; 23 24 import static com.google.common.truth.Truth.assertThat; 25 26 import android.app.Fragment; 27 import android.autofillservice.cts.InstrumentedAutoFillService.SaveRequest; 28 import android.content.Intent; 29 import android.service.autofill.SaveInfo; 30 import androidx.annotation.NonNull; 31 import androidx.annotation.Nullable; 32 import android.view.ViewGroup; 33 import android.widget.EditText; 34 35 import org.junit.Before; 36 import org.junit.Rule; 37 import org.junit.Test; 38 39 import java.util.concurrent.atomic.AtomicReference; 40 41 /** 42 * Tests that the session finishes when the views and fragments go away 43 */ 44 public class AutoFinishSessionTest extends AutoFillServiceTestCase { 45 46 private static final String ID_BUTTON = "button"; 47 48 @Rule 49 public final AutofillActivityTestRule<FragmentContainerActivity> mActivityRule = 50 new AutofillActivityTestRule<>(FragmentContainerActivity.class); 51 private FragmentContainerActivity mActivity; 52 private EditText mEditText1; 53 private EditText mEditText2; 54 private Fragment mFragment; 55 private ViewGroup mParent; 56 57 @Before 58 public void initViews() { 59 mActivity = mActivityRule.getActivity(); 60 mEditText1 = mActivity.findViewById(R.id.editText1); 61 mEditText2 = mActivity.findViewById(R.id.editText2); 62 mFragment = mActivity.getFragmentManager().findFragmentByTag(FRAGMENT_TAG); 63 mParent = ((ViewGroup) mEditText1.getParent()); 64 65 assertThat(mFragment).isNotNull(); 66 } 67 68 // firstRemove and secondRemove run in the UI Thread; firstCheck doesn't 69 private void removeViewsBaseTest(@NonNull Runnable firstRemove, @Nullable Runnable firstCheck, 70 @Nullable Runnable secondRemove, String... viewsToSave) throws Exception { 71 enableService(); 72 73 // Set expectations. 74 sReplier.addResponse(new CannedFillResponse.Builder() 75 .setSaveInfoFlags(SaveInfo.FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE) 76 .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, viewsToSave).build()); 77 78 // Trigger autofill 79 mActivity.syncRunOnUiThread(() -> { 80 mEditText2.requestFocus(); 81 mEditText1.requestFocus(); 82 }); 83 84 sReplier.getNextFillRequest(); 85 86 mUiBot.assertNoDatasetsEver(); 87 88 // remove first set of views 89 mActivity.syncRunOnUiThread(() -> { 90 mEditText1.setText("editText1-filled"); 91 mEditText2.setText("editText2-filled"); 92 firstRemove.run(); 93 }); 94 95 // Check state between remove operations 96 if (firstCheck != null) { 97 firstCheck.run(); 98 } 99 100 // remove second set of views 101 if (secondRemove != null) { 102 mActivity.syncRunOnUiThread(secondRemove); 103 } 104 105 // Save should be shows after all remove operations were executed 106 mUiBot.saveForAutofill(true, SAVE_DATA_TYPE_GENERIC); 107 108 SaveRequest saveRequest = sReplier.getNextSaveRequest(); 109 for (String view : viewsToSave) { 110 assertThat(findNodeByResourceId(saveRequest.structure, view) 111 .getAutofillValue().getTextValue().toString()).isEqualTo(view + "-filled"); 112 } 113 } 114 115 @Test 116 public void removeBothViewsToFinishSession() throws Exception { 117 final AtomicReference<Exception> ref = new AtomicReference<>(); 118 removeViewsBaseTest( 119 () -> ((ViewGroup) mEditText1.getParent()).removeView(mEditText1), 120 () -> assertSaveNotShowing(ref), 121 () -> ((ViewGroup) mEditText2.getParent()).removeView(mEditText2), 122 "editText1", "editText2"); 123 final Exception e = ref.get(); 124 if (e != null) { 125 throw e; 126 } 127 } 128 129 private void assertSaveNotShowing(AtomicReference<Exception> ref) { 130 try { 131 mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_GENERIC); 132 } catch (Exception e) { 133 ref.set(e); 134 } 135 } 136 137 @Test 138 public void removeOneViewToFinishSession() throws Exception { 139 removeViewsBaseTest( 140 () -> { 141 // Do not trigger new partition when switching to editText2 142 mEditText2.setFocusable(false); 143 144 mParent.removeView(mEditText1); 145 }, 146 null, 147 null, 148 "editText1"); 149 } 150 151 @Test 152 public void hideOneViewToFinishSession() throws Exception { 153 removeViewsBaseTest( 154 () -> { 155 // Do not trigger new partition when switching to editText2 156 mEditText2.setFocusable(false); 157 158 mEditText1.setVisibility(ViewGroup.INVISIBLE); 159 }, 160 null, 161 null, 162 "editText1"); 163 } 164 165 @Test 166 public void removeFragmentToFinishSession() throws Exception { 167 removeViewsBaseTest( 168 () -> mActivity.getFragmentManager().beginTransaction().remove( 169 mFragment).commitNow(), 170 null, 171 null, 172 "editText1", "editText2"); 173 } 174 175 @Test 176 public void removeParentToFinishSession() throws Exception { 177 removeViewsBaseTest( 178 () -> ((ViewGroup) mParent.getParent()).removeView(mParent), 179 null, 180 null, 181 "editText1", "editText2"); 182 } 183 184 @Test 185 public void hideParentToFinishSession() throws Exception { 186 removeViewsBaseTest( 187 () -> mParent.setVisibility(ViewGroup.INVISIBLE), 188 null, 189 null, 190 "editText1", "editText2"); 191 } 192 193 /** 194 * An activity that is currently getting autofilled might go into the background. While the 195 * tracked views are not visible on the screen anymore, this should not trigger a save. 196 * 197 * <p>The {@link Runnable}s are synchronously run in the UI thread. 198 */ 199 private void activityToBackgroundShouldNotTriggerSave(@Nullable Runnable removeInBackGround, 200 @Nullable Runnable removeInForeGroup) throws Exception { 201 enableService(); 202 203 // Set expectations. 204 sReplier.addResponse(new CannedFillResponse.Builder() 205 .setSaveInfoFlags(SaveInfo.FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE) 206 .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, "editText1").build()); 207 208 // Trigger autofill 209 mActivity.syncRunOnUiThread(() -> { 210 mEditText2.requestFocus(); 211 mEditText1.requestFocus(); 212 }); 213 214 sReplier.getNextFillRequest(); 215 216 mUiBot.assertNoDatasetsEver(); 217 218 mActivity.syncRunOnUiThread(() -> { 219 mEditText1.setText("editText1-filled"); 220 mEditText2.setText("editText2-filled"); 221 }); 222 223 // Start activity on top 224 mActivity.startActivity(new Intent(getContext(), 225 ManualAuthenticationActivity.class)); 226 mActivity.waitUntilStopped(); 227 228 if (removeInBackGround != null) { 229 mActivity.syncRunOnUiThread(removeInBackGround); 230 } 231 232 mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_GENERIC); 233 234 // Remove previously started activity from top 235 mUiBot.selectByRelativeId(ID_BUTTON); 236 mActivity.waitUntilResumed(); 237 238 if (removeInForeGroup != null) { 239 mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_GENERIC); 240 241 mActivity.syncRunOnUiThread(removeInForeGroup); 242 } 243 244 // Save should be shows after all remove operations were executed 245 mUiBot.saveForAutofill(true, SAVE_DATA_TYPE_GENERIC); 246 247 SaveRequest saveRequest = sReplier.getNextSaveRequest(); 248 assertThat(findNodeByResourceId(saveRequest.structure, "editText1") 249 .getAutofillValue().getTextValue().toString()).isEqualTo("editText1-filled"); 250 } 251 252 @Test 253 public void removeViewInBackground() throws Exception { 254 activityToBackgroundShouldNotTriggerSave( 255 () -> mActivity.syncRunOnUiThread(() -> { 256 // Do not trigger new partition when switching to editText2 257 mEditText2.setFocusable(false); 258 259 mParent.removeView(mEditText1); 260 }), 261 null); 262 } 263 264 @Test 265 public void hideViewInBackground() throws Exception { 266 activityToBackgroundShouldNotTriggerSave(() -> { 267 // Do not trigger new partition when switching to editText2 268 mEditText2.setFocusable(false); 269 270 mEditText1.setVisibility(ViewGroup.INVISIBLE); 271 }, 272 null); 273 } 274 275 @Test 276 public void hideParentInBackground() throws Exception { 277 activityToBackgroundShouldNotTriggerSave(() -> mParent.setVisibility(ViewGroup.INVISIBLE), 278 null); 279 } 280 281 @Test 282 public void removeParentInBackground() throws Exception { 283 activityToBackgroundShouldNotTriggerSave( 284 () -> ((ViewGroup) mParent.getParent()).removeView(mParent), 285 null); 286 } 287 288 @Test 289 public void removeViewAfterBackground() throws Exception { 290 activityToBackgroundShouldNotTriggerSave(() -> { 291 // Do not trigger new fill request when closing activity 292 mEditText1.setFocusable(false); 293 mEditText2.setFocusable(false); 294 }, 295 () -> mParent.removeView(mEditText1)); 296 } 297 } 298