• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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