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("mTestActivity failed to destroy in " + Utils.ACTIVITY_ONRESUME_TIMEOUT_MS + "msec"); 309 } 310 } 311 312 /** 313 * Send broadcast to MainInteractionService to start a session 314 */ startSession()315 protected AutoResetLatch startSession() { 316 return startSession(new Bundle()); 317 } 318 startSession(Bundle extras)319 protected AutoResetLatch startSession(Bundle extras) { 320 return startSession(mTestName, extras); 321 } 322 startSession(String testName, Bundle extras)323 protected AutoResetLatch startSession(String testName, Bundle extras) { 324 Intent intent = new Intent(Utils.BROADCAST_INTENT_START_ASSIST); 325 Log.i(TAG, "passed in class test name is: " + testName); 326 intent.putExtra(Utils.TESTCASE_TYPE, testName); 327 addDimensionsToIntent(intent); 328 intent.putExtras(extras); 329 intent.putExtra(Utils.EXTRA_REMOTE_CALLBACK, mRemoteCallback); 330 intent.setPackage("android.assist.service"); 331 332 mContext.sendBroadcast(intent); 333 return mAssistDataReceivedLatch; 334 } 335 336 /** 337 * Calculate display dimensions (including navbar) to pass along in the given intent. 338 */ addDimensionsToIntent(Intent intent)339 private void addDimensionsToIntent(Intent intent) { 340 if (mDisplaySize == null) { 341 Display.Mode dMode = mTestActivity.getWindowManager().getDefaultDisplay().getMode(); 342 mDisplaySize = new Point(dMode.getPhysicalWidth(), dMode.getPhysicalHeight()); 343 } 344 Rect bounds = mTestActivity.getWindowManager().getMaximumWindowMetrics().getBounds(); 345 intent.putExtra(Utils.DISPLAY_AREA_BOUNDS_KEY, bounds); 346 intent.putExtra(Utils.DISPLAY_WIDTH_KEY, mDisplaySize.x); 347 intent.putExtra(Utils.DISPLAY_HEIGHT_KEY, mDisplaySize.y); 348 } 349 waitForContext(AutoResetLatch sessionLatch)350 protected boolean waitForContext(AutoResetLatch sessionLatch) throws Exception { 351 if (!sessionLatch.await(Utils.getAssistDataTimeout(mTestName), TimeUnit.MILLISECONDS)) { 352 fail("Fail to receive broadcast in " + Utils.getAssistDataTimeout(mTestName) + "msec"); 353 } 354 Log.i(TAG, "Received broadcast with all information."); 355 return true; 356 } 357 358 /** 359 * Checks the nullness of the received 360 * {@link android.service.voice.VoiceInteractionSession.ActivityId}. 361 * 362 * @param isActivityIdNull True if activityId should be null. 363 */ verifyActivityIdNullness(boolean isActivityIdNull)364 protected void verifyActivityIdNullness(boolean isActivityIdNull) { 365 if (mIsActivityIdNull != isActivityIdNull) { 366 fail(String.format("Should %s have been null - ActivityId: %s", 367 isActivityIdNull ? "" : "not", mIsActivityIdNull)); 368 } 369 } 370 371 /** 372 * Checks that the nullness of values are what we expect. 373 * 374 * @param isBundleNull True if assistBundle should be null. 375 * @param isStructureNull True if assistStructure should be null. 376 * @param isContentNull True if assistContent should be null. 377 * @param isScreenshotNull True if screenshot should be null. 378 */ verifyAssistDataNullness(boolean isBundleNull, boolean isStructureNull, boolean isContentNull, boolean isScreenshotNull)379 protected void verifyAssistDataNullness(boolean isBundleNull, boolean isStructureNull, 380 boolean isContentNull, boolean isScreenshotNull) { 381 382 if ((mAssistContent == null) != isContentNull) { 383 fail(String.format("Should %s have been null - AssistContent: %s", 384 isContentNull ? "" : "not", mAssistContent)); 385 } 386 387 if ((mAssistStructure == null) != isStructureNull) { 388 fail(String.format("Should %s have been null - AssistStructure: %s", 389 isStructureNull ? "" : "not", mAssistStructure)); 390 } 391 392 if ((mAssistBundle == null) != isBundleNull) { 393 fail(String.format("Should %s have been null - AssistBundle: %s", 394 isBundleNull ? "" : "not", mAssistBundle)); 395 } 396 397 if (mScreenshot == isScreenshotNull) { 398 fail(String.format("Should %s have been null - Screenshot: %s", 399 isScreenshotNull ? "":"not", mScreenshot)); 400 } 401 } 402 403 /** 404 * Sends a broadcast with the specified scroll positions to the test app. 405 */ scrollTestApp(int scrollX, int scrollY, boolean scrollTextView, boolean scrollScrollView)406 protected void scrollTestApp(int scrollX, int scrollY, boolean scrollTextView, 407 boolean scrollScrollView) { 408 mTestActivity.scrollText(scrollX, scrollY, scrollTextView, scrollScrollView); 409 Intent intent = null; 410 if (scrollTextView) { 411 intent = new Intent(Utils.SCROLL_TEXTVIEW_ACTION); 412 } else if (scrollScrollView) { 413 intent = new Intent(Utils.SCROLL_SCROLLVIEW_ACTION); 414 } 415 intent.putExtra(Utils.SCROLL_X_POSITION, scrollX); 416 intent.putExtra(Utils.SCROLL_Y_POSITION, scrollY); 417 mContext.sendBroadcast(intent); 418 } 419 420 /** 421 * Verifies the view hierarchy of the backgroundApp matches the assist structure. 422 * @param backgroundApp ComponentName of app the assistant is invoked upon 423 * @param isSecureWindow Denotes whether the activity has FLAG_SECURE set 424 */ verifyAssistStructure(ComponentName backgroundApp, boolean isSecureWindow)425 protected void verifyAssistStructure(ComponentName backgroundApp, boolean isSecureWindow) { 426 // Check component name matches 427 assertThat(mAssistStructure.getActivityComponent().flattenToString()) 428 .isEqualTo(backgroundApp.flattenToString()); 429 long acquisitionStart = mAssistStructure.getAcquisitionStartTime(); 430 long acquisitionEnd = mAssistStructure.getAcquisitionEndTime(); 431 assertThat(acquisitionStart).isGreaterThan(0L); 432 assertThat(acquisitionEnd).isGreaterThan(0L); 433 assertThat(acquisitionEnd).isAtLeast(acquisitionStart); 434 Log.i(TAG, "Traversing down structure for: " + backgroundApp.flattenToString()); 435 mView = mTestActivity.findViewById(android.R.id.content).getRootView(); 436 verifyHierarchy(mAssistStructure, isSecureWindow); 437 } 438 logContextAndScreenshotSetting()439 protected static void logContextAndScreenshotSetting() { 440 Log.i(TAG, "Context is: " + sStructureEnabledMgr.get()); 441 Log.i(TAG, "Screenshot is: " + sScreenshotEnabledMgr.get()); 442 } 443 444 /** 445 * Recursively traverse and compare properties in the View hierarchy with the Assist Structure. 446 */ verifyHierarchy(AssistStructure structure, boolean isSecureWindow)447 public void verifyHierarchy(AssistStructure structure, boolean isSecureWindow) { 448 Log.i(TAG, "verifyHierarchy"); 449 450 int numWindows = structure.getWindowNodeCount(); 451 // TODO: multiple windows? 452 assertWithMessage("Number of windows don't match").that(numWindows).isEqualTo(1); 453 int[] appLocationOnScreen = new int[2]; 454 mView.getLocationOnScreen(appLocationOnScreen); 455 456 for (int i = 0; i < numWindows; i++) { 457 AssistStructure.WindowNode windowNode = structure.getWindowNodeAt(i); 458 Log.i(TAG, "Title: " + windowNode.getTitle()); 459 // Verify top level window bounds are as big as the app and pinned to its top-left 460 // corner. 461 assertWithMessage("Window left position wrong: was %s", windowNode.getLeft()) 462 .that(appLocationOnScreen[0]).isEqualTo(windowNode.getLeft()); 463 assertWithMessage("Window top position wrong: was %s", windowNode.getTop()) 464 .that(appLocationOnScreen[1]).isEqualTo(windowNode.getTop()); 465 traverseViewAndStructure( 466 mView, 467 windowNode.getRootViewNode(), 468 isSecureWindow); 469 } 470 } 471 traverseViewAndStructure(View parentView, ViewNode parentNode, boolean isSecureWindow)472 private void traverseViewAndStructure(View parentView, ViewNode parentNode, 473 boolean isSecureWindow) { 474 ViewGroup parentGroup; 475 476 if (parentView == null && parentNode == null) { 477 Log.i(TAG, "Views are null, done traversing this branch."); 478 return; 479 } else if (parentNode == null || parentView == null) { 480 fail(String.format("Views don't match. View: %s, Node: %s", parentView, parentNode)); 481 } 482 483 // Debugging 484 Log.i(TAG, "parentView is of type: " + parentView.getClass().getName()); 485 if (parentView instanceof ViewGroup) { 486 for (int childInt = 0; childInt < ((ViewGroup) parentView).getChildCount(); 487 childInt++) { 488 Log.i(TAG, 489 "viewchild" + childInt + " is of type: " 490 + ((ViewGroup) parentView).getChildAt(childInt).getClass().getName()); 491 } 492 } 493 String parentViewId = null; 494 if (parentView.getId() > 0) { 495 parentViewId = mTestActivity.getResources().getResourceEntryName(parentView.getId()); 496 Log.i(TAG, "View ID: " + parentViewId); 497 } 498 499 Log.i(TAG, "parentNode is of type: " + parentNode.getClassName()); 500 for (int nodeInt = 0; nodeInt < parentNode.getChildCount(); nodeInt++) { 501 Log.i(TAG, 502 "nodechild" + nodeInt + " is of type: " 503 + parentNode.getChildAt(nodeInt).getClassName()); 504 } 505 Log.i(TAG, "Node ID: " + parentNode.getIdEntry()); 506 507 assertWithMessage("IDs do not match").that(parentNode.getIdEntry()).isEqualTo(parentViewId); 508 509 int numViewChildren = 0; 510 int numNodeChildren = 0; 511 if (parentView instanceof ViewGroup) { 512 numViewChildren = ((ViewGroup) parentView).getChildCount(); 513 } 514 numNodeChildren = parentNode.getChildCount(); 515 516 if (isSecureWindow) { 517 assertWithMessage("ViewNode property isAssistBlocked is false") 518 .that(parentNode.isAssistBlocked()).isTrue(); 519 assertWithMessage("Secure window should only traverse root node") 520 .that(numNodeChildren).isEqualTo(0); 521 isSecureWindow = false; 522 } else if (parentNode.getClassName().equals("android.webkit.WebView")) { 523 // WebView will also appear to have no children while the node does, traverse node 524 assertWithMessage("AssistStructure returned a WebView where the view wasn't one").that( 525 parentView instanceof WebView).isTrue(); 526 527 boolean textInWebView = false; 528 529 for (int i = numNodeChildren - 1; i >= 0; i--) { 530 textInWebView |= traverseWebViewForText(parentNode.getChildAt(i)); 531 } 532 assertWithMessage("Did not find expected strings inside WebView").that(textInWebView) 533 .isTrue(); 534 } else { 535 assertWithMessage("Number of children did not match").that(numNodeChildren) 536 .isEqualTo(numViewChildren); 537 538 verifyViewProperties(parentView, parentNode); 539 540 if (parentView instanceof ViewGroup) { 541 parentGroup = (ViewGroup) parentView; 542 543 // TODO: set a max recursion level 544 for (int i = numNodeChildren - 1; i >= 0; i--) { 545 View childView = parentGroup.getChildAt(i); 546 ViewNode childNode = parentNode.getChildAt(i); 547 548 // if isSecureWindow, should not have reached this point. 549 assertThat(isSecureWindow).isFalse(); 550 traverseViewAndStructure(childView, childNode, isSecureWindow); 551 } 552 } 553 } 554 } 555 556 /** 557 * Return true if the expected strings are found in the WebView, else fail. 558 */ traverseWebViewForText(ViewNode parentNode)559 private boolean traverseWebViewForText(ViewNode parentNode) { 560 boolean textFound = false; 561 if (parentNode.getText() != null 562 && parentNode.getText().toString().equals(Utils.WEBVIEW_HTML_GREETING)) { 563 return true; 564 } 565 for (int i = parentNode.getChildCount() - 1; i >= 0; i--) { 566 textFound |= traverseWebViewForText(parentNode.getChildAt(i)); 567 } 568 return textFound; 569 } 570 571 /** 572 * Return true if the expected domain is found in the WebView, else fail. 573 */ verifyAssistStructureHasWebDomain(String domain)574 protected void verifyAssistStructureHasWebDomain(String domain) { 575 assertThat(traverse(mAssistStructure.getWindowNodeAt(0).getRootViewNode(), (n) -> { 576 return n.getWebDomain() != null && domain.equals(n.getWebDomain()); 577 })).isTrue(); 578 } 579 580 /** 581 * Return true if the expected LocaleList is found in the WebView, else fail. 582 */ verifyAssistStructureHasLocaleList(LocaleList localeList)583 protected void verifyAssistStructureHasLocaleList(LocaleList localeList) { 584 assertThat(traverse(mAssistStructure.getWindowNodeAt(0).getRootViewNode(), (n) -> { 585 return n.getLocaleList() != null && localeList.equals(n.getLocaleList()); 586 })).isTrue(); 587 } 588 589 interface ViewNodeVisitor { visit(ViewNode node)590 boolean visit(ViewNode node); 591 } 592 traverse(ViewNode parentNode, ViewNodeVisitor visitor)593 private boolean traverse(ViewNode parentNode, ViewNodeVisitor visitor) { 594 if (visitor.visit(parentNode)) { 595 return true; 596 } 597 for (int i = parentNode.getChildCount() - 1; i >= 0; i--) { 598 if (traverse(parentNode.getChildAt(i), visitor)) { 599 return true; 600 } 601 } 602 return false; 603 } 604 setFeaturesEnabled(StructureEnabled structure, ScreenshotEnabled screenshot)605 protected static void setFeaturesEnabled(StructureEnabled structure, 606 ScreenshotEnabled screenshot) { 607 Log.i(TAG, "setFeaturesEnabled(" + structure + ", " + screenshot + ")"); 608 sStructureEnabledMgr.set(structure.value); 609 sScreenshotEnabledMgr.set(screenshot.value); 610 } 611 612 /** 613 * Compare view properties of the view hierarchy with that reported in the assist structure. 614 */ verifyViewProperties(View parentView, ViewNode parentNode)615 private void verifyViewProperties(View parentView, ViewNode parentNode) { 616 assertWithMessage("Left positions do not match").that(parentNode.getLeft()) 617 .isEqualTo(parentView.getLeft()); 618 assertWithMessage("Top positions do not match").that(parentNode.getTop()) 619 .isEqualTo(parentView.getTop()); 620 assertWithMessage("Opaque flags do not match").that(parentNode.isOpaque()) 621 .isEqualTo(parentView.isOpaque()); 622 623 int viewId = parentView.getId(); 624 625 if (viewId > 0) { 626 if (parentNode.getIdEntry() != null) { 627 assertWithMessage("View IDs do not match.").that(parentNode.getIdEntry()) 628 .isEqualTo(mTestActivity.getResources().getResourceEntryName(viewId)); 629 } 630 } else { 631 assertWithMessage("View Node should not have an ID").that(parentNode.getIdEntry()) 632 .isNull(); 633 } 634 635 Log.i(TAG, "parent text: " + parentNode.getText()); 636 if (parentView instanceof TextView) { 637 Log.i(TAG, "view text: " + ((TextView) parentView).getText()); 638 } 639 640 assertWithMessage("Scroll X does not match").that(parentNode.getScrollX()) 641 .isEqualTo(parentView.getScrollX()); 642 assertWithMessage("Scroll Y does not match").that(parentNode.getScrollY()) 643 .isEqualTo(parentView.getScrollY()); 644 assertWithMessage("Heights do not match").that(parentNode.getHeight()) 645 .isEqualTo(parentView.getHeight()); 646 assertWithMessage("Widths do not match").that(parentNode.getWidth()) 647 .isEqualTo(parentView.getWidth()); 648 649 if (parentView instanceof TextView) { 650 if (parentView instanceof EditText) { 651 assertWithMessage("Text selection start does not match") 652 .that(parentNode.getTextSelectionStart()) 653 .isEqualTo(((EditText) parentView).getSelectionStart()); 654 assertWithMessage("Text selection end does not match") 655 .that(parentNode.getTextSelectionEnd()) 656 .isEqualTo(((EditText) parentView).getSelectionEnd()); 657 } 658 TextView textView = (TextView) parentView; 659 assertThat(parentNode.getTextSize()).isWithin(0.01F).of(textView.getTextSize()); 660 String viewString = textView.getText().toString(); 661 String nodeString = parentNode.getText().toString(); 662 663 if (parentNode.getScrollX() == 0 && parentNode.getScrollY() == 0) { 664 Log.i(TAG, "Verifying text within TextView at the beginning"); 665 Log.i(TAG, "view string: " + viewString); 666 Log.i(TAG, "node string: " + nodeString); 667 assertWithMessage("String length is unexpected: original string - %s, " 668 + "string in AssistData - %s", viewString.length(), nodeString.length()) 669 .that(viewString.length()).isAtLeast(nodeString.length()); 670 assertWithMessage("Expected a longer string to be shown").that( 671 nodeString.length()).isAtLeast(Math.min(viewString.length(), 30)); 672 for (int x = 0; x < parentNode.getText().length(); x++) { 673 assertWithMessage("Char not equal at index: %s", x).that( 674 parentNode.getText().charAt(x)).isEqualTo( 675 ((TextView) parentView).getText().toString().charAt(x)); 676 } 677 } else if (parentNode.getScrollX() == parentView.getWidth()) { 678 679 } 680 } else { 681 assertThat(parentNode.getText()).isNull(); 682 } 683 } 684 setAssistResults(Bundle assistData)685 protected void setAssistResults(Bundle assistData) { 686 mIsActivityIdNull = assistData.getBoolean(Utils.ASSIST_IS_ACTIVITY_ID_NULL);; 687 mAssistBundle = assistData.getBundle(Utils.ASSIST_BUNDLE_KEY); 688 mAssistStructure = assistData.getParcelable(Utils.ASSIST_STRUCTURE_KEY); 689 mAssistContent = assistData.getParcelable(Utils.ASSIST_CONTENT_KEY); 690 691 mScreenshot = assistData.getBoolean(Utils.ASSIST_SCREENSHOT_KEY, false); 692 693 mScreenshotMatches = assistData.getBoolean(Utils.COMPARE_SCREENSHOT_KEY, false); 694 mOnShowArgs = assistData.getBundle(Utils.ON_SHOW_ARGS_KEY); 695 } 696 eventuallyWithSessionClose(@onNull ThrowingRunnable runnable)697 protected void eventuallyWithSessionClose(@NonNull ThrowingRunnable runnable) throws Throwable { 698 AtomicReference<Throwable> innerThrowable = new AtomicReference<>(); 699 try { 700 TIMEOUT.run(getClass().getName(), SLEEP_BEFORE_RETRY_MS, () -> { 701 try { 702 runnable.run(); 703 return runnable; 704 } catch (Throwable throwable) { 705 // Immediately close the session so the next run can redo its action 706 mContext.sendBroadcast(new Intent(Utils.HIDE_SESSION)); 707 mSessionCompletedLatch.await(2, TimeUnit.SECONDS); 708 innerThrowable.set(throwable); 709 return null; 710 } 711 }); 712 } catch (Throwable throwable) { 713 Throwable inner = innerThrowable.get(); 714 if (inner != null) { 715 throw inner; 716 } else { 717 throw throwable; 718 } 719 } 720 } 721 722 protected enum StructureEnabled { 723 TRUE("1"), FALSE("0"); 724 725 private final String value; 726 StructureEnabled(String value)727 private StructureEnabled(String value) { 728 this.value = value; 729 } 730 731 @Override toString()732 public String toString() { 733 return "structure_" + (value.equals("1") ? "enabled" : "disabled"); 734 } 735 736 } 737 738 protected enum ScreenshotEnabled { 739 TRUE("1"), FALSE("0"); 740 741 private final String value; 742 ScreenshotEnabled(String value)743 private ScreenshotEnabled(String value) { 744 this.value = value; 745 } 746 747 @Override toString()748 public String toString() { 749 return "screenshot_" + (value.equals("1") ? "enabled" : "disabled"); 750 } 751 } 752 753 public class ActionLatchReceiver { 754 755 private final Map<String, AutoResetLatch> entries = new HashMap<>(); 756 ActionLatchReceiver(Pair<String, AutoResetLatch>.... entries)757 protected ActionLatchReceiver(Pair<String, AutoResetLatch>... entries) { 758 for (Pair<String, AutoResetLatch> entry : entries) { 759 if (entry.second == null) { 760 throw new IllegalArgumentException("Test cannot pass in a null latch"); 761 } 762 this.entries.put(entry.first, entry.second); 763 } 764 765 this.entries.put(Utils.HIDE_SESSION_COMPLETE, mSessionCompletedLatch); 766 this.entries.put(Utils.APP_3P_HASRESUMED, mHas3pResumedLatch); 767 this.entries.put(Utils.TEST_ACTIVITY_DESTROY, mHasTestDestroyedLatch); 768 this.entries.put(Utils.ASSIST_RECEIVER_REGISTERED, mReadyLatch); 769 this.entries.put(Utils.BROADCAST_ASSIST_DATA_INTENT, mAssistDataReceivedLatch); 770 } 771 ActionLatchReceiver(String action, AutoResetLatch latch)772 protected ActionLatchReceiver(String action, AutoResetLatch latch) { 773 this(Pair.create(action, latch)); 774 } 775 onAction(Bundle bundle, String action)776 protected void onAction(Bundle bundle, String action) { 777 switch (action) { 778 case Utils.BROADCAST_ASSIST_DATA_INTENT: 779 AssistTestBase.this.setAssistResults(bundle); 780 // fall-through 781 default: 782 AutoResetLatch latch = entries.get(action); 783 if (latch == null) { 784 Log.e(TAG, this.getClass() + ": invalid action " + action); 785 } else { 786 latch.countDown(); 787 } 788 break; 789 } 790 } 791 } 792 } 793