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 android.assist.cts.TestStartActivity;
20 import android.assist.common.Utils;
21 
22 import android.app.ActivityManager;
23 import android.app.assist.AssistContent;
24 import android.app.assist.AssistStructure;
25 import android.app.assist.AssistStructure.ViewNode;
26 import android.app.assist.AssistStructure.WindowNode;
27 import android.content.BroadcastReceiver;
28 import android.content.ComponentName;
29 import android.content.Context;
30 import android.content.Intent;
31 import android.content.IntentFilter;
32 import android.content.res.Resources;
33 import android.content.res.XmlResourceParser;
34 import android.cts.util.SystemUtil;
35 import android.graphics.Bitmap;
36 import android.graphics.BitmapFactory;
37 import android.graphics.Color;
38 import android.graphics.Point;
39 import android.graphics.Rect;
40 import android.os.Bundle;
41 import android.provider.Settings;
42 import android.test.ActivityInstrumentationTestCase2;
43 import android.util.Log;
44 import android.view.Display;
45 import android.view.LayoutInflater;
46 import android.view.View;
47 import android.view.ViewGroup;
48 import android.view.Window;
49 import android.view.accessibility.AccessibilityNodeInfo;
50 import android.webkit.WebView;
51 import android.widget.EditText;
52 import android.widget.TextView;
53 
54 import java.lang.Math;
55 import java.util.concurrent.CountDownLatch;
56 import java.util.concurrent.TimeUnit;
57 
58 public class AssistTestBase extends ActivityInstrumentationTestCase2<TestStartActivity> {
59     private static final String TAG = "AssistTestBase";
60 
61     protected ActivityManager mActivityManager;
62     protected TestStartActivity mTestActivity;
63     protected AssistContent mAssistContent;
64     protected AssistStructure mAssistStructure;
65     protected boolean mScreenshot;
66     protected Bitmap mAppScreenshot;
67     protected BroadcastReceiver mReceiver;
68     protected Bundle mAssistBundle;
69     protected Context mContext;
70     protected CountDownLatch mLatch, mScreenshotLatch, mHasResumedLatch;
71     protected boolean mScreenshotMatches;
72     private Point mDisplaySize;
73     private String mTestName;
74     private View mView;
75 
AssistTestBase()76     public AssistTestBase() {
77         super(TestStartActivity.class);
78     }
79 
80     @Override
setUp()81     protected void setUp() throws Exception {
82         super.setUp();
83         mContext = getInstrumentation().getTargetContext();
84         SystemUtil.runShellCommand(getInstrumentation(),
85                 "settings put secure assist_structure_enabled 1");
86         SystemUtil.runShellCommand(getInstrumentation(),
87                 "settings put secure assist_screenshot_enabled 1");
88         logContextAndScreenshotSetting();
89 
90         // reset old values
91         mScreenshotMatches = false;
92         mScreenshot = false;
93         mAssistStructure = null;
94         mAssistContent = null;
95         mAssistBundle = null;
96 
97         if (mReceiver != null) {
98             mContext.unregisterReceiver(mReceiver);
99         }
100         mReceiver = new TestResultsReceiver();
101         mContext.registerReceiver(mReceiver,
102             new IntentFilter(Utils.BROADCAST_ASSIST_DATA_INTENT));
103     }
104 
105     @Override
tearDown()106     protected void tearDown() throws Exception {
107         mTestActivity.finish();
108         mContext.sendBroadcast(new Intent(Utils.HIDE_SESSION));
109         if (mReceiver != null) {
110             mContext.unregisterReceiver(mReceiver);
111             mReceiver = null;
112         }
113         super.tearDown();
114     }
115 
116     /**
117      * Starts the shim service activity
118      */
startTestActivity(String testName)119     protected void startTestActivity(String testName) {
120         Intent intent = new Intent();
121         mTestName = testName;
122         intent.setAction("android.intent.action.TEST_START_ACTIVITY_" + testName);
123         intent.setComponent(new ComponentName(getInstrumentation().getContext(),
124                 TestStartActivity.class));
125         intent.putExtra(Utils.TESTCASE_TYPE, testName);
126         setActivityIntent(intent);
127         mTestActivity = getActivity();
128         mActivityManager = (ActivityManager) mContext.getSystemService(Context.ACTIVITY_SERVICE);
129     }
130 
131     /**
132      * Called when waiting for Assistant's Broadcast Receiver to be setup
133      */
waitForAssistantToBeReady(CountDownLatch latch)134     public void waitForAssistantToBeReady(CountDownLatch latch) throws Exception {
135         Log.i(TAG, "waiting for assistant to be ready before continuing");
136         if (!latch.await(Utils.TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
137             fail("Assistant was not ready before timeout of: " + Utils.TIMEOUT_MS + "msec");
138         }
139     }
140 
141     /**
142      * Send broadcast to MainInteractionService to start a session
143      */
startSession()144     protected void startSession() {
145         startSession(mTestName, new Bundle());
146     }
147 
startSession(String testName, Bundle extras)148     protected void startSession(String testName, Bundle extras) {
149         Intent intent = new Intent(Utils.BROADCAST_INTENT_START_ASSIST);
150         Log.i(TAG, "passed in class test name is: " + testName);
151         intent.putExtra(Utils.TESTCASE_TYPE, testName);
152         addDimensionsToIntent(intent);
153         intent.putExtras(extras);
154         mContext.sendBroadcast(intent);
155     }
156 
157     /**
158      * Calculate display dimensions (including navbar) to pass along in the given intent.
159      */
addDimensionsToIntent(Intent intent)160     private void addDimensionsToIntent(Intent intent) {
161         if (mDisplaySize == null) {
162             Display display = mTestActivity.getWindowManager().getDefaultDisplay();
163             mDisplaySize = new Point();
164             display.getRealSize(mDisplaySize);
165         }
166         intent.putExtra(Utils.DISPLAY_WIDTH_KEY, mDisplaySize.x);
167         intent.putExtra(Utils.DISPLAY_HEIGHT_KEY, mDisplaySize.y);
168     }
169 
170     /**
171      * Called after startTestActivity. Includes check for receiving context.
172      */
waitForBroadcast()173     protected boolean waitForBroadcast() throws Exception {
174         mTestActivity.start3pApp(mTestName);
175         mTestActivity.startTest(mTestName);
176         return waitForContext();
177     }
178 
waitForContext()179     protected boolean waitForContext() throws Exception {
180         mLatch = new CountDownLatch(1);
181 
182         if (mReceiver != null) {
183             mContext.unregisterReceiver(mReceiver);
184         }
185         mReceiver = new TestResultsReceiver();
186         mContext.registerReceiver(mReceiver,
187                 new IntentFilter(Utils.BROADCAST_ASSIST_DATA_INTENT));
188 
189         if (!mLatch.await(Utils.getAssistDataTimeout(mTestName), TimeUnit.MILLISECONDS)) {
190             fail("Fail to receive broadcast in " + Utils.getAssistDataTimeout(mTestName) + "msec");
191         }
192         Log.i(TAG, "Received broadcast with all information.");
193         return true;
194     }
195 
196     /**
197      * Checks that the nullness of values are what we expect.
198      *
199      * @param isBundleNull True if assistBundle should be null.
200      * @param isStructureNull True if assistStructure should be null.
201      * @param isContentNull True if assistContent should be null.
202      * @param isScreenshotNull True if screenshot should be null.
203      */
verifyAssistDataNullness(boolean isBundleNull, boolean isStructureNull, boolean isContentNull, boolean isScreenshotNull)204     protected void verifyAssistDataNullness(boolean isBundleNull, boolean isStructureNull,
205             boolean isContentNull, boolean isScreenshotNull) {
206 
207         if ((mAssistContent == null) != isContentNull) {
208             fail(String.format("Should %s have been null - AssistContent: %s",
209                     isContentNull ? "" : "not", mAssistContent));
210         }
211 
212         if ((mAssistStructure == null) != isStructureNull) {
213             fail(String.format("Should %s have been null - AssistStructure: %s",
214                     isStructureNull ? "" : "not", mAssistStructure));
215         }
216 
217         if ((mAssistBundle == null) != isBundleNull) {
218             fail(String.format("Should %s have been null - AssistBundle: %s",
219                     isBundleNull ? "" : "not", mAssistBundle));
220         }
221 
222         if (mScreenshot == isScreenshotNull) {
223             fail(String.format("Should %s have been null - Screenshot: %s",
224                     isScreenshotNull ? "":"not", mScreenshot));
225         }
226     }
227 
228     /**
229      * Sends a broadcast with the specified scroll positions to the test app.
230      */
scrollTestApp(int scrollX, int scrollY, boolean scrollTextView, boolean scrollScrollView)231     protected void scrollTestApp(int scrollX, int scrollY, boolean scrollTextView,
232             boolean scrollScrollView) {
233         mTestActivity.scrollText(scrollX, scrollY, scrollTextView, scrollScrollView);
234         Intent intent = null;
235         if (scrollTextView) {
236             intent = new Intent(Utils.SCROLL_TEXTVIEW_ACTION);
237         } else if (scrollScrollView) {
238             intent = new Intent(Utils.SCROLL_SCROLLVIEW_ACTION);
239         }
240         intent.putExtra(Utils.SCROLL_X_POSITION, scrollX);
241         intent.putExtra(Utils.SCROLL_Y_POSITION, scrollY);
242         mContext.sendBroadcast(intent);
243     }
244 
245     /**
246      * Verifies the view hierarchy of the backgroundApp matches the assist structure.
247      *
248      * @param backgroundApp ComponentName of app the assistant is invoked upon
249      * @param isSecureWindow Denotes whether the activity has FLAG_SECURE set
250      */
verifyAssistStructure(ComponentName backgroundApp, boolean isSecureWindow)251     protected void verifyAssistStructure(ComponentName backgroundApp, boolean isSecureWindow) {
252         // Check component name matches
253         assertEquals(backgroundApp.flattenToString(),
254                 mAssistStructure.getActivityComponent().flattenToString());
255 
256         Log.i(TAG, "Traversing down structure for: " + backgroundApp.flattenToString());
257         mView = mTestActivity.findViewById(android.R.id.content).getRootView();
258         verifyHierarchy(mAssistStructure, isSecureWindow);
259     }
260 
logContextAndScreenshotSetting()261     protected void logContextAndScreenshotSetting() {
262         Log.i(TAG, "Context is: " + Settings.Secure.getString(
263                 mContext.getContentResolver(), "assist_structure_enabled"));
264         Log.i(TAG, "Screenshot is: " + Settings.Secure.getString(
265                 mContext.getContentResolver(), "assist_screenshot_enabled"));
266     }
267 
268     /**
269      * Recursively traverse and compare properties in the View hierarchy with the Assist Structure.
270      */
verifyHierarchy(AssistStructure structure, boolean isSecureWindow)271     public void verifyHierarchy(AssistStructure structure, boolean isSecureWindow) {
272         Log.i(TAG, "verifyHierarchy");
273         Window mWindow = mTestActivity.getWindow();
274 
275         int numWindows = structure.getWindowNodeCount();
276         // TODO: multiple windows?
277         assertEquals("Number of windows don't match", 1, numWindows);
278 
279         for (int i = 0; i < numWindows; i++) {
280             AssistStructure.WindowNode windowNode = structure.getWindowNodeAt(i);
281             Log.i(TAG, "Title: " + windowNode.getTitle());
282             // verify top level window bounds are as big as the screen and pinned to 0,0
283             assertEquals("Window left position wrong: was " + windowNode.getLeft(),
284                     windowNode.getLeft(), 0);
285             assertEquals("Window top position wrong: was " + windowNode.getTop(),
286                     windowNode.getTop(), 0);
287 
288             traverseViewAndStructure(
289                     mView,
290                     windowNode.getRootViewNode(),
291                     isSecureWindow);
292         }
293     }
294 
traverseViewAndStructure(View parentView, ViewNode parentNode, boolean isSecureWindow)295     private void traverseViewAndStructure(View parentView, ViewNode parentNode,
296             boolean isSecureWindow) {
297         ViewGroup parentGroup;
298 
299         if (parentView == null && parentNode == null) {
300             Log.i(TAG, "Views are null, done traversing this branch.");
301             return;
302         } else if (parentNode == null || parentView == null) {
303             fail(String.format("Views don't match. View: %s, Node: %s", parentView, parentNode));
304         }
305 
306         // Debugging
307         Log.i(TAG, "parentView is of type: " + parentView.getClass().getName());
308         if (parentView instanceof ViewGroup) {
309             for (int childInt = 0; childInt < ((ViewGroup) parentView).getChildCount();
310                     childInt++) {
311                 Log.i(TAG,
312                         "viewchild" + childInt + " is of type: "
313                         + ((ViewGroup) parentView).getChildAt(childInt).getClass().getName());
314             }
315         }
316         String parentViewId = null;
317         if (parentView.getId() > 0) {
318             parentViewId = mTestActivity.getResources().getResourceEntryName(parentView.getId());
319             Log.i(TAG, "View ID: " + parentViewId);
320         }
321 
322         Log.i(TAG, "parentNode is of type: " + parentNode.getClassName());
323         for (int nodeInt = 0; nodeInt < parentNode.getChildCount(); nodeInt++) {
324             Log.i(TAG,
325                     "nodechild" + nodeInt + " is of type: "
326                     + parentNode.getChildAt(nodeInt).getClassName());
327         }
328         Log.i(TAG, "Node ID: " + parentNode.getIdEntry());
329 
330         assertEquals("IDs do not match", parentViewId, parentNode.getIdEntry());
331 
332         int numViewChildren = 0;
333         int numNodeChildren = 0;
334         if (parentView instanceof ViewGroup) {
335             numViewChildren = ((ViewGroup) parentView).getChildCount();
336         }
337         numNodeChildren = parentNode.getChildCount();
338 
339         if (isSecureWindow) {
340             assertTrue("ViewNode property isAssistBlocked is false", parentNode.isAssistBlocked());
341             assertEquals("Secure window should only traverse root node.", 0, numNodeChildren);
342             isSecureWindow = false;
343         } else if (parentNode.getClassName().equals("android.webkit.WebView")) {
344             // WebView will also appear to have no children while the node does, traverse node
345             assertTrue("AssistStructure returned a WebView where the view wasn't one",
346                     parentView instanceof WebView);
347 
348             boolean textInWebView = false;
349 
350             for (int i = numNodeChildren - 1; i >= 0; i--) {
351                textInWebView |= traverseWebViewForText(parentNode.getChildAt(i));
352             }
353             assertTrue("Did not find expected strings inside WebView", textInWebView);
354         } else {
355             assertEquals("Number of children did not match.", numViewChildren, numNodeChildren);
356 
357             verifyViewProperties(parentView, parentNode);
358 
359             if (parentView instanceof ViewGroup) {
360                 parentGroup = (ViewGroup) parentView;
361 
362                 // TODO: set a max recursion level
363                 for (int i = numNodeChildren - 1; i >= 0; i--) {
364                     View childView = parentGroup.getChildAt(i);
365                     ViewNode childNode = parentNode.getChildAt(i);
366 
367                     // if isSecureWindow, should not have reached this point.
368                     assertFalse(isSecureWindow);
369                     traverseViewAndStructure(childView, childNode, isSecureWindow);
370                 }
371             }
372         }
373     }
374 
375     /**
376      * Return true if the expected strings are found in the WebView, else fail.
377      */
traverseWebViewForText(ViewNode parentNode)378     private boolean traverseWebViewForText(ViewNode parentNode) {
379         boolean textFound = false;
380         if (parentNode.getText() != null
381                 && parentNode.getText().toString().equals(Utils.WEBVIEW_HTML_GREETING)) {
382             return true;
383         }
384         for (int i = parentNode.getChildCount() - 1; i >= 0; i--) {
385             textFound |= traverseWebViewForText(parentNode.getChildAt(i));
386         }
387         return textFound;
388     }
389 
390     /**
391      * Compare view properties of the view hierarchy with that reported in the assist structure.
392      */
verifyViewProperties(View parentView, ViewNode parentNode)393     private void verifyViewProperties(View parentView, ViewNode parentNode) {
394         assertEquals("Left positions do not match.", parentView.getLeft(), parentNode.getLeft());
395         assertEquals("Top positions do not match.", parentView.getTop(), parentNode.getTop());
396 
397         int viewId = parentView.getId();
398 
399         if (viewId > 0) {
400             if (parentNode.getIdEntry() != null) {
401                 assertEquals("View IDs do not match.",
402                         mTestActivity.getResources().getResourceEntryName(viewId),
403                         parentNode.getIdEntry());
404             }
405         } else {
406             assertNull("View Node should not have an ID.", parentNode.getIdEntry());
407         }
408 
409         Log.i(TAG, "parent text: " + parentNode.getText());
410         if (parentView instanceof TextView) {
411             Log.i(TAG, "view text: " + ((TextView) parentView).getText());
412         }
413 
414 
415         assertEquals("Scroll X does not match.", parentView.getScrollX(), parentNode.getScrollX());
416         assertEquals("Scroll Y does not match.", parentView.getScrollY(), parentNode.getScrollY());
417         assertEquals("Heights do not match.", parentView.getHeight(), parentNode.getHeight());
418         assertEquals("Widths do not match.", parentView.getWidth(), parentNode.getWidth());
419 
420         if (parentView instanceof TextView) {
421             if (parentView instanceof EditText) {
422                 assertEquals("Text selection start does not match",
423                     ((EditText)parentView).getSelectionStart(), parentNode.getTextSelectionStart());
424                 assertEquals("Text selection end does not match",
425                         ((EditText)parentView).getSelectionEnd(), parentNode.getTextSelectionEnd());
426             }
427             TextView textView = (TextView) parentView;
428             assertEquals(textView.getTextSize(), parentNode.getTextSize());
429             String viewString = textView.getText().toString();
430             String nodeString = parentNode.getText().toString();
431 
432             if (parentNode.getScrollX() == 0 && parentNode.getScrollY() == 0) {
433                 Log.i(TAG, "Verifying text within TextView at the beginning");
434                 Log.i(TAG, "view string: " + viewString);
435                 Log.i(TAG, "node string: " + nodeString);
436                 assertTrue("String length is unexpected: original string - " + viewString.length() +
437                                 ", string in AssistData - " + nodeString.length(),
438                         viewString.length() >= nodeString.length());
439                 assertTrue("Expected a longer string to be shown. expected: "
440                                 + Math.min(viewString.length(), 30) + " was: " + nodeString
441                                 .length(),
442                         nodeString.length() >= Math.min(viewString.length(), 30));
443                 for (int x = 0; x < parentNode.getText().length(); x++) {
444                     assertEquals("Char not equal at index: " + x,
445                             ((TextView) parentView).getText().toString().charAt(x),
446                             parentNode.getText().charAt(x));
447                 }
448             } else if (parentNode.getScrollX() == parentView.getWidth()) {
449 
450             }
451         } else {
452             assertNull(parentNode.getText());
453         }
454     }
455 
456     class TestResultsReceiver extends BroadcastReceiver {
457         @Override
onReceive(Context context, Intent intent)458         public void onReceive(Context context, Intent intent) {
459             if (intent.getAction().equalsIgnoreCase(Utils.BROADCAST_ASSIST_DATA_INTENT)) {
460                 Log.i(TAG, "Received broadcast with assist data.");
461                 Bundle assistData = intent.getExtras();
462                 AssistTestBase.this.mAssistBundle = assistData.getBundle(Utils.ASSIST_BUNDLE_KEY);
463                 AssistTestBase.this.mAssistStructure = assistData.getParcelable(
464                         Utils.ASSIST_STRUCTURE_KEY);
465                 AssistTestBase.this.mAssistContent = assistData.getParcelable(
466                         Utils.ASSIST_CONTENT_KEY);
467 
468                 AssistTestBase.this.mScreenshot =
469                         assistData.getBoolean(Utils.ASSIST_SCREENSHOT_KEY, false);
470 
471                 AssistTestBase.this.mScreenshotMatches = assistData.getBoolean(
472                         Utils.COMPARE_SCREENSHOT_KEY, false);
473 
474                 if (mLatch != null) {
475                     Log.i(AssistTestBase.TAG, "counting down latch. received assist data.");
476                     mLatch.countDown();
477                 }
478             } else if (intent.getAction().equals(Utils.APP_3P_HASRESUMED)) {
479                 if (mHasResumedLatch != null) {
480                     mHasResumedLatch.countDown();
481                 }
482             }
483         }
484     }
485 }
486