1 /* 2 * Copyright (C) 2015 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.assist.cts; 18 19 import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; 20 21 import static com.android.compatibility.common.util.ShellUtils.runShellCommand; 22 23 import static com.google.common.truth.Truth.assertThat; 24 import static com.google.common.truth.Truth.assertWithMessage; 25 26 import static org.junit.Assert.fail; 27 28 import android.app.ActivityManager; 29 import android.app.assist.AssistContent; 30 import android.app.assist.AssistStructure; 31 import android.app.assist.AssistStructure.ViewNode; 32 import android.assist.common.AutoResetLatch; 33 import android.assist.common.Utils; 34 import android.content.ComponentName; 35 import android.content.Context; 36 import android.content.Intent; 37 import android.graphics.Point; 38 import android.graphics.Rect; 39 import android.os.Bundle; 40 import android.os.Handler; 41 import android.os.LocaleList; 42 import android.os.RemoteCallback; 43 import android.provider.Settings; 44 import android.util.Log; 45 import android.util.Pair; 46 import android.view.Display; 47 import android.view.View; 48 import android.view.ViewGroup; 49 import android.webkit.WebView; 50 import android.widget.EditText; 51 import android.widget.TextView; 52 53 import androidx.annotation.NonNull; 54 import androidx.annotation.Nullable; 55 import androidx.test.ext.junit.runners.AndroidJUnit4; 56 import androidx.test.rule.ActivityTestRule; 57 58 import com.android.compatibility.common.util.SettingsStateChangerRule; 59 import com.android.compatibility.common.util.SettingsStateManager; 60 import com.android.compatibility.common.util.StateKeeperRule; 61 import com.android.compatibility.common.util.ThrowingRunnable; 62 import com.android.compatibility.common.util.Timeout; 63 64 import org.junit.After; 65 import org.junit.Before; 66 import org.junit.BeforeClass; 67 import org.junit.Rule; 68 import org.junit.rules.RuleChain; 69 import org.junit.runner.RunWith; 70 71 import java.util.HashMap; 72 import java.util.Map; 73 import java.util.concurrent.TimeUnit; 74 import java.util.concurrent.atomic.AtomicReference; 75 import java.util.function.Consumer; 76 77 @RunWith(AndroidJUnit4.class) 78 abstract class AssistTestBase { 79 private static final String TAG = "AssistTestBase"; 80 81 protected static final String FEATURE_VOICE_RECOGNIZERS = "android.software.voice_recognizers"; 82 83 // TODO: use constants from Settings (should be @TestApi) 84 private static final String ASSIST_STRUCTURE_ENABLED = "assist_structure_enabled"; 85 private static final String ASSIST_SCREENSHOT_ENABLED = "assist_screenshot_enabled"; 86 87 private static final Timeout TIMEOUT = new Timeout( 88 "AssistTestBaseTimeout", 89 10000, 90 2F, 91 10000 92 ); 93 94 private static final long SLEEP_BEFORE_RETRY_MS = 250L; 95 96 private static final Context sContext = getInstrumentation().getTargetContext(); 97 98 private static final SettingsStateManager sStructureEnabledMgr = new SettingsStateManager( 99 sContext, ASSIST_STRUCTURE_ENABLED); 100 private static final SettingsStateManager sScreenshotEnabledMgr = new SettingsStateManager( 101 sContext, ASSIST_SCREENSHOT_ENABLED); 102 103 private final SettingsStateChangerRule mServiceSetterRule = new SettingsStateChangerRule( 104 sContext, Settings.Secure.VOICE_INTERACTION_SERVICE, 105 "android.assist.service/.MainInteractionService"); 106 private final StateKeeperRule<String> mStructureEnabledKeeperRule = new StateKeeperRule<>( 107 sStructureEnabledMgr); 108 private final StateKeeperRule<String> mScreenshotEnabledKeeperRule = new StateKeeperRule<>( 109 sScreenshotEnabledMgr); 110 private final ActivityTestRule<TestStartActivity> mActivityTestRule = 111 new ActivityTestRule<>(TestStartActivity.class, false, false); 112 113 @Rule 114 public final RuleChain mLookAllTheseRules = RuleChain 115 .outerRule(mServiceSetterRule) 116 .around(mStructureEnabledKeeperRule) 117 .around(mScreenshotEnabledKeeperRule) 118 .around(mActivityTestRule); 119 120 protected ActivityManager mActivityManager; 121 private TestStartActivity mTestActivity; 122 protected boolean mIsActivityIdNull; 123 protected AssistContent mAssistContent; 124 protected AssistStructure mAssistStructure; 125 protected boolean mScreenshot; 126 protected Bundle mAssistBundle; 127 protected Bundle mOnShowArgs; 128 protected Context mContext; 129 private AutoResetLatch mReadyLatch = new AutoResetLatch(1); 130 private AutoResetLatch mHas3pResumedLatch = new AutoResetLatch(1); 131 private AutoResetLatch mHasTestDestroyedLatch = new AutoResetLatch(1); 132 private AutoResetLatch mSessionCompletedLatch = new AutoResetLatch(1); 133 protected AutoResetLatch mAssistDataReceivedLatch = new AutoResetLatch(); 134 135 protected ActionLatchReceiver mActionLatchReceiver; 136 137 private final RemoteCallback mRemoteCallback = new RemoteCallback((result) -> { 138 String action = result.getString(Utils.EXTRA_REMOTE_CALLBACK_ACTION); 139 mActionLatchReceiver.onAction(result, action); 140 }); 141 142 @Nullable 143 protected RemoteCallback m3pActivityCallback; 144 @Nullable 145 protected RemoteCallback mSecondary3pActivityCallback; 146 147 protected boolean mScreenshotMatches; 148 private Point mDisplaySize; 149 private String mTestName; 150 private View mView; 151 152 @BeforeClass setFeatures()153 public static void setFeatures() { 154 setFeaturesEnabled(StructureEnabled.TRUE, ScreenshotEnabled.TRUE); 155 logContextAndScreenshotSetting(); 156 } 157 158 @Before setUp()159 public final void setUp() throws Exception { 160 mContext = sContext; 161 162 // reset old values 163 mScreenshotMatches = false; 164 mScreenshot = false; 165 mAssistStructure = null; 166 mAssistContent = null; 167 mAssistBundle = null; 168 mIsActivityIdNull = false; 169 170 mActionLatchReceiver = new ActionLatchReceiver(); 171 172 prepareDevice(); 173 174 customSetup(); 175 } 176 177 /** 178 * Test-specific setup - doesn't need to call {@code super} neither use <code>@Before</code>. 179 */ customSetup()180 protected void customSetup() throws Exception { 181 } 182 183 @After tearDown()184 public final void tearDown() throws Exception { 185 customTearDown(); 186 mTestActivity.finish(); 187 mContext.sendBroadcast(new Intent(Utils.HIDE_SESSION)); 188 189 if (m3pActivityCallback != null) { 190 m3pActivityCallback.sendResult(Utils.bundleOfRemoteAction(Utils.ACTION_END_OF_TEST)); 191 } 192 193 if (mSecondary3pActivityCallback != null) { 194 mSecondary3pActivityCallback 195 .sendResult(Utils.bundleOfRemoteAction(Utils.ACTION_END_OF_TEST)); 196 } 197 198 mSessionCompletedLatch.await(3, TimeUnit.SECONDS); 199 } 200 201 /** 202 * Test-specific teardown - doesn't need to call {@code super} neither use <code>@After</code>. 203 */ customTearDown()204 protected void customTearDown() throws Exception { 205 } 206 prepareDevice()207 private void prepareDevice() throws Exception { 208 Log.d(TAG, "prepareDevice()"); 209 210 // Unlock screen. 211 runShellCommand("input keyevent KEYCODE_WAKEUP"); 212 213 // Dismiss keyguard, in case it's set as "Swipe to unlock". 214 runShellCommand("wm dismiss-keyguard"); 215 } 216 startTest(String testName)217 protected void startTest(String testName) throws Exception { 218 Log.i(TAG, "Starting test activity for TestCaseType = " + testName); 219 Intent intent = new Intent(); 220 intent.putExtra(Utils.TESTCASE_TYPE, testName); 221 intent.setAction("android.intent.action.START_TEST_" + testName); 222 intent.putExtra(Utils.EXTRA_REMOTE_CALLBACK, mRemoteCallback); 223 intent.addFlags(Intent.FLAG_ACTIVITY_MATCH_EXTERNAL); 224 225 mTestActivity.startActivity(intent); 226 waitForTestActivityOnDestroy(); 227 } 228 start3pApp(String testCaseName)229 protected void start3pApp(String testCaseName) throws Exception { 230 start3pApp(testCaseName, null); 231 } 232 start3pApp(String testCaseName, Bundle extras)233 protected void start3pApp(String testCaseName, Bundle extras) throws Exception { 234 Intent intent = new Intent(); 235 intent.putExtra(Utils.TESTCASE_TYPE, testCaseName); 236 Utils.setTestAppAction(intent, testCaseName); 237 intent.putExtra(Utils.EXTRA_REMOTE_CALLBACK, mRemoteCallback); 238 intent.addFlags(Intent.FLAG_ACTIVITY_MATCH_EXTERNAL); 239 240 // In devices which support multi-window Activity positioning by default (such as foldables) 241 // it is necessary to launch additional activities ("screen fillers") so we may validate the 242 // entire screenshot captured by the Assistant (full display, not individual DisplayAreas) 243 if (m3pActivityCallback == null) { // first time start3pApp is called 244 intent.putExtra(Utils.EXTRA_REMOTE_CALLBACK_RECEIVING, 245 createRemoteCallbackReceiver(callback -> m3pActivityCallback = callback)); 246 } else if (mSecondary3pActivityCallback == null) { // second time 247 // launch 3pApp on adjacent screen in test cases that need a "screen filler". 248 // necessary configuration to ensure Activity can be launched in another DisplayArea 249 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT 250 // as we are reusing this intent setup, unconditionally start a new task 251 | Intent.FLAG_ACTIVITY_MULTIPLE_TASK); 252 intent.putExtra(Utils.EXTRA_REMOTE_CALLBACK_RECEIVING, createRemoteCallbackReceiver( 253 remoteCallback -> mSecondary3pActivityCallback = remoteCallback)); 254 } else { 255 throw new IllegalStateException("start3pApp supports a maximum of two App instances."); 256 } 257 258 if (extras != null) { 259 intent.putExtras(extras); 260 } 261 262 mTestActivity.startActivity(intent); 263 waitForOnResume(); 264 } 265 createRemoteCallbackReceiver(Consumer<RemoteCallback> consumer)266 private RemoteCallback createRemoteCallbackReceiver(Consumer<RemoteCallback> consumer) { 267 return new RemoteCallback((results) -> { 268 String action = results.getString(Utils.EXTRA_REMOTE_CALLBACK_ACTION); 269 if (action.equals(Utils.EXTRA_REMOTE_CALLBACK_RECEIVING_ACTION)) { 270 consumer.accept(results.getParcelable(Utils.EXTRA_REMOTE_CALLBACK_RECEIVING)); 271 } 272 }, new Handler(mContext.getMainLooper())); 273 } 274 275 /** 276 * Starts the shim service activity 277 */ startTestActivity(String testName)278 protected void startTestActivity(String testName) { 279 Intent intent = new Intent(); 280 mTestName = testName; 281 intent.setAction("android.intent.action.TEST_START_ACTIVITY_" + testName); 282 intent.putExtra(Utils.TESTCASE_TYPE, testName); 283 intent.putExtra(Utils.EXTRA_REMOTE_CALLBACK, mRemoteCallback); 284 mTestActivity = mActivityTestRule.launchActivity(intent); 285 mActivityManager = (ActivityManager) mContext.getSystemService(Context.ACTIVITY_SERVICE); 286 } 287 288 /** 289 * Called when waiting for Assistant's Broadcast Receiver to be setup 290 */ waitForAssistantToBeReady()291 protected void waitForAssistantToBeReady() throws Exception { 292 Log.i(TAG, "waiting for assistant to be ready before continuing"); 293 if (!mReadyLatch.await(Utils.TIMEOUT_MS, TimeUnit.MILLISECONDS)) { 294 fail("Assistant was not ready before timeout of: " + Utils.TIMEOUT_MS + "msec"); 295 } 296 } 297 waitForOnResume()298 private void waitForOnResume() throws Exception { 299 Log.i(TAG, "waiting for onResume() before continuing"); 300 if (!mHas3pResumedLatch.await(Utils.ACTIVITY_ONRESUME_TIMEOUT_MS, TimeUnit.MILLISECONDS)) { 301 fail("Activity failed to resume in " + Utils.ACTIVITY_ONRESUME_TIMEOUT_MS + "msec"); 302 } 303 } 304 waitForTestActivityOnDestroy()305 private void waitForTestActivityOnDestroy() throws Exception { 306 Log.i(TAG, "waiting for mTestActivity onDestroy() before continuing"); 307 if (!mHasTestDestroyedLatch.await(Utils.ACTIVITY_ONRESUME_TIMEOUT_MS, TimeUnit.MILLISECONDS)) { 308 fail( 309 "mTestActivity failed to destroy in " 310 + Utils.ACTIVITY_ONRESUME_TIMEOUT_MS 311 + "msec"); 312 } 313 } 314 315 /** 316 * Send broadcast to MainInteractionService to start a session 317 */ startSession()318 protected AutoResetLatch startSession() { 319 return startSession(new Bundle()); 320 } 321 startSession(Bundle extras)322 protected AutoResetLatch startSession(Bundle extras) { 323 return startSession(mTestName, extras); 324 } 325 startSession(String testName, Bundle extras)326 protected AutoResetLatch startSession(String testName, Bundle extras) { 327 Intent intent = new Intent(Utils.BROADCAST_INTENT_START_ASSIST); 328 Log.i(TAG, "passed in class test name is: " + testName); 329 intent.putExtra(Utils.TESTCASE_TYPE, testName); 330 addDimensionsToIntent(intent); 331 intent.putExtras(extras); 332 intent.putExtra(Utils.EXTRA_REMOTE_CALLBACK, mRemoteCallback); 333 intent.setPackage("android.assist.service"); 334 335 mContext.sendBroadcast(intent); 336 return mAssistDataReceivedLatch; 337 } 338 339 /** 340 * Calculate display dimensions (including navbar) to pass along in the given intent. 341 */ addDimensionsToIntent(Intent intent)342 private void addDimensionsToIntent(Intent intent) { 343 if (mDisplaySize == null) { 344 Display.Mode dMode = mTestActivity.getWindowManager().getDefaultDisplay().getMode(); 345 mDisplaySize = new Point(dMode.getPhysicalWidth(), dMode.getPhysicalHeight()); 346 } 347 Rect bounds = mTestActivity.getWindowManager().getMaximumWindowMetrics().getBounds(); 348 if (Utils.isXr(mContext)) { 349 bounds = mTestActivity.getWindowManager().getCurrentWindowMetrics().getBounds(); 350 } 351 intent.putExtra(Utils.DISPLAY_AREA_BOUNDS_KEY, bounds); 352 intent.putExtra(Utils.DISPLAY_WIDTH_KEY, mDisplaySize.x); 353 intent.putExtra(Utils.DISPLAY_HEIGHT_KEY, mDisplaySize.y); 354 } 355 waitForContext(AutoResetLatch sessionLatch)356 protected boolean waitForContext(AutoResetLatch sessionLatch) throws Exception { 357 if (!sessionLatch.await(Utils.getAssistDataTimeout(mTestName), TimeUnit.MILLISECONDS)) { 358 fail("Fail to receive broadcast in " + Utils.getAssistDataTimeout(mTestName) + "msec"); 359 } 360 Log.i(TAG, "Received broadcast with all information."); 361 return true; 362 } 363 364 /** 365 * Checks the nullness of the received 366 * {@link android.service.voice.VoiceInteractionSession.ActivityId}. 367 * 368 * @param isActivityIdNull True if activityId should be null. 369 */ verifyActivityIdNullness(boolean isActivityIdNull)370 protected void verifyActivityIdNullness(boolean isActivityIdNull) { 371 if (mIsActivityIdNull != isActivityIdNull) { 372 fail(String.format("Should %s have been null - ActivityId: %s", 373 isActivityIdNull ? "" : "not", mIsActivityIdNull)); 374 } 375 } 376 377 /** 378 * Checks that the nullness of values are what we expect. 379 * 380 * @param isBundleNull True if assistBundle should be null. 381 * @param isStructureNull True if assistStructure should be null. 382 * @param isContentNull True if assistContent should be null. 383 * @param isScreenshotNull True if screenshot should be null. 384 */ verifyAssistDataNullness(boolean isBundleNull, boolean isStructureNull, boolean isContentNull, boolean isScreenshotNull)385 protected void verifyAssistDataNullness(boolean isBundleNull, boolean isStructureNull, 386 boolean isContentNull, boolean isScreenshotNull) { 387 388 if ((mAssistContent == null) != isContentNull) { 389 fail(String.format("Should %s have been null - AssistContent: %s", 390 isContentNull ? "" : "not", mAssistContent)); 391 } 392 393 if ((mAssistStructure == null) != isStructureNull) { 394 fail(String.format("Should %s have been null - AssistStructure: %s", 395 isStructureNull ? "" : "not", mAssistStructure)); 396 } 397 398 if ((mAssistBundle == null) != isBundleNull) { 399 fail(String.format("Should %s have been null - AssistBundle: %s", 400 isBundleNull ? "" : "not", mAssistBundle)); 401 } 402 403 if (mScreenshot == isScreenshotNull) { 404 fail(String.format("Should %s have been null - Screenshot: %s", 405 isScreenshotNull ? "":"not", mScreenshot)); 406 } 407 } 408 409 /** 410 * Sends a broadcast with the specified scroll positions to the test app. 411 */ scrollTestApp(int scrollX, int scrollY, boolean scrollTextView, boolean scrollScrollView)412 protected void scrollTestApp(int scrollX, int scrollY, boolean scrollTextView, 413 boolean scrollScrollView) { 414 mTestActivity.scrollText(scrollX, scrollY, scrollTextView, scrollScrollView); 415 Intent intent = null; 416 if (scrollTextView) { 417 intent = new Intent(Utils.SCROLL_TEXTVIEW_ACTION); 418 } else if (scrollScrollView) { 419 intent = new Intent(Utils.SCROLL_SCROLLVIEW_ACTION); 420 } 421 intent.putExtra(Utils.SCROLL_X_POSITION, scrollX); 422 intent.putExtra(Utils.SCROLL_Y_POSITION, scrollY); 423 mContext.sendBroadcast(intent); 424 } 425 426 /** 427 * Verifies the view hierarchy of the backgroundApp matches the assist structure. 428 * @param backgroundApp ComponentName of app the assistant is invoked upon 429 * @param isSecureWindow Denotes whether the activity has FLAG_SECURE set 430 */ verifyAssistStructure(ComponentName backgroundApp, boolean isSecureWindow)431 protected void verifyAssistStructure(ComponentName backgroundApp, boolean isSecureWindow) { 432 // Check component name matches 433 assertThat(mAssistStructure.getActivityComponent().flattenToString()) 434 .isEqualTo(backgroundApp.flattenToString()); 435 long acquisitionStart = mAssistStructure.getAcquisitionStartTime(); 436 long acquisitionEnd = mAssistStructure.getAcquisitionEndTime(); 437 assertThat(acquisitionStart).isGreaterThan(0L); 438 assertThat(acquisitionEnd).isGreaterThan(0L); 439 assertThat(acquisitionEnd).isAtLeast(acquisitionStart); 440 Log.i(TAG, "Traversing down structure for: " + backgroundApp.flattenToString()); 441 mView = mTestActivity.findViewById(android.R.id.content).getRootView(); 442 verifyHierarchy(mAssistStructure, isSecureWindow); 443 } 444 logContextAndScreenshotSetting()445 protected static void logContextAndScreenshotSetting() { 446 Log.i(TAG, "Context is: " + sStructureEnabledMgr.get()); 447 Log.i(TAG, "Screenshot is: " + sScreenshotEnabledMgr.get()); 448 } 449 450 /** 451 * Recursively traverse and compare properties in the View hierarchy with the Assist Structure. 452 */ verifyHierarchy(AssistStructure structure, boolean isSecureWindow)453 public void verifyHierarchy(AssistStructure structure, boolean isSecureWindow) { 454 Log.i(TAG, "verifyHierarchy"); 455 456 int numWindows = structure.getWindowNodeCount(); 457 // TODO: multiple windows? 458 assertWithMessage("Number of windows don't match").that(numWindows).isEqualTo(1); 459 int[] appLocationOnScreen = new int[2]; 460 mView.getLocationOnScreen(appLocationOnScreen); 461 462 for (int i = 0; i < numWindows; i++) { 463 AssistStructure.WindowNode windowNode = structure.getWindowNodeAt(i); 464 Log.i(TAG, "Title: " + windowNode.getTitle()); 465 // Verify top level window bounds are as big as the app and pinned to its top-left 466 // corner. 467 assertWithMessage("Window left position wrong: was %s", windowNode.getLeft()) 468 .that(appLocationOnScreen[0]).isEqualTo(windowNode.getLeft()); 469 assertWithMessage("Window top position wrong: was %s", windowNode.getTop()) 470 .that(appLocationOnScreen[1]).isEqualTo(windowNode.getTop()); 471 traverseViewAndStructure( 472 mView, 473 windowNode.getRootViewNode(), 474 isSecureWindow); 475 } 476 } 477 traverseViewAndStructure(View parentView, ViewNode parentNode, boolean isSecureWindow)478 private void traverseViewAndStructure(View parentView, ViewNode parentNode, 479 boolean isSecureWindow) { 480 ViewGroup parentGroup; 481 482 if (parentView == null && parentNode == null) { 483 Log.i(TAG, "Views are null, done traversing this branch."); 484 return; 485 } else if (parentNode == null || parentView == null) { 486 fail(String.format("Views don't match. View: %s, Node: %s", parentView, parentNode)); 487 } 488 489 // Debugging 490 Log.i(TAG, "parentView is of type: " + parentView.getClass().getName()); 491 if (parentView instanceof ViewGroup) { 492 for (int childInt = 0; childInt < ((ViewGroup) parentView).getChildCount(); 493 childInt++) { 494 Log.i(TAG, 495 "viewchild" + childInt + " is of type: " 496 + ((ViewGroup) parentView).getChildAt(childInt).getClass().getName()); 497 } 498 } 499 String parentViewId = null; 500 if (parentView.getId() > 0) { 501 parentViewId = mTestActivity.getResources().getResourceEntryName(parentView.getId()); 502 Log.i(TAG, "View ID: " + parentViewId); 503 } 504 505 Log.i(TAG, "parentNode is of type: " + parentNode.getClassName()); 506 for (int nodeInt = 0; nodeInt < parentNode.getChildCount(); nodeInt++) { 507 Log.i(TAG, 508 "nodechild" + nodeInt + " is of type: " 509 + parentNode.getChildAt(nodeInt).getClassName()); 510 } 511 Log.i(TAG, "Node ID: " + parentNode.getIdEntry()); 512 513 assertWithMessage("IDs do not match").that(parentNode.getIdEntry()).isEqualTo(parentViewId); 514 515 int numViewChildren = 0; 516 int numNodeChildren = 0; 517 if (parentView instanceof ViewGroup) { 518 numViewChildren = ((ViewGroup) parentView).getChildCount(); 519 } 520 numNodeChildren = parentNode.getChildCount(); 521 522 if (isSecureWindow) { 523 assertWithMessage("ViewNode property isAssistBlocked is false") 524 .that(parentNode.isAssistBlocked()).isTrue(); 525 assertWithMessage("Secure window should only traverse root node") 526 .that(numNodeChildren).isEqualTo(0); 527 isSecureWindow = false; 528 } else if (parentNode.getClassName().equals("android.webkit.WebView")) { 529 // WebView will also appear to have no children while the node does, traverse node 530 assertWithMessage("AssistStructure returned a WebView where the view wasn't one").that( 531 parentView instanceof WebView).isTrue(); 532 533 boolean textInWebView = false; 534 535 for (int i = numNodeChildren - 1; i >= 0; i--) { 536 textInWebView |= traverseWebViewForText(parentNode.getChildAt(i)); 537 } 538 assertWithMessage("Did not find expected strings inside WebView").that(textInWebView) 539 .isTrue(); 540 } else { 541 assertWithMessage("Number of children did not match").that(numNodeChildren) 542 .isEqualTo(numViewChildren); 543 544 verifyViewProperties(parentView, parentNode); 545 546 if (parentView instanceof ViewGroup) { 547 parentGroup = (ViewGroup) parentView; 548 549 // TODO: set a max recursion level 550 for (int i = numNodeChildren - 1; i >= 0; i--) { 551 View childView = parentGroup.getChildAt(i); 552 ViewNode childNode = parentNode.getChildAt(i); 553 554 // if isSecureWindow, should not have reached this point. 555 assertThat(isSecureWindow).isFalse(); 556 traverseViewAndStructure(childView, childNode, isSecureWindow); 557 } 558 } 559 } 560 } 561 562 /** 563 * Return true if the expected strings are found in the WebView, else fail. 564 */ traverseWebViewForText(ViewNode parentNode)565 private boolean traverseWebViewForText(ViewNode parentNode) { 566 boolean textFound = false; 567 if (parentNode.getText() != null 568 && parentNode.getText().toString().equals(Utils.WEBVIEW_HTML_GREETING)) { 569 return true; 570 } 571 for (int i = parentNode.getChildCount() - 1; i >= 0; i--) { 572 textFound |= traverseWebViewForText(parentNode.getChildAt(i)); 573 } 574 return textFound; 575 } 576 577 /** 578 * Return true if the expected domain is found in the WebView, else fail. 579 */ verifyAssistStructureHasWebDomain(String domain)580 protected void verifyAssistStructureHasWebDomain(String domain) { 581 assertThat(traverse(mAssistStructure.getWindowNodeAt(0).getRootViewNode(), (n) -> { 582 return n.getWebDomain() != null && domain.equals(n.getWebDomain()); 583 })).isTrue(); 584 } 585 586 /** 587 * Return true if the expected LocaleList is found in the WebView, else fail. 588 */ verifyAssistStructureHasLocaleList(LocaleList localeList)589 protected void verifyAssistStructureHasLocaleList(LocaleList localeList) { 590 assertThat(traverse(mAssistStructure.getWindowNodeAt(0).getRootViewNode(), (n) -> { 591 return n.getLocaleList() != null && localeList.equals(n.getLocaleList()); 592 })).isTrue(); 593 } 594 595 interface ViewNodeVisitor { visit(ViewNode node)596 boolean visit(ViewNode node); 597 } 598 traverse(ViewNode parentNode, ViewNodeVisitor visitor)599 private boolean traverse(ViewNode parentNode, ViewNodeVisitor visitor) { 600 if (visitor.visit(parentNode)) { 601 return true; 602 } 603 for (int i = parentNode.getChildCount() - 1; i >= 0; i--) { 604 if (traverse(parentNode.getChildAt(i), visitor)) { 605 return true; 606 } 607 } 608 return false; 609 } 610 setFeaturesEnabled(StructureEnabled structure, ScreenshotEnabled screenshot)611 protected static void setFeaturesEnabled(StructureEnabled structure, 612 ScreenshotEnabled screenshot) { 613 Log.i(TAG, "setFeaturesEnabled(" + structure + ", " + screenshot + ")"); 614 sStructureEnabledMgr.set(structure.value); 615 sScreenshotEnabledMgr.set(screenshot.value); 616 } 617 618 /** 619 * Compare view properties of the view hierarchy with that reported in the assist structure. 620 */ verifyViewProperties(View parentView, ViewNode parentNode)621 private void verifyViewProperties(View parentView, ViewNode parentNode) { 622 assertWithMessage("Left positions do not match").that(parentNode.getLeft()) 623 .isEqualTo(parentView.getLeft()); 624 assertWithMessage("Top positions do not match").that(parentNode.getTop()) 625 .isEqualTo(parentView.getTop()); 626 assertWithMessage("Opaque flags do not match").that(parentNode.isOpaque()) 627 .isEqualTo(parentView.isOpaque()); 628 629 int viewId = parentView.getId(); 630 631 if (viewId > 0) { 632 if (parentNode.getIdEntry() != null) { 633 assertWithMessage("View IDs do not match.").that(parentNode.getIdEntry()) 634 .isEqualTo(mTestActivity.getResources().getResourceEntryName(viewId)); 635 } 636 } else { 637 assertWithMessage("View Node should not have an ID").that(parentNode.getIdEntry()) 638 .isNull(); 639 } 640 641 Log.i(TAG, "parent text: " + parentNode.getText()); 642 if (parentView instanceof TextView) { 643 Log.i(TAG, "view text: " + ((TextView) parentView).getText()); 644 } 645 646 assertWithMessage("Scroll X does not match").that(parentNode.getScrollX()) 647 .isEqualTo(parentView.getScrollX()); 648 assertWithMessage("Scroll Y does not match").that(parentNode.getScrollY()) 649 .isEqualTo(parentView.getScrollY()); 650 assertWithMessage("Heights do not match").that(parentNode.getHeight()) 651 .isEqualTo(parentView.getHeight()); 652 assertWithMessage("Widths do not match").that(parentNode.getWidth()) 653 .isEqualTo(parentView.getWidth()); 654 655 if (parentView instanceof TextView) { 656 if (parentView instanceof EditText) { 657 assertWithMessage("Text selection start does not match") 658 .that(parentNode.getTextSelectionStart()) 659 .isEqualTo(((EditText) parentView).getSelectionStart()); 660 assertWithMessage("Text selection end does not match") 661 .that(parentNode.getTextSelectionEnd()) 662 .isEqualTo(((EditText) parentView).getSelectionEnd()); 663 } 664 TextView textView = (TextView) parentView; 665 assertThat(parentNode.getTextSize()).isWithin(0.01F).of(textView.getTextSize()); 666 String viewString = textView.getText().toString(); 667 String nodeString = parentNode.getText().toString(); 668 669 if (parentNode.getScrollX() == 0 && parentNode.getScrollY() == 0) { 670 Log.i(TAG, "Verifying text within TextView at the beginning"); 671 Log.i(TAG, "view string: " + viewString); 672 Log.i(TAG, "node string: " + nodeString); 673 assertWithMessage("String length is unexpected: original string - %s, " 674 + "string in AssistData - %s", viewString.length(), nodeString.length()) 675 .that(viewString.length()).isAtLeast(nodeString.length()); 676 assertWithMessage("Expected a longer string to be shown").that( 677 nodeString.length()).isAtLeast(Math.min(viewString.length(), 30)); 678 for (int x = 0; x < parentNode.getText().length(); x++) { 679 assertWithMessage("Char not equal at index: %s", x).that( 680 parentNode.getText().charAt(x)).isEqualTo( 681 ((TextView) parentView).getText().toString().charAt(x)); 682 } 683 } else if (parentNode.getScrollX() == parentView.getWidth()) { 684 685 } 686 } else { 687 assertThat(parentNode.getText()).isNull(); 688 } 689 } 690 setAssistResults(Bundle assistData)691 protected void setAssistResults(Bundle assistData) { 692 mIsActivityIdNull = assistData.getBoolean(Utils.ASSIST_IS_ACTIVITY_ID_NULL);; 693 mAssistBundle = assistData.getBundle(Utils.ASSIST_BUNDLE_KEY); 694 mAssistStructure = assistData.getParcelable(Utils.ASSIST_STRUCTURE_KEY); 695 mAssistContent = assistData.getParcelable(Utils.ASSIST_CONTENT_KEY); 696 697 mScreenshot = assistData.getBoolean(Utils.ASSIST_SCREENSHOT_KEY, false); 698 699 mScreenshotMatches = assistData.getBoolean(Utils.COMPARE_SCREENSHOT_KEY, false); 700 mOnShowArgs = assistData.getBundle(Utils.ON_SHOW_ARGS_KEY); 701 } 702 eventuallyWithSessionClose(@onNull ThrowingRunnable runnable)703 protected void eventuallyWithSessionClose(@NonNull ThrowingRunnable runnable) throws Throwable { 704 AtomicReference<Throwable> innerThrowable = new AtomicReference<>(); 705 try { 706 TIMEOUT.run(getClass().getName(), SLEEP_BEFORE_RETRY_MS, () -> { 707 try { 708 runnable.run(); 709 return runnable; 710 } catch (Throwable throwable) { 711 // Immediately close the session so the next run can redo its action 712 mContext.sendBroadcast(new Intent(Utils.HIDE_SESSION)); 713 mSessionCompletedLatch.await(2, TimeUnit.SECONDS); 714 innerThrowable.set(throwable); 715 return null; 716 } 717 }); 718 } catch (Throwable throwable) { 719 Throwable inner = innerThrowable.get(); 720 if (inner != null) { 721 throw inner; 722 } else { 723 throw throwable; 724 } 725 } 726 } 727 728 protected enum StructureEnabled { 729 TRUE("1"), FALSE("0"); 730 731 private final String value; 732 StructureEnabled(String value)733 private StructureEnabled(String value) { 734 this.value = value; 735 } 736 737 @Override toString()738 public String toString() { 739 return "structure_" + (value.equals("1") ? "enabled" : "disabled"); 740 } 741 742 } 743 744 protected enum ScreenshotEnabled { 745 TRUE("1"), FALSE("0"); 746 747 private final String value; 748 ScreenshotEnabled(String value)749 private ScreenshotEnabled(String value) { 750 this.value = value; 751 } 752 753 @Override toString()754 public String toString() { 755 return "screenshot_" + (value.equals("1") ? "enabled" : "disabled"); 756 } 757 } 758 759 public class ActionLatchReceiver { 760 761 private final Map<String, AutoResetLatch> entries = new HashMap<>(); 762 ActionLatchReceiver(Pair<String, AutoResetLatch>.... entries)763 protected ActionLatchReceiver(Pair<String, AutoResetLatch>... entries) { 764 for (Pair<String, AutoResetLatch> entry : entries) { 765 if (entry.second == null) { 766 throw new IllegalArgumentException("Test cannot pass in a null latch"); 767 } 768 this.entries.put(entry.first, entry.second); 769 } 770 771 this.entries.put(Utils.HIDE_SESSION_COMPLETE, mSessionCompletedLatch); 772 this.entries.put(Utils.APP_3P_HASRESUMED, mHas3pResumedLatch); 773 this.entries.put(Utils.TEST_ACTIVITY_DESTROY, mHasTestDestroyedLatch); 774 this.entries.put(Utils.ASSIST_RECEIVER_REGISTERED, mReadyLatch); 775 this.entries.put(Utils.BROADCAST_ASSIST_DATA_INTENT, mAssistDataReceivedLatch); 776 } 777 ActionLatchReceiver(String action, AutoResetLatch latch)778 protected ActionLatchReceiver(String action, AutoResetLatch latch) { 779 this(Pair.create(action, latch)); 780 } 781 onAction(Bundle bundle, String action)782 protected void onAction(Bundle bundle, String action) { 783 switch (action) { 784 case Utils.BROADCAST_ASSIST_DATA_INTENT: 785 AssistTestBase.this.setAssistResults(bundle); 786 // fall-through 787 default: 788 AutoResetLatch latch = entries.get(action); 789 if (latch == null) { 790 Log.e(TAG, this.getClass() + ": invalid action " + action); 791 } else { 792 latch.countDown(); 793 } 794 break; 795 } 796 } 797 } 798 } 799