• 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 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(
309                     "mTestActivity failed to destroy in "
310                             + Utils.ACTIVITY_ONRESUME_TIMEOUT_MS
311                             + "msec");
312         }
313     }
314 
315     /**
316      * Send broadcast to MainInteractionService to start a session
317      */
startSession()318     protected AutoResetLatch startSession() {
319         return startSession(new Bundle());
320     }
321 
startSession(Bundle extras)322     protected AutoResetLatch startSession(Bundle extras) {
323         return startSession(mTestName, extras);
324     }
325 
startSession(String testName, Bundle extras)326     protected AutoResetLatch startSession(String testName, Bundle extras) {
327         Intent intent = new Intent(Utils.BROADCAST_INTENT_START_ASSIST);
328         Log.i(TAG, "passed in class test name is: " + testName);
329         intent.putExtra(Utils.TESTCASE_TYPE, testName);
330         addDimensionsToIntent(intent);
331         intent.putExtras(extras);
332         intent.putExtra(Utils.EXTRA_REMOTE_CALLBACK, mRemoteCallback);
333         intent.setPackage("android.assist.service");
334 
335         mContext.sendBroadcast(intent);
336         return mAssistDataReceivedLatch;
337     }
338 
339     /**
340      * Calculate display dimensions (including navbar) to pass along in the given intent.
341      */
addDimensionsToIntent(Intent intent)342     private void addDimensionsToIntent(Intent intent) {
343         if (mDisplaySize == null) {
344             Display.Mode dMode = mTestActivity.getWindowManager().getDefaultDisplay().getMode();
345             mDisplaySize = new Point(dMode.getPhysicalWidth(), dMode.getPhysicalHeight());
346         }
347         Rect bounds = mTestActivity.getWindowManager().getMaximumWindowMetrics().getBounds();
348         if (Utils.isXr(mContext)) {
349             bounds = mTestActivity.getWindowManager().getCurrentWindowMetrics().getBounds();
350         }
351         intent.putExtra(Utils.DISPLAY_AREA_BOUNDS_KEY, bounds);
352         intent.putExtra(Utils.DISPLAY_WIDTH_KEY, mDisplaySize.x);
353         intent.putExtra(Utils.DISPLAY_HEIGHT_KEY, mDisplaySize.y);
354     }
355 
waitForContext(AutoResetLatch sessionLatch)356     protected boolean waitForContext(AutoResetLatch sessionLatch) throws Exception {
357         if (!sessionLatch.await(Utils.getAssistDataTimeout(mTestName), TimeUnit.MILLISECONDS)) {
358             fail("Fail to receive broadcast in " + Utils.getAssistDataTimeout(mTestName) + "msec");
359         }
360         Log.i(TAG, "Received broadcast with all information.");
361         return true;
362     }
363 
364     /**
365      * Checks the nullness of the received
366      * {@link android.service.voice.VoiceInteractionSession.ActivityId}.
367      *
368      * @param isActivityIdNull True if activityId should be null.
369      */
verifyActivityIdNullness(boolean isActivityIdNull)370     protected void verifyActivityIdNullness(boolean isActivityIdNull) {
371         if (mIsActivityIdNull != isActivityIdNull) {
372             fail(String.format("Should %s have been null - ActivityId: %s",
373                     isActivityIdNull ? "" : "not", mIsActivityIdNull));
374         }
375     }
376 
377     /**
378      * Checks that the nullness of values are what we expect.
379      *
380      * @param isBundleNull True if assistBundle should be null.
381      * @param isStructureNull True if assistStructure should be null.
382      * @param isContentNull True if assistContent should be null.
383      * @param isScreenshotNull True if screenshot should be null.
384      */
verifyAssistDataNullness(boolean isBundleNull, boolean isStructureNull, boolean isContentNull, boolean isScreenshotNull)385     protected void verifyAssistDataNullness(boolean isBundleNull, boolean isStructureNull,
386             boolean isContentNull, boolean isScreenshotNull) {
387 
388         if ((mAssistContent == null) != isContentNull) {
389             fail(String.format("Should %s have been null - AssistContent: %s",
390                     isContentNull ? "" : "not", mAssistContent));
391         }
392 
393         if ((mAssistStructure == null) != isStructureNull) {
394             fail(String.format("Should %s have been null - AssistStructure: %s",
395                     isStructureNull ? "" : "not", mAssistStructure));
396         }
397 
398         if ((mAssistBundle == null) != isBundleNull) {
399             fail(String.format("Should %s have been null - AssistBundle: %s",
400                     isBundleNull ? "" : "not", mAssistBundle));
401         }
402 
403         if (mScreenshot == isScreenshotNull) {
404             fail(String.format("Should %s have been null - Screenshot: %s",
405                     isScreenshotNull ? "":"not", mScreenshot));
406         }
407     }
408 
409     /**
410      * Sends a broadcast with the specified scroll positions to the test app.
411      */
scrollTestApp(int scrollX, int scrollY, boolean scrollTextView, boolean scrollScrollView)412     protected void scrollTestApp(int scrollX, int scrollY, boolean scrollTextView,
413             boolean scrollScrollView) {
414         mTestActivity.scrollText(scrollX, scrollY, scrollTextView, scrollScrollView);
415         Intent intent = null;
416         if (scrollTextView) {
417             intent = new Intent(Utils.SCROLL_TEXTVIEW_ACTION);
418         } else if (scrollScrollView) {
419             intent = new Intent(Utils.SCROLL_SCROLLVIEW_ACTION);
420         }
421         intent.putExtra(Utils.SCROLL_X_POSITION, scrollX);
422         intent.putExtra(Utils.SCROLL_Y_POSITION, scrollY);
423         mContext.sendBroadcast(intent);
424     }
425 
426     /**
427      * Verifies the view hierarchy of the backgroundApp matches the assist structure.
428      * @param backgroundApp ComponentName of app the assistant is invoked upon
429      * @param isSecureWindow Denotes whether the activity has FLAG_SECURE set
430      */
verifyAssistStructure(ComponentName backgroundApp, boolean isSecureWindow)431     protected void verifyAssistStructure(ComponentName backgroundApp, boolean isSecureWindow) {
432         // Check component name matches
433         assertThat(mAssistStructure.getActivityComponent().flattenToString())
434                 .isEqualTo(backgroundApp.flattenToString());
435         long acquisitionStart = mAssistStructure.getAcquisitionStartTime();
436         long acquisitionEnd = mAssistStructure.getAcquisitionEndTime();
437         assertThat(acquisitionStart).isGreaterThan(0L);
438         assertThat(acquisitionEnd).isGreaterThan(0L);
439         assertThat(acquisitionEnd).isAtLeast(acquisitionStart);
440         Log.i(TAG, "Traversing down structure for: " + backgroundApp.flattenToString());
441         mView = mTestActivity.findViewById(android.R.id.content).getRootView();
442         verifyHierarchy(mAssistStructure, isSecureWindow);
443     }
444 
logContextAndScreenshotSetting()445     protected static void logContextAndScreenshotSetting() {
446         Log.i(TAG, "Context is: " + sStructureEnabledMgr.get());
447         Log.i(TAG, "Screenshot is: " + sScreenshotEnabledMgr.get());
448     }
449 
450     /**
451      * Recursively traverse and compare properties in the View hierarchy with the Assist Structure.
452      */
verifyHierarchy(AssistStructure structure, boolean isSecureWindow)453     public void verifyHierarchy(AssistStructure structure, boolean isSecureWindow) {
454         Log.i(TAG, "verifyHierarchy");
455 
456         int numWindows = structure.getWindowNodeCount();
457         // TODO: multiple windows?
458         assertWithMessage("Number of windows don't match").that(numWindows).isEqualTo(1);
459         int[] appLocationOnScreen = new int[2];
460         mView.getLocationOnScreen(appLocationOnScreen);
461 
462         for (int i = 0; i < numWindows; i++) {
463             AssistStructure.WindowNode windowNode = structure.getWindowNodeAt(i);
464             Log.i(TAG, "Title: " + windowNode.getTitle());
465             // Verify top level window bounds are as big as the app and pinned to its top-left
466             // corner.
467             assertWithMessage("Window left position wrong: was %s", windowNode.getLeft())
468                     .that(appLocationOnScreen[0]).isEqualTo(windowNode.getLeft());
469             assertWithMessage("Window top position wrong: was %s", windowNode.getTop())
470                     .that(appLocationOnScreen[1]).isEqualTo(windowNode.getTop());
471             traverseViewAndStructure(
472                     mView,
473                     windowNode.getRootViewNode(),
474                     isSecureWindow);
475         }
476     }
477 
traverseViewAndStructure(View parentView, ViewNode parentNode, boolean isSecureWindow)478     private void traverseViewAndStructure(View parentView, ViewNode parentNode,
479             boolean isSecureWindow) {
480         ViewGroup parentGroup;
481 
482         if (parentView == null && parentNode == null) {
483             Log.i(TAG, "Views are null, done traversing this branch.");
484             return;
485         } else if (parentNode == null || parentView == null) {
486             fail(String.format("Views don't match. View: %s, Node: %s", parentView, parentNode));
487         }
488 
489         // Debugging
490         Log.i(TAG, "parentView is of type: " + parentView.getClass().getName());
491         if (parentView instanceof ViewGroup) {
492             for (int childInt = 0; childInt < ((ViewGroup) parentView).getChildCount();
493                     childInt++) {
494                 Log.i(TAG,
495                         "viewchild" + childInt + " is of type: "
496                         + ((ViewGroup) parentView).getChildAt(childInt).getClass().getName());
497             }
498         }
499         String parentViewId = null;
500         if (parentView.getId() > 0) {
501             parentViewId = mTestActivity.getResources().getResourceEntryName(parentView.getId());
502             Log.i(TAG, "View ID: " + parentViewId);
503         }
504 
505         Log.i(TAG, "parentNode is of type: " + parentNode.getClassName());
506         for (int nodeInt = 0; nodeInt < parentNode.getChildCount(); nodeInt++) {
507             Log.i(TAG,
508                     "nodechild" + nodeInt + " is of type: "
509                     + parentNode.getChildAt(nodeInt).getClassName());
510         }
511         Log.i(TAG, "Node ID: " + parentNode.getIdEntry());
512 
513         assertWithMessage("IDs do not match").that(parentNode.getIdEntry()).isEqualTo(parentViewId);
514 
515         int numViewChildren = 0;
516         int numNodeChildren = 0;
517         if (parentView instanceof ViewGroup) {
518             numViewChildren = ((ViewGroup) parentView).getChildCount();
519         }
520         numNodeChildren = parentNode.getChildCount();
521 
522         if (isSecureWindow) {
523             assertWithMessage("ViewNode property isAssistBlocked is false")
524                     .that(parentNode.isAssistBlocked()).isTrue();
525             assertWithMessage("Secure window should only traverse root node")
526                     .that(numNodeChildren).isEqualTo(0);
527             isSecureWindow = false;
528         } else if (parentNode.getClassName().equals("android.webkit.WebView")) {
529             // WebView will also appear to have no children while the node does, traverse node
530             assertWithMessage("AssistStructure returned a WebView where the view wasn't one").that(
531                     parentView instanceof WebView).isTrue();
532 
533             boolean textInWebView = false;
534 
535             for (int i = numNodeChildren - 1; i >= 0; i--) {
536                textInWebView |= traverseWebViewForText(parentNode.getChildAt(i));
537             }
538             assertWithMessage("Did not find expected strings inside WebView").that(textInWebView)
539                     .isTrue();
540         } else {
541             assertWithMessage("Number of children did not match").that(numNodeChildren)
542                     .isEqualTo(numViewChildren);
543 
544             verifyViewProperties(parentView, parentNode);
545 
546             if (parentView instanceof ViewGroup) {
547                 parentGroup = (ViewGroup) parentView;
548 
549                 // TODO: set a max recursion level
550                 for (int i = numNodeChildren - 1; i >= 0; i--) {
551                     View childView = parentGroup.getChildAt(i);
552                     ViewNode childNode = parentNode.getChildAt(i);
553 
554                     // if isSecureWindow, should not have reached this point.
555                     assertThat(isSecureWindow).isFalse();
556                     traverseViewAndStructure(childView, childNode, isSecureWindow);
557                 }
558             }
559         }
560     }
561 
562     /**
563      * Return true if the expected strings are found in the WebView, else fail.
564      */
traverseWebViewForText(ViewNode parentNode)565     private boolean traverseWebViewForText(ViewNode parentNode) {
566         boolean textFound = false;
567         if (parentNode.getText() != null
568                 && parentNode.getText().toString().equals(Utils.WEBVIEW_HTML_GREETING)) {
569             return true;
570         }
571         for (int i = parentNode.getChildCount() - 1; i >= 0; i--) {
572             textFound |= traverseWebViewForText(parentNode.getChildAt(i));
573         }
574         return textFound;
575     }
576 
577     /**
578      * Return true if the expected domain is found in the WebView, else fail.
579      */
verifyAssistStructureHasWebDomain(String domain)580     protected void verifyAssistStructureHasWebDomain(String domain) {
581         assertThat(traverse(mAssistStructure.getWindowNodeAt(0).getRootViewNode(), (n) -> {
582             return n.getWebDomain() != null && domain.equals(n.getWebDomain());
583         })).isTrue();
584     }
585 
586     /**
587      * Return true if the expected LocaleList is found in the WebView, else fail.
588      */
verifyAssistStructureHasLocaleList(LocaleList localeList)589     protected void verifyAssistStructureHasLocaleList(LocaleList localeList) {
590         assertThat(traverse(mAssistStructure.getWindowNodeAt(0).getRootViewNode(), (n) -> {
591             return n.getLocaleList() != null && localeList.equals(n.getLocaleList());
592         })).isTrue();
593     }
594 
595     interface ViewNodeVisitor {
visit(ViewNode node)596         boolean visit(ViewNode node);
597     }
598 
traverse(ViewNode parentNode, ViewNodeVisitor visitor)599     private boolean traverse(ViewNode parentNode, ViewNodeVisitor visitor) {
600         if (visitor.visit(parentNode)) {
601             return true;
602         }
603         for (int i = parentNode.getChildCount() - 1; i >= 0; i--) {
604             if (traverse(parentNode.getChildAt(i), visitor)) {
605                 return true;
606             }
607         }
608         return false;
609     }
610 
setFeaturesEnabled(StructureEnabled structure, ScreenshotEnabled screenshot)611     protected static void setFeaturesEnabled(StructureEnabled structure,
612             ScreenshotEnabled screenshot) {
613         Log.i(TAG, "setFeaturesEnabled(" + structure + ", " + screenshot + ")");
614         sStructureEnabledMgr.set(structure.value);
615         sScreenshotEnabledMgr.set(screenshot.value);
616     }
617 
618     /**
619      * Compare view properties of the view hierarchy with that reported in the assist structure.
620      */
verifyViewProperties(View parentView, ViewNode parentNode)621     private void verifyViewProperties(View parentView, ViewNode parentNode) {
622         assertWithMessage("Left positions do not match").that(parentNode.getLeft())
623                 .isEqualTo(parentView.getLeft());
624         assertWithMessage("Top positions do not match").that(parentNode.getTop())
625                 .isEqualTo(parentView.getTop());
626         assertWithMessage("Opaque flags do not match").that(parentNode.isOpaque())
627                 .isEqualTo(parentView.isOpaque());
628 
629         int viewId = parentView.getId();
630 
631         if (viewId > 0) {
632             if (parentNode.getIdEntry() != null) {
633                 assertWithMessage("View IDs do not match.").that(parentNode.getIdEntry())
634                         .isEqualTo(mTestActivity.getResources().getResourceEntryName(viewId));
635             }
636         } else {
637             assertWithMessage("View Node should not have an ID").that(parentNode.getIdEntry())
638                     .isNull();
639         }
640 
641         Log.i(TAG, "parent text: " + parentNode.getText());
642         if (parentView instanceof TextView) {
643             Log.i(TAG, "view text: " + ((TextView) parentView).getText());
644         }
645 
646         assertWithMessage("Scroll X does not match").that(parentNode.getScrollX())
647                 .isEqualTo(parentView.getScrollX());
648         assertWithMessage("Scroll Y does not match").that(parentNode.getScrollY())
649                 .isEqualTo(parentView.getScrollY());
650         assertWithMessage("Heights do not match").that(parentNode.getHeight())
651                 .isEqualTo(parentView.getHeight());
652         assertWithMessage("Widths do not match").that(parentNode.getWidth())
653                 .isEqualTo(parentView.getWidth());
654 
655         if (parentView instanceof TextView) {
656             if (parentView instanceof EditText) {
657               assertWithMessage("Text selection start does not match")
658                       .that(parentNode.getTextSelectionStart())
659                       .isEqualTo(((EditText) parentView).getSelectionStart());
660               assertWithMessage("Text selection end does not match")
661                 .that(parentNode.getTextSelectionEnd())
662                       .isEqualTo(((EditText) parentView).getSelectionEnd());
663             }
664             TextView textView = (TextView) parentView;
665             assertThat(parentNode.getTextSize()).isWithin(0.01F).of(textView.getTextSize());
666             String viewString = textView.getText().toString();
667             String nodeString = parentNode.getText().toString();
668 
669             if (parentNode.getScrollX() == 0 && parentNode.getScrollY() == 0) {
670                 Log.i(TAG, "Verifying text within TextView at the beginning");
671                 Log.i(TAG, "view string: " + viewString);
672                 Log.i(TAG, "node string: " + nodeString);
673                 assertWithMessage("String length is unexpected: original string - %s, "
674                         + "string in AssistData - %s", viewString.length(), nodeString.length())
675                                 .that(viewString.length()).isAtLeast(nodeString.length());
676                 assertWithMessage("Expected a longer string to be shown").that(
677                         nodeString.length()).isAtLeast(Math.min(viewString.length(), 30));
678                 for (int x = 0; x < parentNode.getText().length(); x++) {
679                     assertWithMessage("Char not equal at index: %s", x).that(
680                             parentNode.getText().charAt(x)).isEqualTo(
681                             ((TextView) parentView).getText().toString().charAt(x));
682                 }
683             } else if (parentNode.getScrollX() == parentView.getWidth()) {
684 
685             }
686         } else {
687             assertThat(parentNode.getText()).isNull();
688         }
689     }
690 
setAssistResults(Bundle assistData)691     protected void setAssistResults(Bundle assistData) {
692         mIsActivityIdNull = assistData.getBoolean(Utils.ASSIST_IS_ACTIVITY_ID_NULL);;
693         mAssistBundle = assistData.getBundle(Utils.ASSIST_BUNDLE_KEY);
694         mAssistStructure = assistData.getParcelable(Utils.ASSIST_STRUCTURE_KEY);
695         mAssistContent = assistData.getParcelable(Utils.ASSIST_CONTENT_KEY);
696 
697         mScreenshot = assistData.getBoolean(Utils.ASSIST_SCREENSHOT_KEY, false);
698 
699         mScreenshotMatches = assistData.getBoolean(Utils.COMPARE_SCREENSHOT_KEY, false);
700         mOnShowArgs = assistData.getBundle(Utils.ON_SHOW_ARGS_KEY);
701     }
702 
eventuallyWithSessionClose(@onNull ThrowingRunnable runnable)703     protected void eventuallyWithSessionClose(@NonNull ThrowingRunnable runnable) throws Throwable {
704         AtomicReference<Throwable> innerThrowable = new AtomicReference<>();
705         try {
706             TIMEOUT.run(getClass().getName(), SLEEP_BEFORE_RETRY_MS, () -> {
707                 try {
708                     runnable.run();
709                     return runnable;
710                 } catch (Throwable throwable) {
711                     // Immediately close the session so the next run can redo its action
712                     mContext.sendBroadcast(new Intent(Utils.HIDE_SESSION));
713                     mSessionCompletedLatch.await(2, TimeUnit.SECONDS);
714                     innerThrowable.set(throwable);
715                     return null;
716                 }
717             });
718         } catch (Throwable throwable) {
719             Throwable inner = innerThrowable.get();
720             if (inner != null) {
721                 throw inner;
722             } else {
723                 throw throwable;
724             }
725         }
726     }
727 
728     protected enum StructureEnabled {
729         TRUE("1"), FALSE("0");
730 
731         private final String value;
732 
StructureEnabled(String value)733         private StructureEnabled(String value) {
734             this.value = value;
735         }
736 
737         @Override
toString()738         public String toString() {
739             return "structure_" + (value.equals("1") ? "enabled" : "disabled");
740         }
741 
742     }
743 
744     protected enum ScreenshotEnabled {
745         TRUE("1"), FALSE("0");
746 
747         private final String value;
748 
ScreenshotEnabled(String value)749         private ScreenshotEnabled(String value) {
750             this.value = value;
751         }
752 
753         @Override
toString()754         public String toString() {
755             return "screenshot_" + (value.equals("1") ? "enabled" : "disabled");
756         }
757     }
758 
759     public class ActionLatchReceiver {
760 
761         private final Map<String, AutoResetLatch> entries = new HashMap<>();
762 
ActionLatchReceiver(Pair<String, AutoResetLatch>.... entries)763         protected ActionLatchReceiver(Pair<String, AutoResetLatch>... entries) {
764             for (Pair<String, AutoResetLatch> entry : entries) {
765                 if (entry.second == null) {
766                     throw new IllegalArgumentException("Test cannot pass in a null latch");
767                 }
768                 this.entries.put(entry.first, entry.second);
769             }
770 
771             this.entries.put(Utils.HIDE_SESSION_COMPLETE, mSessionCompletedLatch);
772             this.entries.put(Utils.APP_3P_HASRESUMED, mHas3pResumedLatch);
773             this.entries.put(Utils.TEST_ACTIVITY_DESTROY, mHasTestDestroyedLatch);
774             this.entries.put(Utils.ASSIST_RECEIVER_REGISTERED, mReadyLatch);
775             this.entries.put(Utils.BROADCAST_ASSIST_DATA_INTENT, mAssistDataReceivedLatch);
776         }
777 
ActionLatchReceiver(String action, AutoResetLatch latch)778         protected ActionLatchReceiver(String action, AutoResetLatch latch) {
779             this(Pair.create(action, latch));
780         }
781 
onAction(Bundle bundle, String action)782         protected void onAction(Bundle bundle, String action) {
783             switch (action) {
784                 case Utils.BROADCAST_ASSIST_DATA_INTENT:
785                     AssistTestBase.this.setAssistResults(bundle);
786                     // fall-through
787                 default:
788                     AutoResetLatch latch = entries.get(action);
789                     if (latch == null) {
790                         Log.e(TAG, this.getClass() + ": invalid action " + action);
791                     } else {
792                         latch.countDown();
793                     }
794                     break;
795             }
796         }
797     }
798 }
799