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 package com.android.cts.verifier.vr; 17 18 import android.content.ComponentName; 19 import android.content.Intent; 20 import android.os.Bundle; 21 import android.os.Handler; 22 import android.os.HandlerThread; 23 import android.provider.Settings; 24 import android.util.Log; 25 import android.view.LayoutInflater; 26 import android.view.View; 27 import android.view.ViewGroup; 28 import android.widget.Button; 29 import android.widget.ImageView; 30 import android.widget.TextView; 31 32 33 import com.android.cts.verifier.PassFailButtons; 34 import com.android.cts.verifier.R; 35 36 import java.util.Arrays; 37 import java.util.Objects; 38 import java.util.concurrent.ArrayBlockingQueue; 39 import java.util.concurrent.TimeUnit; 40 41 public class VrListenerVerifierActivity extends PassFailButtons.Activity { 42 43 private static final String TAG = "VrListenerActivity"; 44 public static final String ENABLED_VR_LISTENERS = "enabled_vr_listeners"; 45 private static final String STATE = "state"; 46 private static final int POLL_DELAY_MS = 2000; 47 static final String EXTRA_LAUNCH_SECOND_INTENT = "do2intents"; 48 49 private LayoutInflater mInflater; 50 private InteractiveTestCase[] mTests; 51 private ViewGroup mTestViews; 52 private int mCurrentIdx; 53 private Handler mMainHandler; 54 private Handler mTestHandler; 55 private HandlerThread mTestThread; 56 57 public enum Status { 58 SETUP, 59 RUNNING, 60 PASS, 61 FAIL, 62 WAIT_FOR_USER; 63 } 64 65 @Override onCreate(Bundle savedState)66 protected void onCreate(Bundle savedState) { 67 super.onCreate(savedState); 68 mCurrentIdx = (savedState == null) ? 0 : savedState.getInt(STATE, 0); 69 70 mTestThread = new HandlerThread("VrTestThread"); 71 mTestThread.start(); 72 mTestHandler = new Handler(mTestThread.getLooper()); 73 mInflater = getLayoutInflater(); 74 View v = mInflater.inflate(R.layout.vr_main, null); 75 setContentView(v); 76 setPassFailButtonClickListeners(); 77 getPassButton().setEnabled(false); 78 setInfoResources(R.string.vr_test_title, R.string.vr_info, -1); 79 80 mTestViews = (ViewGroup) v.findViewById(R.id.vr_test_items); 81 mTests = new InteractiveTestCase[] { 82 new IsDefaultDisabledTest(), 83 new UserEnableTest(), 84 new VrModeSwitchTest(), 85 new VrModeMultiSwitchTest(), 86 new UserDisableTest(), 87 }; 88 89 for (InteractiveTestCase test : mTests) { 90 test.setStatus((savedState == null) ? Status.SETUP : 91 Status.values()[savedState.getInt(test.getClass().getSimpleName(), 0)]); 92 mTestViews.addView(test.getView(mTestViews)); 93 } 94 95 updateUiState(); 96 97 mMainHandler = new Handler(); 98 } 99 100 @Override onDestroy()101 public void onDestroy() { 102 super.onDestroy(); 103 if (mTestThread != null) { 104 mTestThread.quit(); 105 } 106 } 107 108 @Override onSaveInstanceState(Bundle outState)109 protected void onSaveInstanceState(Bundle outState) { 110 outState.putInt(STATE, mCurrentIdx); 111 for (InteractiveTestCase i : mTests) { 112 outState.putInt(i.getClass().getSimpleName(), i.getStatus().ordinal()); 113 } 114 super.onSaveInstanceState(outState); 115 } 116 117 @Override onResume()118 protected void onResume() { 119 super.onResume(); 120 runNext(); 121 } 122 updateUiState()123 private void updateUiState() { 124 boolean allPassed = true; 125 for (InteractiveTestCase t : mTests) { 126 t.updateViews(); 127 if (t.getStatus() != Status.PASS) { 128 allPassed = false; 129 } 130 } 131 132 if (allPassed) { 133 getPassButton().setEnabled(true); 134 } 135 } 136 logWithStack(String message)137 protected void logWithStack(String message) { 138 logWithStack(message, null); 139 } 140 logWithStack(String message, Throwable stackTrace)141 protected void logWithStack(String message, Throwable stackTrace) { 142 if (stackTrace == null) { 143 stackTrace = new Throwable(); 144 stackTrace.fillInStackTrace(); 145 } 146 Log.e(TAG, message, stackTrace); 147 } 148 selectNext()149 private void selectNext() { 150 mCurrentIdx++; 151 if (mCurrentIdx >= mTests.length) { 152 done(); 153 return; 154 } 155 final InteractiveTestCase current = mTests[mCurrentIdx]; 156 current.markWaiting(); 157 } 158 runNext()159 private void runNext() { 160 if (mCurrentIdx >= mTests.length) { 161 done(); 162 return; 163 } 164 final InteractiveTestCase current = mTests[mCurrentIdx]; 165 mTestHandler.post(new Runnable() { 166 @Override 167 public void run() { 168 Log.i(TAG, "Starting test: " + current.getClass().getSimpleName()); 169 boolean passed = true; 170 try { 171 current.setUp(); 172 current.test(); 173 } catch (Throwable e) { 174 logWithStack("Failed " + current.getClass().getSimpleName() + " with: ", e); 175 setFailed(current); 176 passed = false; 177 } finally { 178 try { 179 current.tearDown(); 180 } catch (Throwable e) { 181 logWithStack("Failed tearDown of " + current.getClass().getSimpleName() + 182 " with: ", e); 183 setFailed(current); 184 passed = false; 185 } 186 } 187 if (passed) { 188 current.markPassed(); 189 mMainHandler.post(new Runnable() { 190 @Override 191 public void run() { 192 selectNext(); 193 } 194 }); 195 } 196 Log.i(TAG, "Done test: " + current.getClass().getSimpleName()); 197 } 198 }); 199 } 200 done()201 private void done() { 202 updateUiState(); 203 Log.i(TAG, "Completed run!"); 204 } 205 206 setFailed(final InteractiveTestCase current)207 private void setFailed(final InteractiveTestCase current) { 208 mMainHandler.post(new Runnable() { 209 @Override 210 public void run() { 211 getPassButton().setEnabled(false); 212 current.markFailed(); 213 } 214 }); 215 } 216 createUserInteractionTestView(ViewGroup parent, int stringId, int messageId)217 protected View createUserInteractionTestView(ViewGroup parent, int stringId, int messageId) { 218 View v = mInflater.inflate(R.layout.vr_item, parent, false); 219 TextView instructions = (TextView) v.findViewById(R.id.vr_instructions); 220 instructions.setText(getString(messageId)); 221 Button b = (Button) v.findViewById(R.id.vr_action_button); 222 b.setText(stringId); 223 b.setTag(stringId); 224 return v; 225 } 226 createAutoTestView(ViewGroup parent, int messageId)227 protected View createAutoTestView(ViewGroup parent, int messageId) { 228 View v = mInflater.inflate(R.layout.vr_item, parent, false); 229 TextView instructions = (TextView) v.findViewById(R.id.vr_instructions); 230 instructions.setText(getString(messageId)); 231 Button b = (Button) v.findViewById(R.id.vr_action_button); 232 b.setVisibility(View.GONE); 233 return v; 234 } 235 236 protected abstract class InteractiveTestCase { 237 protected static final String TAG = "InteractiveTest"; 238 private Status status; 239 private View view; 240 inflate(ViewGroup parent)241 abstract View inflate(ViewGroup parent); 242 getView(ViewGroup parent)243 View getView(ViewGroup parent) { 244 if (view == null) { 245 view = inflate(parent); 246 } 247 return view; 248 } 249 test()250 abstract void test() throws Throwable; 251 setUp()252 void setUp() throws Throwable { 253 // Noop 254 } 255 tearDown()256 void tearDown() throws Throwable { 257 // Noop 258 } 259 getStatus()260 Status getStatus() { 261 return status; 262 } 263 setStatus(Status s)264 void setStatus(Status s) { 265 status = s; 266 } 267 markFailed()268 void markFailed() { 269 Log.i(TAG, "FAILED test: " + this.getClass().getSimpleName()); 270 mMainHandler.post(new Runnable() { 271 @Override 272 public void run() { 273 InteractiveTestCase.this.setStatus(Status.FAIL); 274 updateViews(); 275 } 276 }); 277 } 278 markPassed()279 void markPassed() { 280 Log.i(TAG, "PASSED test: " + this.getClass().getSimpleName()); 281 mMainHandler.post(new Runnable() { 282 @Override 283 public void run() { 284 InteractiveTestCase.this.setStatus(Status.PASS); 285 updateViews(); 286 } 287 }); 288 } 289 markFocused()290 void markFocused() { 291 mMainHandler.post(new Runnable() { 292 @Override 293 public void run() { 294 InteractiveTestCase.this.setStatus(Status.SETUP); 295 updateViews(); 296 } 297 }); 298 } 299 markWaiting()300 void markWaiting() { 301 mMainHandler.post(new Runnable() { 302 @Override 303 public void run() { 304 InteractiveTestCase.this.setStatus(Status.WAIT_FOR_USER); 305 updateViews(); 306 } 307 }); 308 } 309 markRunning()310 void markRunning() { 311 mMainHandler.post(new Runnable() { 312 @Override 313 public void run() { 314 InteractiveTestCase.this.setStatus(Status.RUNNING); 315 updateViews(); 316 } 317 }); 318 } 319 updateViews()320 private void updateViews() { 321 View item = view; 322 ImageView statusView = (ImageView) item.findViewById(R.id.vr_status); 323 View button = item.findViewById(R.id.vr_action_button); 324 switch (status) { 325 case WAIT_FOR_USER: 326 statusView.setImageResource(R.drawable.fs_warning); 327 button.setEnabled(true); 328 break; 329 case SETUP: 330 statusView.setImageResource(R.drawable.fs_indeterminate); 331 button.setEnabled(false); 332 break; 333 case RUNNING: 334 statusView.setImageResource(R.drawable.fs_clock); 335 break; 336 case FAIL: 337 statusView.setImageResource(R.drawable.fs_error); 338 break; 339 case PASS: 340 statusView.setImageResource(R.drawable.fs_good); 341 button.setClickable(false); 342 button.setEnabled(false); 343 break; 344 } 345 statusView.invalidate(); 346 } 347 } 348 assertTrue(String message, boolean b)349 private static void assertTrue(String message, boolean b) { 350 if (!b) { 351 throw new IllegalStateException(message); 352 } 353 } 354 assertIn(String message, E elem, E[] c)355 private static <E> void assertIn(String message, E elem, E[] c) { 356 if (!Arrays.asList(c).contains(elem)) { 357 throw new IllegalStateException(message); 358 } 359 } 360 assertEventIn(String message, MockVrListenerService.Event elem, MockVrListenerService.EventType[] c)361 private static void assertEventIn(String message, MockVrListenerService.Event elem, 362 MockVrListenerService.EventType[] c) { 363 if (!Arrays.asList(c).contains(elem.type)) { 364 throw new IllegalStateException(message); 365 } 366 } 367 launchVrListenerSettings()368 protected void launchVrListenerSettings() { 369 VrListenerVerifierActivity.this.startActivity( 370 new Intent(Settings.ACTION_VR_LISTENER_SETTINGS)); 371 } 372 launchVrActivity()373 protected void launchVrActivity() { 374 VrListenerVerifierActivity.this.startActivity( 375 new Intent(VrListenerVerifierActivity.this, MockVrActivity.class)); 376 } 377 launchDoubleVrActivity()378 protected void launchDoubleVrActivity() { 379 VrListenerVerifierActivity.this.startActivity( 380 new Intent(VrListenerVerifierActivity.this, MockVrActivity.class). 381 putExtra(EXTRA_LAUNCH_SECOND_INTENT, true)); 382 } 383 actionPressed(View v)384 public void actionPressed(View v) { 385 Object tag = v.getTag(); 386 if (tag instanceof Integer) { 387 int id = ((Integer) tag).intValue(); 388 if (id == R.string.vr_start_settings) { 389 launchVrListenerSettings(); 390 } else if (id == R.string.vr_start_vr_activity) { 391 launchVrActivity(); 392 } else if (id == R.string.vr_start_double_vr_activity) { 393 launchDoubleVrActivity(); 394 } 395 } 396 } 397 398 private class IsDefaultDisabledTest extends InteractiveTestCase { 399 400 @Override inflate(ViewGroup parent)401 View inflate(ViewGroup parent) { 402 return createAutoTestView(parent, R.string.vr_check_disabled); 403 } 404 405 @Override setUp()406 void setUp() { 407 markFocused(); 408 } 409 410 @Override test()411 void test() { 412 assertTrue("VR listeners should not be bound by default.", 413 MockVrListenerService.getNumBoundMockVrListeners() == 0); 414 } 415 } 416 417 private class UserEnableTest extends InteractiveTestCase { 418 419 @Override inflate(ViewGroup parent)420 View inflate(ViewGroup parent) { 421 return createUserInteractionTestView(parent, R.string.vr_start_settings, 422 R.string.vr_enable_service); 423 } 424 425 @Override setUp()426 void setUp() { 427 markWaiting(); 428 } 429 430 @Override test()431 void test() { 432 String helpers = Settings.Secure.getString(getContentResolver(), ENABLED_VR_LISTENERS); 433 ComponentName c = new ComponentName(VrListenerVerifierActivity.this, 434 MockVrListenerService.class); 435 if (MockVrListenerService.getPendingEvents().size() > 0) { 436 MockVrListenerService.getPendingEvents().clear(); 437 throw new IllegalStateException("VrListenerService bound before entering VR mode!"); 438 } 439 assertTrue("Settings must now contain " + c.flattenToString(), 440 helpers != null && helpers.contains(c.flattenToString())); 441 } 442 } 443 444 private class VrModeSwitchTest extends InteractiveTestCase { 445 446 @Override inflate(ViewGroup parent)447 View inflate(ViewGroup parent) { 448 return createUserInteractionTestView(parent, R.string.vr_start_vr_activity, 449 R.string.vr_start_vr_activity_desc); 450 } 451 452 @Override setUp()453 void setUp() { 454 markWaiting(); 455 } 456 457 @Override test()458 void test() throws Throwable { 459 ArrayBlockingQueue<MockVrListenerService.Event> q = 460 MockVrListenerService.getPendingEvents(); 461 MockVrListenerService.Event e = q.poll(POLL_DELAY_MS, TimeUnit.MILLISECONDS); 462 assertTrue("Timed out before receive onCreate or onBind event from VrListenerService.", 463 e != null); 464 assertEventIn("First listener service event must be onCreate or onBind, but was " + 465 e.type, e, new MockVrListenerService.EventType[]{ 466 MockVrListenerService.EventType.ONCREATE, 467 MockVrListenerService.EventType.ONBIND 468 }); 469 if (e.type == MockVrListenerService.EventType.ONCREATE) { 470 e = q.poll(POLL_DELAY_MS, TimeUnit.MILLISECONDS); 471 assertTrue("Timed out before receive onBind event from VrListenerService.", 472 e != null); 473 assertEventIn("Second listener service event must be onBind, but was " + 474 e.type, e, new MockVrListenerService.EventType[]{ 475 MockVrListenerService.EventType.ONBIND 476 }); 477 } 478 479 e = q.poll(POLL_DELAY_MS, TimeUnit.MILLISECONDS); 480 assertTrue("Timed out before receive onCurrentVrModeActivityChanged event " + 481 "from VrListenerService.", e != null); 482 assertTrue("Listener service must receive onCurrentVrModeActivityChanged, but was " + 483 e.type, 484 e.type == MockVrListenerService.EventType.ONCURRENTVRMODEACTIVITYCHANGED); 485 ComponentName expected = new ComponentName(VrListenerVerifierActivity.this, 486 MockVrActivity.class); 487 assertTrue("Activity component must be " + expected + ", but was: " + e.arg1, 488 Objects.equals(expected, e.arg1)); 489 490 e = q.poll(POLL_DELAY_MS, TimeUnit.MILLISECONDS); 491 assertTrue("Timed out before receive unbind event from VrListenerService.", e != null); 492 assertEventIn("Listener service must receive onUnbind, but was " + 493 e.type, e, new MockVrListenerService.EventType[]{ 494 MockVrListenerService.EventType.ONUNBIND 495 }); 496 497 // Consume onDestroy 498 e = q.poll(POLL_DELAY_MS, TimeUnit.MILLISECONDS); 499 assertTrue("Timed out before receive onDestroy event from VrListenerService.", 500 e != null); 501 assertEventIn("Listener service must receive onDestroy, but was " + 502 e.type, e, new MockVrListenerService.EventType[]{ 503 MockVrListenerService.EventType.ONDESTROY 504 }); 505 506 markRunning(); 507 508 e = q.poll(POLL_DELAY_MS, TimeUnit.MILLISECONDS); 509 if (e != null) { 510 throw new IllegalStateException("Spurious event received after onDestroy: " 511 + e.type); 512 } 513 } 514 } 515 516 private class VrModeMultiSwitchTest extends InteractiveTestCase { 517 518 @Override inflate(ViewGroup parent)519 View inflate(ViewGroup parent) { 520 return createUserInteractionTestView(parent, R.string.vr_start_double_vr_activity, 521 R.string.vr_start_vr_double_activity_desc); 522 } 523 524 @Override setUp()525 void setUp() { 526 markWaiting(); 527 } 528 529 @Override test()530 void test() throws Throwable { 531 ArrayBlockingQueue<MockVrListenerService.Event> q = 532 MockVrListenerService.getPendingEvents(); 533 MockVrListenerService.Event e = q.poll(POLL_DELAY_MS, TimeUnit.MILLISECONDS); 534 assertTrue("Timed out before receive event from VrListenerService.", e != null); 535 assertEventIn("First listener service event must be onCreate or onBind, but was " + 536 e.type, e, new MockVrListenerService.EventType[]{ 537 MockVrListenerService.EventType.ONCREATE, 538 MockVrListenerService.EventType.ONBIND 539 }); 540 if (e.type == MockVrListenerService.EventType.ONCREATE) { 541 e = q.poll(POLL_DELAY_MS, TimeUnit.MILLISECONDS); 542 assertTrue("Timed out before receive event from VrListenerService.", e != null); 543 assertEventIn("Second listener service event must be onBind, but was " + 544 e.type, e, new MockVrListenerService.EventType[]{ 545 MockVrListenerService.EventType.ONBIND 546 }); 547 } 548 549 e = q.poll(POLL_DELAY_MS, TimeUnit.MILLISECONDS); 550 assertTrue("Timed out before receive event from VrListenerService.", e != null); 551 assertTrue("Listener service must receive onCurrentVrModeActivityChanged, but received " 552 + e.type, e.type == 553 MockVrListenerService.EventType.ONCURRENTVRMODEACTIVITYCHANGED); 554 ComponentName expected = new ComponentName(VrListenerVerifierActivity.this, 555 MockVrActivity.class); 556 assertTrue("Activity component must be " + expected + ", but was: " + e.arg1, 557 Objects.equals(expected, e.arg1)); 558 559 e = q.poll(POLL_DELAY_MS, TimeUnit.MILLISECONDS); 560 assertTrue("Timed out before receive event from VrListenerService.", e != null); 561 assertTrue("Listener service must receive onCurrentVrModeActivityChanged, but received " 562 + e.type, e.type == 563 MockVrListenerService.EventType.ONCURRENTVRMODEACTIVITYCHANGED); 564 ComponentName expected2 = new ComponentName(VrListenerVerifierActivity.this, 565 MockVrActivity2.class); 566 assertTrue("Activity component must be " + expected2 + ", but was: " + e.arg1, 567 Objects.equals(expected2, e.arg1)); 568 569 e = q.poll(POLL_DELAY_MS, TimeUnit.MILLISECONDS); 570 assertTrue("Timed out before receive event from VrListenerService.", e != null); 571 assertTrue("Listener service must receive onCurrentVrModeActivityChanged, but received " 572 + e.type, e.type == 573 MockVrListenerService.EventType.ONCURRENTVRMODEACTIVITYCHANGED); 574 assertTrue("Activity component must be " + expected + ", but was: " + e.arg1, 575 Objects.equals(expected, e.arg1)); 576 577 e = q.poll(POLL_DELAY_MS, TimeUnit.MILLISECONDS); 578 assertTrue("Timed out before receive event from VrListenerService.", e != null); 579 assertEventIn("Listener service must receive onUnbind, but was " + 580 e.type, e, new MockVrListenerService.EventType[]{ 581 MockVrListenerService.EventType.ONUNBIND 582 }); 583 584 // Consume onDestroy 585 e = q.poll(POLL_DELAY_MS, TimeUnit.MILLISECONDS); 586 assertTrue("Timed out before receive onDestroy event from VrListenerService.", 587 e != null); 588 assertEventIn("Listener service must receive onDestroy, but was " + 589 e.type, e, new MockVrListenerService.EventType[]{ 590 MockVrListenerService.EventType.ONDESTROY 591 }); 592 593 markRunning(); 594 595 e = q.poll(POLL_DELAY_MS, TimeUnit.MILLISECONDS); 596 if (e != null) { 597 throw new IllegalStateException("Spurious event received after onDestroy: " 598 + e.type); 599 } 600 } 601 } 602 603 private class UserDisableTest extends InteractiveTestCase { 604 605 @Override inflate(ViewGroup parent)606 View inflate(ViewGroup parent) { 607 return createUserInteractionTestView(parent, R.string.vr_start_settings, 608 R.string.vr_disable_service); 609 } 610 611 @Override setUp()612 void setUp() { 613 markWaiting(); 614 } 615 616 @Override test()617 void test() { 618 String helpers = Settings.Secure.getString(getContentResolver(), ENABLED_VR_LISTENERS); 619 ComponentName c = new ComponentName(VrListenerVerifierActivity.this, 620 MockVrListenerService.class); 621 assertTrue("Settings must no longer contain " + c.flattenToString(), 622 helpers == null || !(helpers.contains(c.flattenToString()))); 623 } 624 } 625 626 } 627