1 /* 2 * Copyright (C) 2012 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.camera.intents; 17 18 import android.app.job.JobInfo; 19 import android.app.job.JobParameters; 20 import android.app.job.JobScheduler; 21 import android.content.BroadcastReceiver; 22 import android.content.ComponentName; 23 import android.content.Context; 24 import android.content.Intent; 25 import android.content.IntentFilter; 26 import android.content.pm.PackageManager; 27 import android.content.pm.PermissionInfo; 28 import android.hardware.Camera; 29 import android.media.MediaMetadataRetriever; 30 import android.net.Uri; 31 import android.os.AsyncTask; 32 import android.os.Bundle; 33 import android.provider.MediaStore; 34 import android.util.Log; 35 import android.view.SurfaceHolder; 36 import android.view.View; 37 import android.view.View.OnClickListener; 38 import android.widget.Button; 39 import android.widget.ImageButton; 40 import android.widget.TextView; 41 import androidx.core.content.FileProvider; 42 import android.Manifest; 43 44 import com.android.cts.verifier.camera.intents.CameraContentJobService; 45 import com.android.cts.verifier.PassFailButtons; 46 import com.android.cts.verifier.R; 47 import com.android.cts.verifier.TestResult; 48 import android.widget.Toast; 49 50 import static android.media.MediaMetadataRetriever.METADATA_KEY_HAS_VIDEO; 51 import static android.media.MediaMetadataRetriever.METADATA_KEY_LOCATION; 52 53 import java.io.File; 54 import java.util.TreeSet; 55 import java.util.Date; 56 import java.text.SimpleDateFormat; 57 58 /** 59 * Tests for manual verification of uri trigger and camera intents being fired. 60 * 61 * MediaStore.Images.Media.EXTERNAL_CONTENT_URI: 62 * android.hardware.Camera.ACTION_NEW_PICTURE: 63 * These should fire when a new picture was captured by the camera app, and 64 * it has been added to the media store. 65 * MediaStore.Video.Media.EXTERNAL_CONTENT_URI: 66 * android.hardware.Camera.ACTION_NEW_VIDEO: 67 * These should fire when a new video has been captured by the camera app, and 68 * it has been added to the media store. 69 * 70 * The tests verify this both by asking the user to manually launch 71 * the camera activity, as well as by programatically launching the camera 72 * activity via MediaStore intents. 73 * 74 * Please ensure when replacing the default camera app on a device, 75 * that these intents are still firing as a lot of 3rd party applications 76 * (e.g. social network apps that upload a photo after you take a picture) 77 * rely on this functionality present and correctly working. 78 */ 79 public class CameraIntentsActivity extends PassFailButtons.Activity 80 implements OnClickListener, SurfaceHolder.Callback { 81 82 private static final String TAG = "CameraIntents"; 83 private static final int STATE_OFF = 0; 84 private static final int STATE_STARTED = 1; 85 private static final int STATE_SUCCESSFUL = 2; 86 private static final int STATE_FAILED = 3; 87 88 private static final int STAGE_APP_PICTURE = 0; 89 private static final int STAGE_APP_VIDEO = 1; 90 private static final int STAGE_INTENT_PICTURE = 2; 91 private static final int STAGE_INTENT_VIDEO = 3; 92 private static final int NUM_STAGES = 4; 93 private static final String STAGE_INDEX_EXTRA = "stageIndex"; 94 95 private static String[] EXPECTED_INTENTS = new String[] { 96 Camera.ACTION_NEW_PICTURE, 97 Camera.ACTION_NEW_VIDEO, 98 null, 99 Camera.ACTION_NEW_VIDEO 100 }; 101 102 private ImageButton mPassButton; 103 private ImageButton mFailButton; 104 private Button mStartTestButton; 105 private Button mSettingsButton; 106 private File mVideoTargetDir = null; 107 private File mVideoTarget = null; 108 private int mState = STATE_OFF; 109 // MediaStore.Images.Media.EXTERNAL_CONTENT_URI or 110 // MediaStore.Video.Media.EXTERNAL_CONTENT_URI are successfully received. 111 private boolean mUriSuccess = false; 112 // android.hardware.Camera.ACTION_NEW_PICTURE or 113 // android.hardware.Camera.ACTION_NEW_VIDEO are successfully received. 114 private boolean mActionSuccess = false; 115 private Object mLock = new Object(); 116 117 private BroadcastReceiver mReceiver; 118 private IntentFilter mFilterPicture; 119 private boolean mActivityResult = false; 120 private boolean mDetectCheating = false; 121 122 private StringBuilder mReportBuilder = new StringBuilder(); 123 private final TreeSet<String> mTestedCombinations = new TreeSet<String>(); 124 private final TreeSet<String> mUntestedCombinations = new TreeSet<String>(); 125 126 private CameraContentJobService.TestEnvironment mTestEnv; 127 private static final int CAMERA_JOB_ID = CameraIntentsActivity.class.hashCode(); 128 private static final int JOB_TYPE_IMAGE = 0; 129 private static final int JOB_TYPE_VIDEO = 1; 130 131 private static int[] TEST_JOB_TYPES = new int[] { 132 JOB_TYPE_IMAGE, 133 JOB_TYPE_VIDEO, 134 JOB_TYPE_IMAGE, 135 JOB_TYPE_VIDEO 136 }; 137 138 private JobInfo makeJobInfo(int jobType) { 139 JobInfo.Builder builder = new JobInfo.Builder(CAMERA_JOB_ID, 140 new ComponentName(this, CameraContentJobService.class)); 141 // Look for specific changes to images in the provider. 142 Uri uriToTrigger = null; 143 switch (jobType) { 144 case JOB_TYPE_IMAGE: 145 uriToTrigger = MediaStore.Images.Media.EXTERNAL_CONTENT_URI; 146 break; 147 case JOB_TYPE_VIDEO: 148 uriToTrigger = MediaStore.Video.Media.EXTERNAL_CONTENT_URI; 149 break; 150 default: 151 Log.e(TAG, "Unknown jobType" + jobType); 152 return null; 153 } 154 builder.addTriggerContentUri(new JobInfo.TriggerContentUri( 155 uriToTrigger, 156 JobInfo.TriggerContentUri.FLAG_NOTIFY_FOR_DESCENDANTS)); 157 // For testing purposes, react quickly. 158 builder.setTriggerContentUpdateDelay(100); 159 builder.setTriggerContentMaxDelay(100); 160 return builder.build(); 161 } 162 163 /* Callback from mReceiver#onReceive */ 164 public void onReceivedIntent(Intent intent) { 165 Log.v(TAG, "Received intent " + intent.toString()); 166 synchronized(mLock) { 167 if (mState == STATE_STARTED) { 168 169 /* this can happen if.. 170 the camera apps intent finishes, 171 user returns to cts verifier, 172 user leaves cts verifier and tries to fake receiver intents 173 */ 174 if (mDetectCheating) { 175 Log.w(TAG, "Cheating attempt suppressed"); 176 177 mState = STATE_FAILED; 178 } 179 180 String expectedIntent = EXPECTED_INTENTS[getStageIndex()]; 181 if (expectedIntent != intent.getAction()) { 182 Log.e(TAG, "FAIL: Test # " + getStageIndex() 183 + " must not broadcast " 184 + intent.getAction() 185 + ", expected: " 186 + (expectedIntent != null ? expectedIntent : "no intent")); 187 188 mState = STATE_FAILED; 189 } 190 191 if (mState != STATE_FAILED) { 192 mActionSuccess = true; 193 } 194 updateSuccessState(); 195 } 196 } 197 } 198 199 private void updateSuccessState() { 200 if (mActionSuccess && mUriSuccess) { 201 mState = STATE_SUCCESSFUL; 202 } 203 204 setPassButton(mState == STATE_SUCCESSFUL); 205 } 206 207 private void setPassButton(Boolean pass) { 208 mPassButton.setEnabled(pass); 209 mFailButton.setEnabled(!pass); 210 } 211 212 private int getStageIndex() 213 { 214 final int stageIndex = getIntent().getIntExtra(STAGE_INDEX_EXTRA, 0); 215 return stageIndex; 216 } 217 218 private String getStageString(int stageIndex) 219 { 220 if (stageIndex == STAGE_APP_PICTURE) { 221 return "Application Picture"; 222 } 223 if (stageIndex == STAGE_APP_VIDEO) { 224 return "Application Video"; 225 } 226 if (stageIndex == STAGE_INTENT_PICTURE) { 227 return "Intent Picture"; 228 } 229 if (stageIndex == STAGE_INTENT_VIDEO) { 230 return "Intent Video"; 231 } 232 233 return "Unknown!!!"; 234 } 235 236 private String getStageIntentString(int stageIndex) 237 { 238 if (stageIndex == STAGE_APP_PICTURE) { 239 return android.hardware.Camera.ACTION_NEW_PICTURE; 240 } 241 if (stageIndex == STAGE_APP_VIDEO) { 242 return android.hardware.Camera.ACTION_NEW_VIDEO; 243 } 244 if (stageIndex == STAGE_INTENT_PICTURE) { 245 return android.hardware.Camera.ACTION_NEW_PICTURE; 246 } 247 if (stageIndex == STAGE_INTENT_VIDEO) { 248 return android.hardware.Camera.ACTION_NEW_VIDEO; 249 } 250 251 return "Unknown Intent!!!"; 252 } 253 254 private String getStageInstructionLabel(int stageIndex) 255 { 256 if (stageIndex == STAGE_APP_PICTURE) { 257 return getString(R.string.ci_instruction_text_app_picture_label); 258 } 259 if (stageIndex == STAGE_APP_VIDEO) { 260 return getString(R.string.ci_instruction_text_app_video_label); 261 } 262 if (stageIndex == STAGE_INTENT_PICTURE) { 263 return getString(R.string.ci_instruction_text_intent_picture_label); 264 } 265 if (stageIndex == STAGE_INTENT_VIDEO) { 266 return getString(R.string.ci_instruction_text_intent_video_label); 267 } 268 269 return "Unknown Instruction Label!!!"; 270 } 271 272 @Override 273 public void onCreate(Bundle savedInstanceState) { 274 super.onCreate(savedInstanceState); 275 276 setContentView(R.layout.ci_main); 277 setPassFailButtonClickListeners(); 278 setInfoResources(R.string.camera_intents, R.string.ci_info, -1); 279 280 mPassButton = (ImageButton) findViewById(R.id.pass_button); 281 mFailButton = (ImageButton) findViewById(R.id.fail_button); 282 mStartTestButton = (Button) findViewById(R.id.start_test_button); 283 mSettingsButton = (Button) findViewById(R.id.settings_button); 284 mStartTestButton.setOnClickListener(this); 285 mSettingsButton.setOnClickListener(this); 286 287 // This activity is reused multiple times 288 // to test each camera/intents combination 289 final int stageIndex = getIntent().getIntExtra(STAGE_INDEX_EXTRA, 0); 290 291 // Hitting the pass button goes to the next test activity. 292 // Only the last one uses the PassFailButtons click callback function, 293 // which gracefully terminates the activity. 294 if (stageIndex + 1 < NUM_STAGES) { 295 setPassButtonGoesToNextStage(stageIndex); 296 } 297 resetButtons(); 298 299 // Set initial values 300 301 TextView intentsLabel = 302 (TextView) findViewById(R.id.intents_text); 303 intentsLabel.setText( 304 getString(R.string.ci_intents_label) 305 + " " 306 + Integer.toString(getStageIndex()+1) 307 + " of " 308 + Integer.toString(NUM_STAGES) 309 + ": " 310 + getStageIntentString(getStageIndex()) 311 ); 312 313 TextView instructionLabel = 314 (TextView) findViewById(R.id.instruction_text); 315 instructionLabel.setText(R.string.ci_instruction_text_photo_label); 316 317 /* Display the instructions to launch camera app and take a photo */ 318 TextView cameraExtraLabel = 319 (TextView) findViewById(R.id.instruction_extra_text); 320 cameraExtraLabel.setText(getStageInstructionLabel(getStageIndex())); 321 322 mStartTestButton.setEnabled(true); 323 mSettingsButton.setEnabled(true); 324 325 mReceiver = new BroadcastReceiver() { 326 @Override 327 public void onReceive(Context context, Intent intent) { 328 onReceivedIntent(intent); 329 } 330 }; 331 332 mFilterPicture = new IntentFilter(); 333 mFilterPicture.addAction(Camera.ACTION_NEW_PICTURE); 334 mFilterPicture.addAction(Camera.ACTION_NEW_VIDEO); 335 336 try { 337 mFilterPicture.addDataType("video/*"); 338 mFilterPicture.addDataType("image/*"); 339 } 340 catch(IntentFilter.MalformedMimeTypeException e) { 341 Log.e(TAG, "Caught exceptione e " + e.toString()); 342 } 343 registerReceiver(mReceiver, mFilterPicture); 344 } 345 346 @Override 347 public void onDestroy() { 348 super.onDestroy(); 349 Log.v(TAG, "onDestroy"); 350 this.unregisterReceiver(mReceiver); 351 } 352 353 @Override 354 public void onResume() { 355 super.onResume(); 356 mFailButton.setEnabled(false); 357 /** 358 * If location is not enabled, fail buttons should be disabled, since they take us back to 359 * the original CTS Verifier activity where other tests might depend on these 360 * If we're in STAGE_INTENT_VIDEO even the pass button should be disabled till location 361 * access is turned back on for CTS Verifier. 362 */ 363 Boolean locationEnabled = (checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) == 364 PackageManager.PERMISSION_GRANTED); 365 366 if (getStageIndex() == STAGE_INTENT_VIDEO) { 367 /** 368 * Don't enable the pass /fail button till the user grants CTS verifier location 369 * access again. 370 */ 371 if (mActionSuccess) { 372 mState = STATE_SUCCESSFUL; 373 } 374 mPassButton.setEnabled(false); 375 if (locationEnabled) { 376 if (mState == STATE_SUCCESSFUL) { 377 mPassButton.setEnabled(true); 378 } else { 379 mFailButton.setEnabled(true); 380 } 381 } else if (mState != STATE_OFF) { 382 Toast.makeText(this, R.string.ci_location_permissions_error, 383 Toast.LENGTH_SHORT).show(); 384 } 385 } else { 386 if (locationEnabled) { 387 mFailButton.setEnabled(true); 388 } else { 389 Toast.makeText(this, R.string.ci_location_permissions_fail_error, 390 Toast.LENGTH_SHORT).show(); 391 } 392 } 393 } 394 395 @Override 396 public void onPause() { 397 super.onPause(); 398 /* 399 When testing INTENT_PICTURE, INTENT_VIDEO, 400 do not allow user to cheat by going to camera app and re-firing 401 the intents by taking a photo/video 402 */ 403 if (getStageIndex() == STAGE_INTENT_PICTURE || 404 getStageIndex() == STAGE_INTENT_VIDEO) { 405 406 if (mActivityResult && mState == STATE_STARTED) { 407 mDetectCheating = true; 408 Log.w(TAG, "Potential cheating detected"); 409 } 410 } 411 412 } 413 414 @Override 415 protected void onActivityResult( 416 int requestCode, int resultCode, Intent data) { 417 int stageIndex = getStageIndex(); 418 if (requestCode == 1337 + stageIndex) { 419 Log.v(TAG, "Activity we launched was finished"); 420 mActivityResult = true; 421 synchronized(mLock) { 422 if (mState != STATE_FAILED) { 423 /** 424 * For images, we don't need to do more checks, since location in image exif is 425 * checked by cts test: MediaStoreUiTest . 426 */ 427 if (stageIndex == STAGE_INTENT_PICTURE) { 428 mActionSuccess = true; 429 // Set UriSuccess to true to avoid long wait in WaitForTriggerTask 430 mUriSuccess = true; 431 updateSuccessState(); 432 return; 433 } 434 if (stageIndex != STAGE_INTENT_VIDEO) { 435 return; 436 } 437 438 if (mVideoTarget == null) { 439 Log.d(TAG, "Video target was not set"); 440 return; 441 } 442 /** 443 * Check that there is no location data in video. 444 */ 445 MediaMetadataRetriever mediaRetriever = new MediaMetadataRetriever(); 446 mediaRetriever.setDataSource(mVideoTarget.toString()); 447 if (mediaRetriever.extractMetadata(METADATA_KEY_HAS_VIDEO) == null || 448 mediaRetriever.extractMetadata(METADATA_KEY_LOCATION) != null) { 449 mState = STATE_FAILED; 450 } else { 451 mVideoTarget.delete(); 452 } 453 Log.d(TAG, "METADATA_KEY_HAS_VIDEO: " + 454 mediaRetriever.extractMetadata(METADATA_KEY_HAS_VIDEO) + 455 " METADATA_KEY_LOCATION: " + 456 mediaRetriever.extractMetadata(METADATA_KEY_LOCATION)); 457 mediaRetriever.release(); 458 /* successful, unless we get the URI trigger back 459 at some point later on */ 460 mActionSuccess = true; 461 } 462 } 463 } 464 } 465 466 @Override 467 public String getTestDetails() { 468 return mReportBuilder.toString(); 469 } 470 471 private class WaitForTriggerTask extends AsyncTask<Void, Void, Boolean> { 472 protected Boolean doInBackground(Void... param) { 473 try { 474 boolean executed = mTestEnv.awaitExecution(); 475 synchronized(mLock) { 476 // Check latest test param 477 if (executed && mState == STATE_STARTED) { 478 479 // this can happen if.. 480 // the camera apps intent finishes, 481 // user returns to cts verifier, 482 // user leaves cts verifier and tries to fake receiver intents 483 if (mDetectCheating) { 484 Log.w(TAG, "Cheating attempt suppressed"); 485 mState = STATE_FAILED; 486 } 487 488 // For STAGE_INTENT_PICTURE test, if EXTRA_OUTPUT is not assigned in intent, 489 // file should NOT be saved so triggering this is a test failure. 490 if (getStageIndex() == STAGE_INTENT_PICTURE) { 491 Log.e(TAG, "FAIL: STAGE_INTENT_PICTURE test should not create file"); 492 mState = STATE_FAILED; 493 } 494 495 if (mState != STATE_FAILED) { 496 return true; 497 } else { 498 return false; 499 } 500 } 501 } 502 } catch (InterruptedException e) { 503 e.printStackTrace(); 504 } 505 506 if (getStageIndex() == STAGE_INTENT_PICTURE) { 507 // STAGE_INTENT_PICTURE should timeout 508 return true; 509 } else { 510 Log.e(TAG, "FAIL: timeout waiting for URI trigger"); 511 return false; 512 } 513 } 514 515 protected void onPostExecute(Boolean pass) { 516 synchronized(mLock) { 517 mUriSuccess = pass; 518 updateSuccessState(); 519 } 520 } 521 } 522 523 @Override 524 public void onClick(View view) { 525 Log.v(TAG, "Click detected"); 526 527 final int stageIndex = getStageIndex(); 528 if (view == mSettingsButton) { 529 Log.v(TAG, "Opening up Settings app"); 530 startActivity(new Intent(android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS)); 531 } 532 533 if (view == mStartTestButton) { 534 Log.v(TAG, "Starting testing... "); 535 536 mState = STATE_STARTED; 537 mUriSuccess = false; 538 mActionSuccess = false; 539 540 JobScheduler jobScheduler = (JobScheduler) getSystemService( 541 Context.JOB_SCHEDULER_SERVICE); 542 jobScheduler.cancelAll(); 543 544 mTestEnv = CameraContentJobService.TestEnvironment.getTestEnvironment(); 545 546 mTestEnv.setUp(); 547 548 /** 549 * Video intents do not need to wait on a ContentProvider broadcast since we're starting 550 * the intent activity with EXTRA_OUTPUT set 551 */ 552 if (stageIndex != STAGE_INTENT_VIDEO) { 553 JobInfo job = makeJobInfo(TEST_JOB_TYPES[stageIndex]); 554 jobScheduler.schedule(job); 555 new WaitForTriggerTask().execute(); 556 } 557 558 /* we can allow user to fail immediately if location is on, otherwise they must 559 * enable location */ 560 if (checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) == 561 PackageManager.PERMISSION_GRANTED) { 562 mFailButton.setEnabled(true); 563 } 564 565 /* trigger an ACTION_IMAGE_CAPTURE intent 566 which will run the camera app itself */ 567 String intentStr = null; 568 Intent cameraIntent = null; 569 if (stageIndex == STAGE_INTENT_PICTURE) { 570 intentStr = android.provider.MediaStore.ACTION_IMAGE_CAPTURE; 571 } 572 else if (stageIndex == STAGE_INTENT_VIDEO) { 573 intentStr = android.provider.MediaStore.ACTION_VIDEO_CAPTURE; 574 } 575 576 if (intentStr != null) { 577 cameraIntent = new Intent(intentStr); 578 if (stageIndex == STAGE_INTENT_VIDEO) { 579 String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date()); 580 mVideoTargetDir = new File(this.getFilesDir(), "debug"); 581 mVideoTarget = new File(mVideoTargetDir, timeStamp + "video.mp4"); 582 mVideoTargetDir.mkdirs(); 583 if (!mVideoTargetDir.exists()) { 584 Toast.makeText(this, R.string.ci_directory_creation_error, 585 Toast.LENGTH_SHORT).show(); 586 Log.v(TAG, "Could not create directory"); 587 return; 588 } 589 cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, FileProvider.getUriForFile(this, 590 "com.android.cts.verifier.managedprovisioning.fileprovider", 591 mVideoTarget)); 592 } 593 startActivityForResult(cameraIntent, 1337 + getStageIndex()); 594 } 595 596 mStartTestButton.setEnabled(false); 597 } 598 599 if(view == mPassButton || view == mFailButton) { 600 // Stop any running wait 601 mTestEnv.cancelWait(); 602 603 for (int counter = 0; counter < NUM_STAGES; counter++) { 604 String combination = getStageString(counter) + "\n"; 605 606 if(counter < stageIndex) { 607 // test already passed, or else wouldn't have made 608 // it to current stageIndex 609 mTestedCombinations.add(combination); 610 } 611 612 if(counter == stageIndex) { 613 // current test configuration 614 if(view == mPassButton) { 615 mTestedCombinations.add(combination); 616 } 617 else if(view == mFailButton) { 618 mUntestedCombinations.add(combination); 619 } 620 } 621 622 if(counter > stageIndex) { 623 // test not passed yet, since haven't made it to 624 // stageIndex 625 mUntestedCombinations.add(combination); 626 } 627 628 counter++; 629 } 630 631 mReportBuilder = new StringBuilder(); 632 mReportBuilder.append("Passed combinations:\n"); 633 for (String combination : mTestedCombinations) { 634 mReportBuilder.append(combination); 635 } 636 mReportBuilder.append("Failed/untested combinations:\n"); 637 for (String combination : mUntestedCombinations) { 638 mReportBuilder.append(combination); 639 } 640 641 if(view == mPassButton) { 642 TestResult.setPassedResult(this, "CameraIntentsActivity", 643 getTestDetails()); 644 } 645 if(view == mFailButton) { 646 TestResult.setFailedResult(this, "CameraIntentsActivity", 647 getTestDetails()); 648 } 649 650 // restart activity to test next intents 651 Intent intent = new Intent(CameraIntentsActivity.this, 652 CameraIntentsActivity.class); 653 intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP 654 | Intent.FLAG_ACTIVITY_FORWARD_RESULT); 655 intent.putExtra(STAGE_INDEX_EXTRA, stageIndex + 1); 656 startActivity(intent); 657 } 658 } 659 660 private void resetButtons() { 661 enablePassFailButtons(false); 662 } 663 664 private void enablePassFailButtons(boolean enable) { 665 mPassButton.setEnabled(enable); 666 mFailButton.setEnabled(enable); 667 } 668 669 @Override 670 public void surfaceChanged(SurfaceHolder holder, int format, int width, 671 int height) { 672 } 673 674 @Override 675 public void surfaceCreated(SurfaceHolder holder) { 676 // Auto-generated method stub 677 } 678 679 @Override 680 public void surfaceDestroyed(SurfaceHolder holder) { 681 // Auto-generated method stub 682 } 683 684 private void setPassButtonGoesToNextStage(final int stageIndex) { 685 findViewById(R.id.pass_button).setOnClickListener(this); 686 } 687 688 } 689