1 /* 2 * Copyright (C) 2016 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 com.android.cts.usepermission; 18 19 import static junit.framework.Assert.assertEquals; 20 import static org.junit.Assert.assertNotNull; 21 import static org.junit.Assert.fail; 22 23 import android.Manifest; 24 import android.app.Activity; 25 import android.app.Instrumentation; 26 import android.content.Context; 27 import android.content.Intent; 28 import android.content.pm.PackageManager; 29 import android.content.res.Resources; 30 import android.net.Uri; 31 import android.os.Bundle; 32 import android.os.SystemClock; 33 import android.provider.Settings; 34 import android.support.test.InstrumentationRegistry; 35 import android.support.test.runner.AndroidJUnit4; 36 import android.support.test.uiautomator.By; 37 import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; 38 import android.support.test.uiautomator.UiDevice; 39 import android.support.test.uiautomator.UiObject; 40 import android.support.test.uiautomator.UiObject2; 41 import android.support.test.uiautomator.UiScrollable; 42 import android.support.test.uiautomator.UiSelector; 43 import android.util.ArrayMap; 44 import android.util.Log; 45 import android.view.accessibility.AccessibilityEvent; 46 import android.view.accessibility.AccessibilityNodeInfo; 47 import android.widget.Switch; 48 import android.widget.ScrollView; 49 import junit.framework.Assert; 50 import org.junit.Before; 51 import org.junit.runner.RunWith; 52 53 import java.util.List; 54 import java.util.Map; 55 import java.util.concurrent.Callable; 56 import java.util.concurrent.TimeoutException; 57 58 @RunWith(AndroidJUnit4.class) 59 public abstract class BasePermissionsTest { 60 private static final String PLATFORM_PACKAGE_NAME = "android"; 61 62 private static final long IDLE_TIMEOUT_MILLIS = 500; 63 private static final long GLOBAL_TIMEOUT_MILLIS = 5000; 64 65 private static final long RETRY_TIMEOUT = 3 * GLOBAL_TIMEOUT_MILLIS; 66 private static final String LOG_TAG = "BasePermissionsTest"; 67 68 private static Map<String, String> sPermissionToLabelResNameMap = new ArrayMap<>(); 69 static { 70 // Contacts sPermissionToLabelResNameMap.put(Manifest.permission.READ_CONTACTS, "@android:string/permgrouplab_contacts")71 sPermissionToLabelResNameMap.put(Manifest.permission.READ_CONTACTS, 72 "@android:string/permgrouplab_contacts"); sPermissionToLabelResNameMap.put(Manifest.permission.WRITE_CONTACTS, "@android:string/permgrouplab_contacts")73 sPermissionToLabelResNameMap.put(Manifest.permission.WRITE_CONTACTS, 74 "@android:string/permgrouplab_contacts"); 75 // Calendar sPermissionToLabelResNameMap.put(Manifest.permission.READ_CALENDAR, "@android:string/permgrouplab_calendar")76 sPermissionToLabelResNameMap.put(Manifest.permission.READ_CALENDAR, 77 "@android:string/permgrouplab_calendar"); sPermissionToLabelResNameMap.put(Manifest.permission.WRITE_CALENDAR, "@android:string/permgrouplab_calendar")78 sPermissionToLabelResNameMap.put(Manifest.permission.WRITE_CALENDAR, 79 "@android:string/permgrouplab_calendar"); 80 // SMS sPermissionToLabelResNameMap.put(Manifest.permission.SEND_SMS, "@android:string/permgrouplab_sms")81 sPermissionToLabelResNameMap.put(Manifest.permission.SEND_SMS, 82 "@android:string/permgrouplab_sms"); sPermissionToLabelResNameMap.put(Manifest.permission.RECEIVE_SMS, "@android:string/permgrouplab_sms")83 sPermissionToLabelResNameMap.put(Manifest.permission.RECEIVE_SMS, 84 "@android:string/permgrouplab_sms"); sPermissionToLabelResNameMap.put(Manifest.permission.READ_SMS, "@android:string/permgrouplab_sms")85 sPermissionToLabelResNameMap.put(Manifest.permission.READ_SMS, 86 "@android:string/permgrouplab_sms"); sPermissionToLabelResNameMap.put(Manifest.permission.RECEIVE_WAP_PUSH, "@android:string/permgrouplab_sms")87 sPermissionToLabelResNameMap.put(Manifest.permission.RECEIVE_WAP_PUSH, 88 "@android:string/permgrouplab_sms"); sPermissionToLabelResNameMap.put(Manifest.permission.RECEIVE_MMS, "@android:string/permgrouplab_sms")89 sPermissionToLabelResNameMap.put(Manifest.permission.RECEIVE_MMS, 90 "@android:string/permgrouplab_sms"); 91 sPermissionToLabelResNameMap.put("android.permission.READ_CELL_BROADCASTS", 92 "@android:string/permgrouplab_sms"); 93 // Storage sPermissionToLabelResNameMap.put(Manifest.permission.READ_EXTERNAL_STORAGE, "@android:string/permgrouplab_storage")94 sPermissionToLabelResNameMap.put(Manifest.permission.READ_EXTERNAL_STORAGE, 95 "@android:string/permgrouplab_storage"); sPermissionToLabelResNameMap.put(Manifest.permission.WRITE_EXTERNAL_STORAGE, "@android:string/permgrouplab_storage")96 sPermissionToLabelResNameMap.put(Manifest.permission.WRITE_EXTERNAL_STORAGE, 97 "@android:string/permgrouplab_storage"); 98 // Location sPermissionToLabelResNameMap.put(Manifest.permission.ACCESS_FINE_LOCATION, "@android:string/permgrouplab_location")99 sPermissionToLabelResNameMap.put(Manifest.permission.ACCESS_FINE_LOCATION, 100 "@android:string/permgrouplab_location"); sPermissionToLabelResNameMap.put(Manifest.permission.ACCESS_COARSE_LOCATION, "@android:string/permgrouplab_location")101 sPermissionToLabelResNameMap.put(Manifest.permission.ACCESS_COARSE_LOCATION, 102 "@android:string/permgrouplab_location"); 103 // Phone sPermissionToLabelResNameMap.put(Manifest.permission.READ_PHONE_STATE, "@android:string/permgrouplab_phone")104 sPermissionToLabelResNameMap.put(Manifest.permission.READ_PHONE_STATE, 105 "@android:string/permgrouplab_phone"); sPermissionToLabelResNameMap.put(Manifest.permission.CALL_PHONE, "@android:string/permgrouplab_phone")106 sPermissionToLabelResNameMap.put(Manifest.permission.CALL_PHONE, 107 "@android:string/permgrouplab_phone"); 108 sPermissionToLabelResNameMap.put("android.permission.ACCESS_IMS_CALL_SERVICE", 109 "@android:string/permgrouplab_phone"); sPermissionToLabelResNameMap.put(Manifest.permission.READ_CALL_LOG, "@android:string/permgrouplab_phone")110 sPermissionToLabelResNameMap.put(Manifest.permission.READ_CALL_LOG, 111 "@android:string/permgrouplab_phone"); sPermissionToLabelResNameMap.put(Manifest.permission.WRITE_CALL_LOG, "@android:string/permgrouplab_phone")112 sPermissionToLabelResNameMap.put(Manifest.permission.WRITE_CALL_LOG, 113 "@android:string/permgrouplab_phone"); sPermissionToLabelResNameMap.put(Manifest.permission.ADD_VOICEMAIL, "@android:string/permgrouplab_phone")114 sPermissionToLabelResNameMap.put(Manifest.permission.ADD_VOICEMAIL, 115 "@android:string/permgrouplab_phone"); sPermissionToLabelResNameMap.put(Manifest.permission.USE_SIP, "@android:string/permgrouplab_phone")116 sPermissionToLabelResNameMap.put(Manifest.permission.USE_SIP, 117 "@android:string/permgrouplab_phone"); sPermissionToLabelResNameMap.put(Manifest.permission.PROCESS_OUTGOING_CALLS, "@android:string/permgrouplab_phone")118 sPermissionToLabelResNameMap.put(Manifest.permission.PROCESS_OUTGOING_CALLS, 119 "@android:string/permgrouplab_phone"); 120 // Microphone sPermissionToLabelResNameMap.put(Manifest.permission.RECORD_AUDIO, "@android:string/permgrouplab_microphone")121 sPermissionToLabelResNameMap.put(Manifest.permission.RECORD_AUDIO, 122 "@android:string/permgrouplab_microphone"); 123 // Camera sPermissionToLabelResNameMap.put(Manifest.permission.CAMERA, "@android:string/permgrouplab_camera")124 sPermissionToLabelResNameMap.put(Manifest.permission.CAMERA, 125 "@android:string/permgrouplab_camera"); 126 // Body sensors sPermissionToLabelResNameMap.put(Manifest.permission.BODY_SENSORS, "@android:string/permgrouplab_sensors")127 sPermissionToLabelResNameMap.put(Manifest.permission.BODY_SENSORS, 128 "@android:string/permgrouplab_sensors"); 129 } 130 131 private Context mContext; 132 private Resources mPlatformResources; 133 private boolean mWatch; 134 getInstrumentation()135 protected static Instrumentation getInstrumentation() { 136 return InstrumentationRegistry.getInstrumentation(); 137 } 138 assertPermissionRequestResult(BasePermissionActivity.Result result, int requestCode, String[] permissions, boolean[] granted)139 protected static void assertPermissionRequestResult(BasePermissionActivity.Result result, 140 int requestCode, String[] permissions, boolean[] granted) { 141 assertEquals(requestCode, result.requestCode); 142 for (int i = 0; i < permissions.length; i++) { 143 assertEquals(permissions[i], result.permissions[i]); 144 assertEquals(granted[i] ? PackageManager.PERMISSION_GRANTED 145 : PackageManager.PERMISSION_DENIED, result.grantResults[i]); 146 147 } 148 } 149 getUiDevice()150 protected static UiDevice getUiDevice() { 151 return UiDevice.getInstance(getInstrumentation()); 152 } 153 launchActivity(String packageName, Class<?> clazz, Bundle extras)154 protected static Activity launchActivity(String packageName, 155 Class<?> clazz, Bundle extras) { 156 Intent intent = new Intent(Intent.ACTION_MAIN); 157 intent.setClassName(packageName, clazz.getName()); 158 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 159 if (extras != null) { 160 intent.putExtras(extras); 161 } 162 Activity activity = getInstrumentation().startActivitySync(intent); 163 getInstrumentation().waitForIdleSync(); 164 165 return activity; 166 } 167 168 @Before beforeTest()169 public void beforeTest() { 170 mContext = InstrumentationRegistry.getTargetContext(); 171 try { 172 Context platformContext = mContext.createPackageContext(PLATFORM_PACKAGE_NAME, 0); 173 mPlatformResources = platformContext.getResources(); 174 } catch (PackageManager.NameNotFoundException e) { 175 /* cannot happen */ 176 } 177 178 mWatch = mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH); 179 180 UiObject2 button = getUiDevice().findObject(By.text("Close")); 181 if (button != null) { 182 button.click(); 183 } 184 } 185 requestPermissions( String[] permissions, int requestCode, Class<?> clazz, Runnable postRequestAction)186 protected BasePermissionActivity.Result requestPermissions( 187 String[] permissions, int requestCode, Class<?> clazz, Runnable postRequestAction) 188 throws Exception { 189 // Start an activity 190 BasePermissionActivity activity = (BasePermissionActivity) launchActivity( 191 getInstrumentation().getTargetContext().getPackageName(), clazz, null); 192 193 activity.waitForOnCreate(); 194 195 // Request the permissions 196 activity.requestPermissions(permissions, requestCode); 197 198 // Define a more conservative idle criteria 199 getInstrumentation().getUiAutomation().waitForIdle( 200 IDLE_TIMEOUT_MILLIS, GLOBAL_TIMEOUT_MILLIS); 201 202 // Perform the post-request action 203 if (postRequestAction != null) { 204 postRequestAction.run(); 205 } 206 207 BasePermissionActivity.Result result = activity.getResult(); 208 activity.finish(); 209 return result; 210 } 211 clickAllowButton()212 protected void clickAllowButton() throws Exception { 213 scrollToBottomIfWatch(); 214 getUiDevice().findObject(new UiSelector().resourceId( 215 "com.android.packageinstaller:id/permission_allow_button")).click(); 216 } 217 clickDenyButton()218 protected void clickDenyButton() throws Exception { 219 scrollToBottomIfWatch(); 220 getUiDevice().findObject(new UiSelector().resourceId( 221 "com.android.packageinstaller:id/permission_deny_button")).click(); 222 } 223 clickDontAskAgainCheckbox()224 protected void clickDontAskAgainCheckbox() throws Exception { 225 getUiDevice().findObject(new UiSelector().resourceId( 226 "com.android.packageinstaller:id/do_not_ask_checkbox")).click(); 227 } 228 clickDontAskAgainButton()229 protected void clickDontAskAgainButton() throws Exception { 230 scrollToBottomIfWatch(); 231 getUiDevice().findObject(new UiSelector().resourceId( 232 "com.android.packageinstaller:id/permission_deny_dont_ask_again_button")).click(); 233 } 234 grantPermission(String permission)235 protected void grantPermission(String permission) throws Exception { 236 grantPermissions(new String[]{permission}); 237 } 238 grantPermissions(String[] permissions)239 protected void grantPermissions(String[] permissions) throws Exception { 240 setPermissionGrantState(permissions, true, false); 241 } 242 revokePermission(String permission)243 protected void revokePermission(String permission) throws Exception { 244 revokePermissions(new String[] {permission}, false); 245 } 246 revokePermissions(String[] permissions, boolean legacyApp)247 protected void revokePermissions(String[] permissions, boolean legacyApp) throws Exception { 248 setPermissionGrantState(permissions, false, legacyApp); 249 } 250 scrollToBottomIfWatch()251 private void scrollToBottomIfWatch() throws Exception { 252 if (mWatch) { 253 UiScrollable scrollable = 254 new UiScrollable(new UiSelector().className(ScrollView.class)); 255 if (scrollable.exists()) { 256 scrollable.flingToEnd(10); 257 } 258 } 259 } 260 setPermissionGrantState(String[] permissions, boolean granted, boolean legacyApp)261 private void setPermissionGrantState(String[] permissions, boolean granted, 262 boolean legacyApp) throws Exception { 263 getUiDevice().pressBack(); 264 waitForIdle(); 265 getUiDevice().pressBack(); 266 waitForIdle(); 267 getUiDevice().pressBack(); 268 waitForIdle(); 269 270 if (isTv()) { 271 getUiDevice().pressHome(); 272 waitForIdle(); 273 } 274 275 // Open the app details settings 276 Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); 277 intent.addCategory(Intent.CATEGORY_DEFAULT); 278 intent.setData(Uri.parse("package:" + mContext.getPackageName())); 279 startActivity(intent); 280 281 waitForIdle(); 282 283 // Open the permissions UI 284 String label = mContext.getResources().getString(R.string.Permissions); 285 AccessibilityNodeInfo permLabelView = getNodeTimed(() -> findByText(label), true); 286 Assert.assertNotNull("Permissions label should be present", permLabelView); 287 288 AccessibilityNodeInfo permItemView = findCollectionItem(permLabelView); 289 Assert.assertNotNull("Permissions item should be present", permItemView); 290 291 click(permItemView); 292 293 waitForIdle(); 294 295 for (String permission : permissions) { 296 // Find the permission toggle 297 String permissionLabel = getPermissionLabel(permission); 298 299 AccessibilityNodeInfo labelView = getNodeTimed(() -> findByText(permissionLabel), true); 300 Assert.assertNotNull("Permission label should be present", labelView); 301 302 AccessibilityNodeInfo itemView = findCollectionItem(labelView); 303 Assert.assertNotNull("Permission item should be present", itemView); 304 305 final AccessibilityNodeInfo toggleView = findSwitch(itemView); 306 Assert.assertNotNull("Permission toggle should be present", toggleView); 307 308 final boolean wasGranted = toggleView.isChecked(); 309 if (granted != wasGranted) { 310 // Toggle the permission 311 312 if (!itemView.getActionList().contains(AccessibilityAction.ACTION_CLICK)) { 313 click(toggleView); 314 } else { 315 click(itemView); 316 } 317 318 waitForIdle(); 319 320 if (wasGranted && legacyApp) { 321 scrollToBottomIfWatch(); 322 String packageName = getInstrumentation().getContext().getPackageManager() 323 .getPermissionControllerPackageName(); 324 String resIdName = "com.android.packageinstaller" 325 + ":string/grant_dialog_button_deny_anyway"; 326 Resources resources = getInstrumentation().getContext() 327 .createPackageContext(packageName, 0).getResources(); 328 final int confirmResId = resources.getIdentifier(resIdName, null, null); 329 String confirmTitle = resources.getString(confirmResId); 330 UiObject denyAnyway = getUiDevice().findObject(new UiSelector() 331 .textStartsWith(confirmTitle)); 332 denyAnyway.click(); 333 334 waitForIdle(); 335 } 336 } 337 } 338 339 getUiDevice().pressBack(); 340 waitForIdle(); 341 getUiDevice().pressBack(); 342 waitForIdle(); 343 } 344 getPermissionLabel(String permission)345 private String getPermissionLabel(String permission) throws Exception { 346 String labelResName = sPermissionToLabelResNameMap.get(permission); 347 assertNotNull("Unknown permisison " + permission, labelResName); 348 final int resourceId = mPlatformResources.getIdentifier(labelResName, null, null); 349 return mPlatformResources.getString(resourceId); 350 } 351 startActivity(final Intent intent)352 private void startActivity(final Intent intent) throws Exception { 353 getInstrumentation().getUiAutomation().executeAndWaitForEvent( 354 () -> { 355 try { 356 getInstrumentation().getContext().startActivity(intent); 357 } catch (Exception e) { 358 fail("Cannot start activity: " + intent); 359 } 360 }, (AccessibilityEvent event) -> event.getEventType() 361 == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED 362 , GLOBAL_TIMEOUT_MILLIS); 363 } 364 findByText(String text)365 private AccessibilityNodeInfo findByText(String text) throws Exception { 366 AccessibilityNodeInfo root = getInstrumentation().getUiAutomation().getRootInActiveWindow(); 367 AccessibilityNodeInfo result = findByText(root, text); 368 if (result != null) { 369 return result; 370 } 371 return findByTextInCollection(root, text); 372 } 373 findByText(AccessibilityNodeInfo root, String text)374 private static AccessibilityNodeInfo findByText(AccessibilityNodeInfo root, String text) { 375 List<AccessibilityNodeInfo> nodes = root.findAccessibilityNodeInfosByText(text); 376 for (AccessibilityNodeInfo node : nodes) { 377 if (node.getText().toString().equals(text)) { 378 return node; 379 } 380 } 381 return null; 382 } 383 findByTextInCollection(AccessibilityNodeInfo root, String text)384 private static AccessibilityNodeInfo findByTextInCollection(AccessibilityNodeInfo root, 385 String text) throws Exception { 386 AccessibilityNodeInfo result; 387 final int childCount = root.getChildCount(); 388 for (int i = 0; i < childCount; i++) { 389 AccessibilityNodeInfo child = root.getChild(i); 390 if (child == null) { 391 continue; 392 } 393 if (child.getCollectionInfo() != null) { 394 scrollTop(child); 395 result = getNodeTimed(() -> findByText(child, text), false); 396 if (result != null) { 397 return result; 398 } 399 try { 400 while (child.getActionList().contains(AccessibilityAction.ACTION_SCROLL_FORWARD)) { 401 scrollForward(child); 402 result = getNodeTimed(() -> findByText(child, text), false); 403 if (result != null) { 404 return result; 405 } 406 } 407 } catch (TimeoutException e) { 408 /* ignore */ 409 } 410 } else { 411 result = findByTextInCollection(child, text); 412 if (result != null) { 413 return result; 414 } 415 } 416 } 417 return null; 418 } 419 scrollTop(AccessibilityNodeInfo node)420 private static void scrollTop(AccessibilityNodeInfo node) throws Exception { 421 try { 422 while (node.getActionList().contains(AccessibilityAction.ACTION_SCROLL_BACKWARD)) { 423 scroll(node, false); 424 } 425 } catch (TimeoutException e) { 426 /* ignore */ 427 } 428 } 429 scrollForward(AccessibilityNodeInfo node)430 private static void scrollForward(AccessibilityNodeInfo node) throws Exception { 431 scroll(node, true); 432 } 433 scroll(AccessibilityNodeInfo node, boolean forward)434 private static void scroll(AccessibilityNodeInfo node, boolean forward) throws Exception { 435 getInstrumentation().getUiAutomation().executeAndWaitForEvent( 436 () -> node.performAction(forward 437 ? AccessibilityNodeInfo.ACTION_SCROLL_FORWARD 438 : AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD), 439 (AccessibilityEvent event) -> event.getEventType() 440 == AccessibilityEvent.TYPE_VIEW_SCROLLED, 441 GLOBAL_TIMEOUT_MILLIS); 442 node.refresh(); 443 waitForIdle(); 444 } 445 446 click(AccessibilityNodeInfo node)447 private static void click(AccessibilityNodeInfo node) throws Exception { 448 getInstrumentation().getUiAutomation().executeAndWaitForEvent( 449 () -> node.performAction(AccessibilityNodeInfo.ACTION_CLICK), 450 (AccessibilityEvent event) -> event.getEventType() 451 == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED, 452 GLOBAL_TIMEOUT_MILLIS); 453 } 454 findCollectionItem(AccessibilityNodeInfo current)455 private static AccessibilityNodeInfo findCollectionItem(AccessibilityNodeInfo current) 456 throws Exception { 457 AccessibilityNodeInfo result = current; 458 while (result != null) { 459 if (result.getCollectionItemInfo() != null) { 460 return result; 461 } 462 result = result.getParent(); 463 } 464 return null; 465 } 466 findSwitch(AccessibilityNodeInfo root)467 private static AccessibilityNodeInfo findSwitch(AccessibilityNodeInfo root) throws Exception { 468 if (Switch.class.getName().equals(root.getClassName().toString())) { 469 return root; 470 } 471 final int childCount = root.getChildCount(); 472 for (int i = 0; i < childCount; i++) { 473 AccessibilityNodeInfo child = root.getChild(i); 474 if (child == null) { 475 continue; 476 } 477 if (Switch.class.getName().equals(child.getClassName().toString())) { 478 return child; 479 } 480 AccessibilityNodeInfo result = findSwitch(child); 481 if (result != null) { 482 return result; 483 } 484 } 485 return null; 486 } 487 getNodeTimed( Callable<AccessibilityNodeInfo> callable, boolean retry)488 private static AccessibilityNodeInfo getNodeTimed( 489 Callable<AccessibilityNodeInfo> callable, boolean retry) throws Exception { 490 final long startTimeMillis = SystemClock.uptimeMillis(); 491 while (true) { 492 try { 493 AccessibilityNodeInfo node = callable.call(); 494 495 if (node != null) { 496 return node; 497 } 498 } catch (NullPointerException e) { 499 Log.e(LOG_TAG, "NPE while finding AccessibilityNodeInfo", e); 500 } 501 502 final long elapsedTimeMillis = SystemClock.uptimeMillis() - startTimeMillis; 503 if (!retry || elapsedTimeMillis > RETRY_TIMEOUT) { 504 return null; 505 } 506 SystemClock.sleep(2 * elapsedTimeMillis); 507 } 508 } 509 waitForIdle()510 private static void waitForIdle() throws TimeoutException { 511 getInstrumentation().getUiAutomation().waitForIdle(IDLE_TIMEOUT_MILLIS, 512 GLOBAL_TIMEOUT_MILLIS); 513 } 514 isTv()515 private static boolean isTv() { 516 return getInstrumentation().getContext().getPackageManager() 517 .hasSystemFeature(PackageManager.FEATURE_LEANBACK); 518 } 519 } 520