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