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 android.view.ViewGroup; 31 import android.widget.EditText; 32 33 import androidx.annotation.NonNull; 34 import androidx.annotation.Nullable; 35 36 import org.junit.Test; 37 38 import java.util.concurrent.atomic.AtomicReference; 39 40 /** 41 * Tests that the session finishes when the views and fragments go away 42 */ 43 public class AutoFinishSessionTest 44 extends AutoFillServiceTestCase.AutoActivityLaunch<FragmentContainerActivity> { 45 46 private static final String ID_BUTTON = "button"; 47 48 private FragmentContainerActivity mActivity; 49 private EditText mEditText1; 50 private EditText mEditText2; 51 private Fragment mFragment; 52 private ViewGroup mParent; 53 54 @Override 55 protected AutofillActivityTestRule<FragmentContainerActivity> getActivityRule() { 56 return new AutofillActivityTestRule<FragmentContainerActivity>( 57 FragmentContainerActivity.class) { 58 @Override 59 protected void afterActivityLaunched() { 60 initViews(getActivity()); 61 } 62 }; 63 } 64 65 private void initViews(FragmentContainerActivity activitiy) { 66 mActivity = activitiy; 67 mEditText1 = mActivity.findViewById(R.id.editText1); 68 mEditText2 = mActivity.findViewById(R.id.editText2); 69 mFragment = mActivity.getFragmentManager().findFragmentByTag(FRAGMENT_TAG); 70 mParent = ((ViewGroup) mEditText1.getParent()); 71 72 assertThat(mFragment).isNotNull(); 73 } 74 75 // firstRemove and secondRemove run in the UI Thread; firstCheck doesn't 76 private void removeViewsBaseTest(@NonNull Runnable firstRemove, @Nullable Runnable firstCheck, 77 @Nullable Runnable secondRemove, String... viewsToSave) throws Exception { 78 enableService(); 79 80 // Set expectations. 81 sReplier.addResponse(new CannedFillResponse.Builder() 82 .setSaveInfoFlags(SaveInfo.FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE) 83 .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, viewsToSave).build()); 84 85 // Trigger autofill on editText2 86 mActivity.syncRunOnUiThread(() -> mEditText2.requestFocus()); 87 sReplier.getNextFillRequest(); 88 mUiBot.assertNoDatasetsEver(); 89 90 // Now it's safe to focus on editText1 without triggering a new partition due to race 91 // conditions 92 mActivity.syncRunOnUiThread(() -> mEditText1.requestFocus()); 93 94 // remove first set of views 95 mActivity.syncRunOnUiThread(() -> { 96 mEditText1.setText("editText1-filled"); 97 mEditText2.setText("editText2-filled"); 98 firstRemove.run(); 99 }); 100 101 // Check state between remove operations 102 if (firstCheck != null) { 103 firstCheck.run(); 104 } 105 106 // remove second set of views 107 if (secondRemove != null) { 108 mActivity.syncRunOnUiThread(secondRemove); 109 } 110 111 // Save should be shows after all remove operations were executed 112 mUiBot.saveForAutofill(true, SAVE_DATA_TYPE_GENERIC); 113 114 SaveRequest saveRequest = sReplier.getNextSaveRequest(); 115 for (String view : viewsToSave) { 116 assertThat(findNodeByResourceId(saveRequest.structure, view) 117 .getAutofillValue().getTextValue().toString()).isEqualTo(view + "-filled"); 118 } 119 } 120 121 @Test 122 public void removeBothViewsToFinishSession() throws Exception { 123 final AtomicReference<Exception> ref = new AtomicReference<>(); 124 removeViewsBaseTest( 125 () -> ((ViewGroup) mEditText1.getParent()).removeView(mEditText1), 126 () -> assertSaveNotShowing(ref), 127 () -> ((ViewGroup) mEditText2.getParent()).removeView(mEditText2), 128 "editText1", "editText2"); 129 final Exception e = ref.get(); 130 if (e != null) { 131 throw e; 132 } 133 } 134 135 private void assertSaveNotShowing(AtomicReference<Exception> ref) { 136 try { 137 mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_GENERIC); 138 } catch (Exception e) { 139 ref.set(e); 140 } 141 } 142 143 @Test 144 public void removeOneViewToFinishSession() throws Exception { 145 removeViewsBaseTest( 146 () -> { 147 // Do not trigger new partition when switching to editText2 148 mEditText2.setFocusable(false); 149 150 mParent.removeView(mEditText1); 151 }, 152 null, 153 null, 154 "editText1"); 155 } 156 157 @Test 158 public void hideOneViewToFinishSession() throws Exception { 159 removeViewsBaseTest( 160 () -> { 161 // Do not trigger new partition when switching to editText2 162 mEditText2.setFocusable(false); 163 164 mEditText1.setVisibility(ViewGroup.INVISIBLE); 165 }, 166 null, 167 null, 168 "editText1"); 169 } 170 171 @Test 172 public void removeFragmentToFinishSession() throws Exception { 173 removeViewsBaseTest( 174 () -> mActivity.getFragmentManager().beginTransaction().remove( 175 mFragment).commitNow(), 176 null, 177 null, 178 "editText1", "editText2"); 179 } 180 181 @Test 182 public void removeParentToFinishSession() throws Exception { 183 removeViewsBaseTest( 184 () -> ((ViewGroup) mParent.getParent()).removeView(mParent), 185 null, 186 null, 187 "editText1", "editText2"); 188 } 189 190 @Test 191 public void hideParentToFinishSession() throws Exception { 192 removeViewsBaseTest( 193 () -> mParent.setVisibility(ViewGroup.INVISIBLE), 194 null, 195 null, 196 "editText1", "editText2"); 197 } 198 199 /** 200 * An activity that is currently getting autofilled might go into the background. While the 201 * tracked views are not visible on the screen anymore, this should not trigger a save. 202 * 203 * <p>The {@link Runnable}s are synchronously run in the UI thread. 204 */ 205 private void activityToBackgroundShouldNotTriggerSave(@Nullable Runnable removeInBackGround, 206 @Nullable Runnable removeInForeGroup) throws Exception { 207 enableService(); 208 209 // Set expectations. 210 sReplier.addResponse(new CannedFillResponse.Builder() 211 .setSaveInfoFlags(SaveInfo.FLAG_SAVE_ON_ALL_VIEWS_INVISIBLE) 212 .setRequiredSavableIds(SAVE_DATA_TYPE_GENERIC, "editText1").build()); 213 214 215 // Trigger autofill on editText2 216 mActivity.syncRunOnUiThread(() -> mEditText2.requestFocus()); 217 sReplier.getNextFillRequest(); 218 mUiBot.assertNoDatasetsEver(); 219 220 // Now it's safe to focus on editText1 without triggering a new partition due to race 221 // conditions 222 mActivity.syncRunOnUiThread(() -> mEditText1.requestFocus()); 223 224 mActivity.syncRunOnUiThread(() -> { 225 mEditText1.setText("editText1-filled"); 226 mEditText2.setText("editText2-filled"); 227 }); 228 229 // Start activity on top 230 mActivity.startActivity(new Intent(getContext(), 231 ManualAuthenticationActivity.class)); 232 mActivity.waitUntilStopped(); 233 234 if (removeInBackGround != null) { 235 mActivity.syncRunOnUiThread(removeInBackGround); 236 } 237 238 mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_GENERIC); 239 240 // Remove previously started activity from top 241 mUiBot.selectByRelativeId(ID_BUTTON); 242 mActivity.waitUntilResumed(); 243 244 if (removeInForeGroup != null) { 245 mUiBot.assertSaveNotShowing(SAVE_DATA_TYPE_GENERIC); 246 247 mActivity.syncRunOnUiThread(removeInForeGroup); 248 } 249 250 // Save should be shows after all remove operations were executed 251 mUiBot.saveForAutofill(true, SAVE_DATA_TYPE_GENERIC); 252 253 SaveRequest saveRequest = sReplier.getNextSaveRequest(); 254 assertThat(findNodeByResourceId(saveRequest.structure, "editText1") 255 .getAutofillValue().getTextValue().toString()).isEqualTo("editText1-filled"); 256 } 257 258 @Test 259 public void removeViewInBackground() throws Exception { 260 activityToBackgroundShouldNotTriggerSave( 261 () -> mActivity.syncRunOnUiThread(() -> { 262 // Do not trigger new partition when switching to editText2 263 mEditText2.setFocusable(false); 264 265 mParent.removeView(mEditText1); 266 }), 267 null); 268 } 269 270 @Test 271 public void hideViewInBackground() throws Exception { 272 activityToBackgroundShouldNotTriggerSave(() -> { 273 // Do not trigger new partition when switching to editText2 274 mEditText2.setFocusable(false); 275 276 mEditText1.setVisibility(ViewGroup.INVISIBLE); 277 }, 278 null); 279 } 280 281 @Test 282 public void hideParentInBackground() throws Exception { 283 activityToBackgroundShouldNotTriggerSave(() -> mParent.setVisibility(ViewGroup.INVISIBLE), 284 null); 285 } 286 287 @Test 288 public void removeParentInBackground() throws Exception { 289 activityToBackgroundShouldNotTriggerSave( 290 () -> ((ViewGroup) mParent.getParent()).removeView(mParent), 291 null); 292 } 293 294 @Test 295 public void removeViewAfterBackground() throws Exception { 296 activityToBackgroundShouldNotTriggerSave(() -> { 297 // Do not trigger new fill request when closing activity 298 mEditText1.setFocusable(false); 299 mEditText2.setFocusable(false); 300 }, 301 () -> mParent.removeView(mEditText1)); 302 } 303 } 304