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