Home | History | Annotate | Download | only in cts
      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