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