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.ComponentName; 22 import android.content.Context; 23 import android.content.Intent; 24 import android.content.IntentFilter; 25 import android.hardware.Camera; 26 import android.net.Uri; 27 import android.os.AsyncTask; 28 import android.os.Bundle; 29 import android.provider.MediaStore; 30 import android.util.Log; 31 import android.view.SurfaceHolder; 32 import android.view.View; 33 import android.view.View.OnClickListener; 34 import android.widget.Button; 35 import android.widget.ImageButton; 36 import android.widget.TextView; 37 38 import com.android.cts.verifier.camera.intents.CameraContentJobService; 39 import com.android.cts.verifier.PassFailButtons; 40 import com.android.cts.verifier.R; 41 import com.android.cts.verifier.TestResult; 42 43 import java.util.TreeSet; 44 45 /** 46 * Tests for manual verification of uri trigger being fired. 47 * 48 * MediaStore.Images.Media.EXTERNAL_CONTENT_URI - this should fire 49 * when a new picture was captured by the camera app, and it has been 50 * added to the media store. 51 * MediaStore.Video.Media.EXTERNAL_CONTENT_URI - this should fire when a new 52 * video has been captured by the camera app, and it has been added 53 * to the media store. 54 * 55 * The tests verify this both by asking the user to manually launch 56 * the camera activity, as well as by programatically launching the camera 57 * activity via MediaStore intents. 58 * 59 * Please ensure when replacing the default camera app on a device, 60 * that these intents are still firing as a lot of 3rd party applications 61 * (e.g. social network apps that upload a photo after you take a picture) 62 * rely on this functionality present and correctly working. 63 */ 64 public class CameraIntentsActivity extends PassFailButtons.Activity 65 implements OnClickListener, SurfaceHolder.Callback { 66 67 private static final String TAG = "CameraIntents"; 68 private static final int STATE_OFF = 0; 69 private static final int STATE_STARTED = 1; 70 private static final int STATE_SUCCESSFUL = 2; 71 private static final int STATE_FAILED = 3; 72 private static final int NUM_STAGES = 4; 73 private static final String STAGE_INDEX_EXTRA = "stageIndex"; 74 75 private static final int STAGE_APP_PICTURE = 0; 76 private static final int STAGE_APP_VIDEO = 1; 77 private static final int STAGE_INTENT_PICTURE = 2; 78 private static final int STAGE_INTENT_VIDEO = 3; 79 80 private ImageButton mPassButton; 81 private ImageButton mFailButton; 82 private Button mStartTestButton; 83 84 private int mState = STATE_OFF; 85 86 private boolean mActivityResult = false; 87 private boolean mDetectCheating = false; 88 89 private StringBuilder mReportBuilder = new StringBuilder(); 90 private final TreeSet<String> mTestedCombinations = new TreeSet<String>(); 91 private final TreeSet<String> mUntestedCombinations = new TreeSet<String>(); 92 93 private CameraContentJobService.TestEnvironment mTestEnv; 94 private static final int CAMERA_JOB_ID = CameraIntentsActivity.class.hashCode(); 95 private static final int JOB_TYPE_IMAGE = 0; 96 private static final int JOB_TYPE_VIDEO = 1; 97 98 private static int[] TEST_JOB_TYPES = new int[] { 99 JOB_TYPE_IMAGE, 100 JOB_TYPE_VIDEO, 101 JOB_TYPE_IMAGE, 102 JOB_TYPE_VIDEO 103 }; 104 makeJobInfo(int jobType)105 private JobInfo makeJobInfo(int jobType) { 106 JobInfo.Builder builder = new JobInfo.Builder(CAMERA_JOB_ID, 107 new ComponentName(this, CameraContentJobService.class)); 108 // Look for specific changes to images in the provider. 109 Uri uriToTrigger = null; 110 switch (jobType) { 111 case JOB_TYPE_IMAGE: 112 uriToTrigger = MediaStore.Images.Media.EXTERNAL_CONTENT_URI; 113 break; 114 case JOB_TYPE_VIDEO: 115 uriToTrigger = MediaStore.Video.Media.EXTERNAL_CONTENT_URI; 116 break; 117 default: 118 Log.e(TAG, "Unknown jobType" + jobType); 119 return null; 120 } 121 builder.addTriggerContentUri(new JobInfo.TriggerContentUri( 122 uriToTrigger, 123 JobInfo.TriggerContentUri.FLAG_NOTIFY_FOR_DESCENDANTS)); 124 // For testing purposes, react quickly. 125 builder.setTriggerContentUpdateDelay(100); 126 builder.setTriggerContentMaxDelay(100); 127 return builder.build(); 128 } 129 getStageIndex()130 private int getStageIndex() 131 { 132 final int stageIndex = getIntent().getIntExtra(STAGE_INDEX_EXTRA, 0); 133 return stageIndex; 134 } 135 getStageString(int stageIndex)136 private String getStageString(int stageIndex) 137 { 138 if (stageIndex == STAGE_APP_PICTURE) { 139 return "Application Picture"; 140 } 141 if (stageIndex == STAGE_APP_VIDEO) { 142 return "Application Video"; 143 } 144 if (stageIndex == STAGE_INTENT_PICTURE) { 145 return "Intent Picture"; 146 } 147 if (stageIndex == STAGE_INTENT_VIDEO) { 148 return "Intent Video"; 149 } 150 151 return "Unknown!!!"; 152 } 153 getStageIntentString(int stageIndex)154 private String getStageIntentString(int stageIndex) 155 { 156 if (stageIndex == STAGE_APP_PICTURE) { 157 return android.hardware.Camera.ACTION_NEW_PICTURE; 158 } 159 if (stageIndex == STAGE_APP_VIDEO) { 160 return android.hardware.Camera.ACTION_NEW_VIDEO; 161 } 162 if (stageIndex == STAGE_INTENT_PICTURE) { 163 return android.hardware.Camera.ACTION_NEW_PICTURE; 164 } 165 if (stageIndex == STAGE_INTENT_VIDEO) { 166 return android.hardware.Camera.ACTION_NEW_VIDEO; 167 } 168 169 return "Unknown Intent!!!"; 170 } 171 getStageInstructionLabel(int stageIndex)172 private String getStageInstructionLabel(int stageIndex) 173 { 174 if (stageIndex == STAGE_APP_PICTURE) { 175 return getString(R.string.ci_instruction_text_app_picture_label); 176 } 177 if (stageIndex == STAGE_APP_VIDEO) { 178 return getString(R.string.ci_instruction_text_app_video_label); 179 } 180 if (stageIndex == STAGE_INTENT_PICTURE) { 181 return getString(R.string.ci_instruction_text_intent_picture_label); 182 } 183 if (stageIndex == STAGE_INTENT_VIDEO) { 184 return getString(R.string.ci_instruction_text_intent_video_label); 185 } 186 187 return "Unknown Instruction Label!!!"; 188 } 189 190 @Override onCreate(Bundle savedInstanceState)191 public void onCreate(Bundle savedInstanceState) { 192 super.onCreate(savedInstanceState); 193 194 setContentView(R.layout.ci_main); 195 setPassFailButtonClickListeners(); 196 setInfoResources(R.string.camera_intents, R.string.ci_info, -1); 197 198 mPassButton = (ImageButton) findViewById(R.id.pass_button); 199 mFailButton = (ImageButton) findViewById(R.id.fail_button); 200 mStartTestButton = (Button) findViewById(R.id.start_test_button); 201 mStartTestButton.setOnClickListener(this); 202 203 // This activity is reused multiple times 204 // to test each camera/intents combination 205 final int stageIndex = getIntent().getIntExtra(STAGE_INDEX_EXTRA, 0); 206 207 // Hitting the pass button goes to the next test activity. 208 // Only the last one uses the PassFailButtons click callback function, 209 // which gracefully terminates the activity. 210 if (stageIndex + 1 < NUM_STAGES) { 211 setPassButtonGoesToNextStage(stageIndex); 212 } 213 resetButtons(); 214 215 // Set initial values 216 217 TextView intentsLabel = 218 (TextView) findViewById(R.id.intents_text); 219 intentsLabel.setText( 220 getString(R.string.ci_intents_label) 221 + " " 222 + Integer.toString(getStageIndex()+1) 223 + " of " 224 + Integer.toString(NUM_STAGES) 225 + ": " 226 + getStageIntentString(getStageIndex()) 227 ); 228 229 TextView instructionLabel = 230 (TextView) findViewById(R.id.instruction_text); 231 instructionLabel.setText(R.string.ci_instruction_text_photo_label); 232 233 /* Display the instructions to launch camera app and take a photo */ 234 TextView cameraExtraLabel = 235 (TextView) findViewById(R.id.instruction_extra_text); 236 cameraExtraLabel.setText(getStageInstructionLabel(getStageIndex())); 237 238 mStartTestButton.setEnabled(true); 239 } 240 241 @Override onDestroy()242 public void onDestroy() { 243 super.onDestroy(); 244 Log.v(TAG, "onDestroy"); 245 } 246 247 @Override onResume()248 public void onResume() { 249 super.onResume(); 250 } 251 252 @Override onPause()253 public void onPause() { 254 super.onPause(); 255 /* 256 When testing INTENT_PICTURE, INTENT_VIDEO, 257 do not allow user to cheat by going to camera app and re-firing 258 the intents by taking a photo/video 259 */ 260 if (getStageIndex() == STAGE_INTENT_PICTURE || 261 getStageIndex() == STAGE_INTENT_VIDEO) { 262 263 if (mActivityResult && mState == STATE_STARTED) { 264 mDetectCheating = true; 265 Log.w(TAG, "Potential cheating detected"); 266 } 267 } 268 269 } 270 271 @Override onActivityResult( int requestCode, int resultCode, Intent data)272 protected void onActivityResult( 273 int requestCode, int resultCode, Intent data) { 274 if (requestCode == 1337 + getStageIndex()) { 275 Log.v(TAG, "Activity we launched was finished"); 276 mActivityResult = true; 277 278 if (mState != STATE_FAILED 279 && getStageIndex() == STAGE_INTENT_PICTURE) { 280 mPassButton.setEnabled(true); 281 mFailButton.setEnabled(false); 282 283 mState = STATE_SUCCESSFUL; 284 /* successful, unless we get the URI trigger back 285 at some point later on */ 286 } 287 } 288 } 289 290 @Override getTestDetails()291 public String getTestDetails() { 292 return mReportBuilder.toString(); 293 } 294 295 private class WaitForTriggerTask extends AsyncTask<Void, Void, Boolean> { doInBackground(Void... param)296 protected Boolean doInBackground(Void... param) { 297 try { 298 boolean executed = mTestEnv.awaitExecution(); 299 // Check latest test param 300 if (executed && mState == STATE_STARTED) { 301 302 // this can happen if.. 303 // the camera apps intent finishes, 304 // user returns to cts verifier, 305 // user leaves cts verifier and tries to fake receiver intents 306 if (mDetectCheating) { 307 Log.w(TAG, "Cheating attempt suppressed"); 308 mState = STATE_FAILED; 309 } 310 311 // For STAGE_INTENT_PICTURE test, if EXTRA_OUTPUT is not assigned in intent, 312 // file should NOT be saved so triggering this is a test failure. 313 if (getStageIndex() == STAGE_INTENT_PICTURE) { 314 Log.e(TAG, "FAIL: STAGE_INTENT_PICTURE test should not create file"); 315 mState = STATE_FAILED; 316 } 317 318 if (mState != STATE_FAILED) { 319 mState = STATE_SUCCESSFUL; 320 return true; 321 } else { 322 return false; 323 } 324 } 325 } catch (InterruptedException e) { 326 e.printStackTrace(); 327 } 328 329 if (getStageIndex() == STAGE_INTENT_PICTURE) { 330 // STAGE_INTENT_PICTURE should timeout 331 return true; 332 } else { 333 Log.e(TAG, "FAIL: timeout waiting for URI trigger"); 334 return false; 335 } 336 } 337 onPostExecute(Boolean pass)338 protected void onPostExecute(Boolean pass) { 339 if (pass) { 340 mPassButton.setEnabled(true); 341 mFailButton.setEnabled(false); 342 } else { 343 mPassButton.setEnabled(false); 344 mFailButton.setEnabled(true); 345 } 346 } 347 } 348 349 @Override onClick(View view)350 public void onClick(View view) { 351 Log.v(TAG, "Click detected"); 352 353 final int stageIndex = getStageIndex(); 354 355 if (view == mStartTestButton) { 356 Log.v(TAG, "Starting testing... "); 357 358 359 mState = STATE_STARTED; 360 361 JobScheduler jobScheduler = (JobScheduler) getSystemService( 362 Context.JOB_SCHEDULER_SERVICE); 363 jobScheduler.cancelAll(); 364 365 mTestEnv = CameraContentJobService.TestEnvironment.getTestEnvironment(); 366 367 mTestEnv.setUp(); 368 369 JobInfo job = makeJobInfo(TEST_JOB_TYPES[stageIndex]); 370 jobScheduler.schedule(job); 371 372 new WaitForTriggerTask().execute(); 373 374 /* we can allow user to fail immediately */ 375 mFailButton.setEnabled(true); 376 377 /* trigger an ACTION_IMAGE_CAPTURE intent 378 which will run the camera app itself */ 379 String intentStr = null; 380 Intent cameraIntent = null; 381 if (stageIndex == STAGE_INTENT_PICTURE) { 382 intentStr = android.provider.MediaStore.ACTION_IMAGE_CAPTURE; 383 } 384 else if (stageIndex == STAGE_INTENT_VIDEO) { 385 intentStr = android.provider.MediaStore.ACTION_VIDEO_CAPTURE; 386 } 387 388 if (intentStr != null) { 389 cameraIntent = new Intent(intentStr); 390 startActivityForResult(cameraIntent, 1337 + getStageIndex()); 391 } 392 393 mStartTestButton.setEnabled(false); 394 } 395 396 if(view == mPassButton || view == mFailButton) { 397 // Stop any running wait 398 mTestEnv.cancelWait(); 399 400 for (int counter = 0; counter < NUM_STAGES; counter++) { 401 String combination = getStageString(counter) + "\n"; 402 403 if(counter < stageIndex) { 404 // test already passed, or else wouldn't have made 405 // it to current stageIndex 406 mTestedCombinations.add(combination); 407 } 408 409 if(counter == stageIndex) { 410 // current test configuration 411 if(view == mPassButton) { 412 mTestedCombinations.add(combination); 413 } 414 else if(view == mFailButton) { 415 mUntestedCombinations.add(combination); 416 } 417 } 418 419 if(counter > stageIndex) { 420 // test not passed yet, since haven't made it to 421 // stageIndex 422 mUntestedCombinations.add(combination); 423 } 424 425 counter++; 426 } 427 428 mReportBuilder = new StringBuilder(); 429 mReportBuilder.append("Passed combinations:\n"); 430 for (String combination : mTestedCombinations) { 431 mReportBuilder.append(combination); 432 } 433 mReportBuilder.append("Failed/untested combinations:\n"); 434 for (String combination : mUntestedCombinations) { 435 mReportBuilder.append(combination); 436 } 437 438 if(view == mPassButton) { 439 TestResult.setPassedResult(this, "CameraIntentsActivity", 440 getTestDetails()); 441 } 442 if(view == mFailButton) { 443 TestResult.setFailedResult(this, "CameraIntentsActivity", 444 getTestDetails()); 445 } 446 447 // restart activity to test next intents 448 Intent intent = new Intent(CameraIntentsActivity.this, 449 CameraIntentsActivity.class); 450 intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP 451 | Intent.FLAG_ACTIVITY_FORWARD_RESULT); 452 intent.putExtra(STAGE_INDEX_EXTRA, stageIndex + 1); 453 startActivity(intent); 454 } 455 } 456 resetButtons()457 private void resetButtons() { 458 enablePassFailButtons(false); 459 } 460 enablePassFailButtons(boolean enable)461 private void enablePassFailButtons(boolean enable) { 462 mPassButton.setEnabled(enable); 463 mFailButton.setEnabled(enable); 464 } 465 466 @Override surfaceChanged(SurfaceHolder holder, int format, int width, int height)467 public void surfaceChanged(SurfaceHolder holder, int format, int width, 468 int height) { 469 } 470 471 @Override surfaceCreated(SurfaceHolder holder)472 public void surfaceCreated(SurfaceHolder holder) { 473 // Auto-generated method stub 474 } 475 476 @Override surfaceDestroyed(SurfaceHolder holder)477 public void surfaceDestroyed(SurfaceHolder holder) { 478 // Auto-generated method stub 479 } 480 setPassButtonGoesToNextStage(final int stageIndex)481 private void setPassButtonGoesToNextStage(final int stageIndex) { 482 findViewById(R.id.pass_button).setOnClickListener(this); 483 } 484 485 } 486