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