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