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