1 /*
2  * Copyright (C) 2013 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.video;
17 
18 import android.app.AlertDialog;
19 import android.content.Context;
20 import android.content.DialogInterface;
21 import android.graphics.Matrix;
22 import android.graphics.SurfaceTexture;
23 import android.hardware.Camera;
24 import android.hardware.Camera.CameraInfo;
25 import android.hardware.Camera.Size;
26 import android.hardware.camera2.CameraAccessException;
27 import android.hardware.camera2.CameraCharacteristics;
28 import android.hardware.camera2.CameraManager;
29 import android.media.CamcorderProfile;
30 import android.media.MediaPlayer;
31 import android.media.MediaRecorder;
32 import android.os.Bundle;
33 import android.os.Environment;
34 import android.os.Handler;
35 import android.text.method.ScrollingMovementMethod;
36 import android.util.Log;
37 import android.view.Surface;
38 import android.view.TextureView;
39 import android.view.View;
40 import android.widget.AdapterView;
41 import android.widget.ArrayAdapter;
42 import android.widget.Button;
43 import android.widget.ImageButton;
44 import android.widget.Spinner;
45 import android.widget.TextView;
46 import android.widget.Toast;
47 import android.widget.VideoView;
48 
49 import com.android.cts.verifier.PassFailButtons;
50 import com.android.cts.verifier.R;
51 
52 import java.io.File;
53 import java.io.IOException;
54 import java.text.SimpleDateFormat;
55 import java.util.ArrayList;
56 import java.util.Comparator;
57 import java.util.Date;
58 import java.util.List;
59 import java.util.Optional;
60 import java.util.TreeSet;
61 
62 
63 /**
64  * Tests for manual verification of camera video capture
65  */
66 public class CameraVideoActivity extends PassFailButtons.Activity
67         implements TextureView.SurfaceTextureListener {
68 
69     private static final String TAG = "CtsCameraVideo";
70     private static final boolean VERBOSE = Log.isLoggable(TAG, Log.VERBOSE);
71     private static final int MEDIA_TYPE_IMAGE = 1;
72     private static final int MEDIA_TYPE_VIDEO = 2;
73     private static final int VIDEO_LENGTH = 3000; // in ms
74 
75     private TextureView mPreviewView;
76     private SurfaceTexture mPreviewTexture;
77     private int mPreviewTexWidth;
78     private int mPreviewTexHeight;
79     private int mPreviewRotation;
80     private int mVideoRotation;
81 
82     private VideoView mPlaybackView;
83 
84     private Spinner mCameraSpinner;
85     private Spinner mResolutionSpinner;
86 
87     private int mCurrentCameraId = -1;
88     private Camera mCamera;
89     private boolean mIsExternalCamera;
90 
91     private MediaRecorder mMediaRecorder;
92 
93     private List<Size> mPreviewSizes;
94     private Size mNextPreviewSize;
95     private Size mPreviewSize;
96     private List<Integer> mVideoSizeIds;
97     private List<String> mVideoSizeNames;
98     private int mCurrentVideoSizeId;
99     private String mCurrentVideoSizeName;
100 
101     private boolean isRecording = false;
102     private boolean isPlayingBack = false;
103     private Button captureButton;
104     private ImageButton mPassButton;
105     private ImageButton mFailButton;
106 
107     private TextView mStatusLabel;
108 
109     private TreeSet<CameraCombination> mTestedCombinations = new TreeSet<>(COMPARATOR);
110     private TreeSet<CameraCombination> mUntestedCombinations = new TreeSet<>(COMPARATOR);
111     private TreeSet<String> mUntestedCameras = new TreeSet<>();
112 
113     private File outputVideoFile;
114 
115     private class CameraCombination {
116         private final int mCameraIndex;
117         private final int mVideoSizeIdIndex;
118         private final String mVideoSizeName;
119 
CameraCombination( int cameraIndex, int videoSizeIdIndex, String videoSizeName)120         private CameraCombination(
121             int cameraIndex, int videoSizeIdIndex, String videoSizeName) {
122             this.mCameraIndex = cameraIndex;
123             this.mVideoSizeIdIndex = videoSizeIdIndex;
124             this.mVideoSizeName = videoSizeName;
125         }
126 
127         @Override
toString()128         public String toString() {
129             return String.format("Camera %d, %s", mCameraIndex, mVideoSizeName);
130         }
131     }
132 
133     private static final Comparator<CameraCombination> COMPARATOR =
134         Comparator.<CameraCombination, Integer>comparing(c -> c.mCameraIndex)
135             .thenComparing(c -> c.mVideoSizeIdIndex);
136 
137     /**
138      * @see #MEDIA_TYPE_IMAGE
139      * @see #MEDIA_TYPE_VIDEO
140      */
getOutputMediaFile(int type)141     private static File getOutputMediaFile(int type) {
142         // Question: why do I need to comment this to get it working?
143         // Logcat says "external storage not ready"
144         // if (Environment.getExternalStorageState() != Environment.MEDIA_MOUNTED) {
145         //     Log.e(TAG, "external storage not ready");
146         //     return null;
147         // }
148 
149         File mediaStorageDir = new File(Environment.getExternalStoragePublicDirectory(
150                 Environment.DIRECTORY_MOVIES), TAG);
151 
152         if (!mediaStorageDir.exists()) {
153             if (!mediaStorageDir.mkdirs()) {
154                 Log.d(TAG, "failed to create directory");
155                 return null;
156             }
157         }
158 
159         String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
160         File mediaFile;
161         if (type == MEDIA_TYPE_IMAGE) {
162             mediaFile = new File(mediaStorageDir.getPath() + File.separator +
163                     "IMG_" + timeStamp + ".jpg");
164         } else if (type == MEDIA_TYPE_VIDEO) {
165             mediaFile = new File(mediaStorageDir.getPath() + File.separator +
166                     "VID_" + timeStamp + ".mp4");
167             if (VERBOSE) {
168                 Log.v(TAG, "getOutputMediaFile: output file " + mediaFile.getPath());
169             }
170         } else {
171             return null;
172         }
173 
174         return mediaFile;
175     }
176 
177     private static final int BIT_RATE_720P = 8000000;
178     private static final int BIT_RATE_MIN = 64000;
179     private static final int BIT_RATE_MAX = BIT_RATE_720P;
180 
getVideoBitRate(Camera.Size sz)181     private int getVideoBitRate(Camera.Size sz) {
182         int rate = BIT_RATE_720P;
183         float scaleFactor = sz.height * sz.width / (float)(1280 * 720);
184         rate = (int)(rate * scaleFactor);
185 
186         // Clamp to the MIN, MAX range.
187         return Math.max(BIT_RATE_MIN, Math.min(BIT_RATE_MAX, rate));
188     }
189 
prepareVideoRecorder()190     private boolean prepareVideoRecorder() {
191 
192         mMediaRecorder = new MediaRecorder();
193 
194         // Step 1: unlock and set camera to MediaRecorder
195         mCamera.unlock();
196         mMediaRecorder.setCamera(mCamera);
197 
198         // Step 2: set sources
199         mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.CAMCORDER);
200         mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA);
201 
202         // Step 3: set a CamcorderProfile
203         if (mIsExternalCamera) {
204             Camera.Size recordSize = null;
205             switch (mCurrentVideoSizeId) {
206                 case CamcorderProfile.QUALITY_QCIF:
207                     recordSize = mCamera.new Size(176, 144);
208                 break;
209                 case CamcorderProfile.QUALITY_QVGA:
210                     recordSize = mCamera.new Size(320, 240);
211                 break;
212                 case CamcorderProfile.QUALITY_CIF:
213                     recordSize = mCamera.new Size(352, 288);
214                 break;
215                 case CamcorderProfile.QUALITY_480P:
216                     recordSize = mCamera.new Size(720, 480);
217                 break;
218                 case CamcorderProfile.QUALITY_720P:
219                     recordSize = mCamera.new Size(1280, 720);
220                 break;
221                 default:
222                     String msg = "Unknown CamcorderProfile: " + mCurrentVideoSizeId;
223                     Log.e(TAG, msg);
224                     releaseMediaRecorder();
225                     throw new AssertionError(msg);
226             }
227 
228             mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.DEFAULT);
229             mMediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.DEFAULT);
230             mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.DEFAULT);
231             mMediaRecorder.setVideoEncodingBitRate(getVideoBitRate(recordSize));
232             mMediaRecorder.setVideoSize(recordSize.width, recordSize.height);
233         } else {
234             mMediaRecorder.setProfile(CamcorderProfile.get(mCurrentCameraId, mCurrentVideoSizeId));
235         }
236 
237         // Step 4: set output file
238         outputVideoFile = getOutputMediaFile(MEDIA_TYPE_VIDEO);
239         mMediaRecorder.setOutputFile(outputVideoFile.toString());
240 
241         // Step 5: set preview output
242         // This is not necessary since preview has been taken care of
243 
244         // Step 6: set orientation hint
245         mMediaRecorder.setOrientationHint(mVideoRotation);
246 
247         // Step 7: prepare configured MediaRecorder
248         try {
249             mMediaRecorder.prepare();
250         } catch (IOException e) {
251             Log.e(TAG, "IOException preparing MediaRecorder: ", e);
252             releaseMediaRecorder();
253             throw new AssertionError(e);
254         }
255 
256         mMediaRecorder.setOnErrorListener(
257                 new MediaRecorder.OnErrorListener() {
258                     @Override
259                     public void onError(MediaRecorder mr, int what, int extra) {
260                         if (what == MediaRecorder.MEDIA_RECORDER_ERROR_UNKNOWN) {
261                             Log.e(TAG, "unknown error in media recorder, error: " + extra);
262                         } else {
263                             Log.e(TAG, "media recorder server died, error: " + extra);
264                         }
265 
266                         failTest("Media recorder error.");
267                     }
268                 });
269 
270         if (VERBOSE) {
271             Log.v(TAG, "prepareVideoRecorder: prepared configured MediaRecorder");
272         }
273 
274         return true;
275     }
276 
277     @Override
onCreate(Bundle savedInstanceState)278     public void onCreate(Bundle savedInstanceState) {
279         super.onCreate(savedInstanceState);
280 
281         setContentView(R.layout.camera_video);
282         setPassFailButtonClickListeners();
283         setInfoResources(R.string.camera_video, R.string.video_info, /*viewId*/-1);
284 
285         mPreviewView = (TextureView) findViewById(R.id.video_capture);
286         mPlaybackView = (VideoView) findViewById(R.id.video_playback);
287         mPlaybackView.setOnCompletionListener(mPlaybackViewListener);
288 
289         captureButton = (Button) findViewById(R.id.record_button);
290         mPassButton = (ImageButton) findViewById(R.id.pass_button);
291         mFailButton = (ImageButton) findViewById(R.id.fail_button);
292         mPassButton.setEnabled(false);
293         mFailButton.setEnabled(true);
294 
295         mPreviewView.setSurfaceTextureListener(this);
296 
297         int numCameras = Camera.getNumberOfCameras();
298         String[] cameraNames = new String[numCameras];
299         for (int i = 0; i < numCameras; i++) {
300             cameraNames[i] = "Camera " + i;
301             mUntestedCameras.add("All combinations for Camera " + i + "\n");
302         }
303         if (VERBOSE) {
304             Log.v(TAG, "onCreate: number of cameras=" + numCameras);
305         }
306         mCameraSpinner = (Spinner) findViewById(R.id.cameras_selection);
307         mCameraSpinner.setAdapter(
308             new ArrayAdapter<String>(
309                 this, R.layout.cf_format_list_item, cameraNames));
310         mCameraSpinner.setOnItemSelectedListener(mCameraSpinnerListener);
311 
312         mResolutionSpinner = (Spinner) findViewById(R.id.resolution_selection);
313         mResolutionSpinner.setOnItemSelectedListener(mResolutionSelectedListener);
314 
315         mStatusLabel = (TextView) findViewById(R.id.status_label);
316 
317         Button mNextButton = (Button) findViewById(R.id.next_button);
318         mNextButton.setOnClickListener(v -> {
319             setUntestedCombination();
320             if (VERBOSE) {
321                 Log.v(TAG, "onClick: mCurrentVideoSizeId = " +
322                     mCurrentVideoSizeId + " " + mCurrentVideoSizeName);
323                 Log.v(TAG, "onClick: setting preview size "
324                     + mNextPreviewSize.width + "x" + mNextPreviewSize.height);
325             }
326 
327             startPreview();
328             if (VERBOSE) {
329                 Log.v(TAG, "onClick: started new preview");
330             }
331             captureButton.performClick();
332         });
333     }
334 
335     /**
336      * Set an untested combination of the current camera and video size.
337      * Triggered by next button click.
338      */
setUntestedCombination()339     private void setUntestedCombination() {
340         Optional<CameraCombination> combination = mUntestedCombinations.stream().filter(
341             c -> c.mCameraIndex == mCurrentCameraId).findFirst();
342         if (!combination.isPresent()) {
343             Toast.makeText(this, "All Camera " + mCurrentCameraId + " tests are done.",
344                 Toast.LENGTH_SHORT).show();
345             return;
346         }
347 
348         // There is untested combination for the current camera, set the next untested combination.
349         int mNextVideoSizeIdIndex = combination.get().mVideoSizeIdIndex;
350 
351         mCurrentVideoSizeId = mVideoSizeIds.get(mNextVideoSizeIdIndex);
352         mCurrentVideoSizeName = mVideoSizeNames.get(mNextVideoSizeIdIndex);
353         mNextPreviewSize = matchPreviewRecordSize();
354         mResolutionSpinner.setSelection(mNextVideoSizeIdIndex);
355     }
356 
357     @Override
onResume()358     public void onResume() {
359         super.onResume();
360 
361         setUpCamera(mCameraSpinner.getSelectedItemPosition());
362         if (VERBOSE) {
363             Log.v(TAG, "onResume: camera has been setup");
364         }
365 
366         setUpCaptureButton();
367         if (VERBOSE) {
368             Log.v(TAG, "onResume: captureButton has been setup");
369         }
370 
371     }
372 
373     @Override
onPause()374     public void onPause() {
375         super.onPause();
376 
377         releaseMediaRecorder();
378         shutdownCamera();
379         mPreviewTexture = null;
380     }
381 
382     private MediaPlayer.OnCompletionListener mPlaybackViewListener =
383             new MediaPlayer.OnCompletionListener() {
384 
385                 @Override
386                 public void onCompletion(MediaPlayer mp) {
387                     isPlayingBack = false;
388                     mPlaybackView.stopPlayback();
389                     captureButton.setEnabled(true);
390 
391                     mStatusLabel.setMovementMethod(new ScrollingMovementMethod());
392                     StringBuilder progress = new StringBuilder();
393                     progress.append(getResources().getString(R.string.status_ready));
394                     progress.append("\n---- Progress ----\n");
395                     progress.append(getTestDetails());
396                     mStatusLabel.setText(progress.toString());
397                 }
398 
399     };
400 
releaseMediaRecorder()401     private void releaseMediaRecorder() {
402         if (mMediaRecorder != null) {
403             mMediaRecorder.reset();
404             mMediaRecorder.release();
405             mMediaRecorder = null;
406             mCamera.lock(); // check here, lock camera for later use
407         }
408     }
409 
410     @Override
getTestDetails()411     public String getTestDetails() {
412         StringBuilder reportBuilder = new StringBuilder();
413         reportBuilder.append("Tested combinations:\n");
414         for (CameraCombination combination: mTestedCombinations) {
415             reportBuilder.append(combination);
416             reportBuilder.append("\n");
417         }
418         reportBuilder.append("Untested combinations:\n");
419         for (String untestedCam : mUntestedCameras) {
420             reportBuilder.append(untestedCam);
421         }
422         for (CameraCombination combination: mUntestedCombinations) {
423             reportBuilder.append(combination);
424             reportBuilder.append("\n");
425         }
426         return reportBuilder.toString();
427     }
428 
429     @Override
onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height)430     public void onSurfaceTextureAvailable(SurfaceTexture surface,
431             int width, int height) {
432         mPreviewTexture = surface;
433         mPreviewTexWidth = width;
434         mPreviewTexHeight = height;
435         if (mCamera != null) {
436             startPreview();
437         }
438     }
439 
440     @Override
onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height)441     public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
442         // Ignored, Camera does all the work for us
443     }
444 
445     @Override
onSurfaceTextureDestroyed(SurfaceTexture surface)446     public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
447         return true;
448     }
449 
450 
451     @Override
onSurfaceTextureUpdated(SurfaceTexture surface)452     public void onSurfaceTextureUpdated(SurfaceTexture surface) {
453         // Invoked every time there's a new Camera preview frame
454     }
455 
456     private AdapterView.OnItemSelectedListener mCameraSpinnerListener =
457             new AdapterView.OnItemSelectedListener() {
458                 @Override
459                 public void onItemSelected(AdapterView<?> parent,
460                         View view, int pos, long id) {
461                     if (mCurrentCameraId != pos) {
462                         setUpCamera(pos);
463                     }
464                 }
465 
466                 @Override
467                 public void onNothingSelected(AdapterView<?> parent) {
468                     // Intentionally left blank
469                 }
470 
471             };
472 
473     private AdapterView.OnItemSelectedListener mResolutionSelectedListener =
474             new AdapterView.OnItemSelectedListener() {
475                 @Override
476                 public void onItemSelected(AdapterView<?> parent,
477                         View view, int position, long id) {
478                     if (mVideoSizeIds.get(position) != mCurrentVideoSizeId) {
479                         mCurrentVideoSizeId = mVideoSizeIds.get(position);
480                         mCurrentVideoSizeName = mVideoSizeNames.get(position);
481                         if (VERBOSE) {
482                             Log.v(TAG, "onItemSelected: mCurrentVideoSizeId = " +
483                                     mCurrentVideoSizeId + " " + mCurrentVideoSizeName);
484                         }
485                         mNextPreviewSize = matchPreviewRecordSize();
486                         if (VERBOSE) {
487                             Log.v(TAG, "onItemSelected: setting preview size "
488                                     + mNextPreviewSize.width + "x" + mNextPreviewSize.height);
489                         }
490 
491                         startPreview();
492                         if (VERBOSE) {
493                             Log.v(TAG, "onItemSelected: started new preview");
494                         }
495                     }
496                 }
497 
498                 @Override
499                 public void onNothingSelected(AdapterView<?> parent) {
500                     // Intentionally left blank
501                 }
502 
503             };
504 
505 
setUpCaptureButton()506     private void setUpCaptureButton() {
507         captureButton.setOnClickListener (
508                 new View.OnClickListener() {
509                     @Override
510                     public void onClick(View V) {
511                         if ((!isRecording) && (!isPlayingBack)) {
512                             if (prepareVideoRecorder()) {
513                                 mMediaRecorder.start();
514                                 if (VERBOSE) {
515                                     Log.v(TAG, "onClick: started mMediaRecorder");
516                                 }
517                                 isRecording = true;
518                                 captureButton.setEnabled(false);
519                                 mStatusLabel.setText(getResources()
520                                         .getString(R.string.status_recording));
521                             } else {
522                                 releaseMediaRecorder();
523                                 Log.e(TAG, "media recorder cannot be set up");
524                                 failTest("Unable to set up media recorder.");
525                             }
526                             Handler h = new Handler();
527                             Runnable mDelayedPreview = new Runnable() {
528                                 @Override
529                                 public void run() {
530                                     mMediaRecorder.stop();
531                                     releaseMediaRecorder();
532 
533                                     mPlaybackView.setVideoPath(outputVideoFile.getPath());
534                                     mPlaybackView.start();
535                                     isRecording = false;
536                                     isPlayingBack = true;
537                                     mStatusLabel.setText(getResources()
538                                             .getString(R.string.status_playback));
539 
540                                     int resIdx = mResolutionSpinner.getSelectedItemPosition();
541                                     CameraCombination combination = new CameraCombination(
542                                             mCurrentCameraId, resIdx,
543                                             mVideoSizeNames.get(resIdx));
544 
545                                     mUntestedCombinations.remove(combination);
546                                     mTestedCombinations.add(combination);
547 
548                                     if (mUntestedCombinations.isEmpty() &&
549                                             mUntestedCameras.isEmpty()) {
550                                         mPassButton.setEnabled(true);
551                                         if (VERBOSE) {
552                                             Log.v(TAG, "run: test success");
553                                         }
554                                     }
555                                 }
556                             };
557                             h.postDelayed(mDelayedPreview, VIDEO_LENGTH);
558                         }
559 
560                     }
561                 }
562         );
563     }
564 
565     private class VideoSizeNamePair {
566         private int sizeId;
567         private String sizeName;
568 
VideoSizeNamePair(int id, String name)569         public VideoSizeNamePair(int id, String name) {
570             sizeId = id;
571             sizeName = name;
572         }
573 
getSizeId()574         public int getSizeId() {
575             return sizeId;
576         }
577 
getSizeName()578         public String getSizeName() {
579             return sizeName;
580         }
581     }
582 
getVideoSizeNamePairs(int cameraId)583     private ArrayList<VideoSizeNamePair> getVideoSizeNamePairs(int cameraId) {
584         int[] qualityArray = {
585                 CamcorderProfile.QUALITY_LOW,
586                 CamcorderProfile.QUALITY_HIGH,
587                 CamcorderProfile.QUALITY_QCIF,  // 176x144
588                 CamcorderProfile.QUALITY_QVGA,  // 320x240
589                 CamcorderProfile.QUALITY_CIF,   // 352x288
590                 CamcorderProfile.QUALITY_480P,  // 720x480
591                 CamcorderProfile.QUALITY_720P,  // 1280x720
592                 CamcorderProfile.QUALITY_1080P, // 1920x1080 or 1920x1088
593                 CamcorderProfile.QUALITY_2160P
594         };
595 
596         final Camera.Size skip = mCamera.new Size(-1, -1);
597         Camera.Size[] videoSizeArray = {
598                 skip,
599                 skip,
600                 mCamera.new Size(176, 144),
601                 mCamera.new Size(320, 240),
602                 mCamera.new Size(352, 288),
603                 mCamera.new Size(720, 480),
604                 mCamera.new Size(1280, 720),
605                 skip,
606                 skip
607         };
608 
609         String[] nameArray = {
610                 "LOW",
611                 "HIGH",
612                 "QCIF",
613                 "QVGA",
614                 "CIF",
615                 "480P",
616                 "720P",
617                 "1080P",
618                 "2160P"
619         };
620 
621         ArrayList<VideoSizeNamePair> availableSizes =
622                 new ArrayList<VideoSizeNamePair> ();
623 
624         Camera.Parameters p = mCamera.getParameters();
625         List<Camera.Size> supportedVideoSizes = p.getSupportedVideoSizes();
626         for (int i = 0; i < qualityArray.length; i++) {
627             if (mIsExternalCamera) {
628                 Camera.Size videoSz = videoSizeArray[i];
629                 if (videoSz.equals(skip)) {
630                     continue;
631                 }
632                 if (supportedVideoSizes.contains(videoSz)) {
633                     VideoSizeNamePair pair = new VideoSizeNamePair(qualityArray[i], nameArray[i]);
634                     availableSizes.add(pair);
635                 }
636             } else {
637                 if (CamcorderProfile.hasProfile(cameraId, qualityArray[i])) {
638                     VideoSizeNamePair pair = new VideoSizeNamePair(qualityArray[i], nameArray[i]);
639                     availableSizes.add(pair);
640                 }
641             }
642         }
643         return availableSizes;
644     }
645 
646     static class ResolutionQuality {
647         private int videoSizeId;
648         private int width;
649         private int height;
650 
ResolutionQuality()651         public ResolutionQuality() {
652             // intentionally left blank
653         }
ResolutionQuality(int newSizeId, int newWidth, int newHeight)654         public ResolutionQuality(int newSizeId, int newWidth, int newHeight) {
655             videoSizeId = newSizeId;
656             width = newWidth;
657             height = newHeight;
658         }
659     }
660 
findRecordSize(int cameraId)661     private Size findRecordSize(int cameraId) {
662         int[] possibleQuality = {
663                 CamcorderProfile.QUALITY_LOW,
664                 CamcorderProfile.QUALITY_HIGH,
665                 CamcorderProfile.QUALITY_QCIF,
666                 CamcorderProfile.QUALITY_QVGA,
667                 CamcorderProfile.QUALITY_CIF,
668                 CamcorderProfile.QUALITY_480P,
669                 CamcorderProfile.QUALITY_720P,
670                 CamcorderProfile.QUALITY_1080P,
671                 CamcorderProfile.QUALITY_2160P
672         };
673 
674         final Camera.Size skip = mCamera.new Size(-1, -1);
675         Camera.Size[] videoSizeArray = {
676                 skip,
677                 skip,
678                 mCamera.new Size(176, 144),
679                 mCamera.new Size(320, 240),
680                 mCamera.new Size(352, 288),
681                 mCamera.new Size(720, 480),
682                 mCamera.new Size(1280, 720),
683                 skip,
684                 skip
685         };
686 
687         ArrayList<ResolutionQuality> qualityList = new ArrayList<ResolutionQuality>();
688         Camera.Parameters p = mCamera.getParameters();
689         List<Camera.Size> supportedVideoSizes = p.getSupportedVideoSizes();
690         for (int i = 0; i < possibleQuality.length; i++) {
691             if (mIsExternalCamera) {
692                 Camera.Size videoSz = videoSizeArray[i];
693                 if (videoSz.equals(skip)) {
694                     continue;
695                 }
696                 if (supportedVideoSizes.contains(videoSz)) {
697                     qualityList.add(new ResolutionQuality(possibleQuality[i],
698                             videoSz.width, videoSz.height));
699                 }
700             } else {
701                 if (CamcorderProfile.hasProfile(cameraId, possibleQuality[i])) {
702                     CamcorderProfile profile = CamcorderProfile.get(cameraId, possibleQuality[i]);
703                     qualityList.add(new ResolutionQuality(possibleQuality[i],
704                             profile.videoFrameWidth, profile.videoFrameHeight));
705                 }
706             }
707         }
708 
709         Size recordSize = null;
710         for (int i = 0; i < qualityList.size(); i++) {
711             if (mCurrentVideoSizeId == qualityList.get(i).videoSizeId) {
712                 recordSize = mCamera.new Size(qualityList.get(i).width,
713                         qualityList.get(i).height);
714                 break;
715             }
716         }
717 
718         if (recordSize == null) {
719             Log.e(TAG, "findRecordSize: did not find a match");
720             failTest("Cannot find video size");
721         }
722         return recordSize;
723     }
724 
725     // Match preview size with current recording size mCurrentVideoSizeId
matchPreviewRecordSize()726     private Size matchPreviewRecordSize() {
727         Size recordSize = findRecordSize(mCurrentCameraId);
728 
729         Size matchedSize = null;
730         // First try to find exact match in size
731         for (int i = 0; i < mPreviewSizes.size(); i++) {
732             if (mPreviewSizes.get(i).equals(recordSize)) {
733                 matchedSize = mCamera.new Size(recordSize.width, recordSize.height);
734                 break;
735             }
736         }
737         // Second try to find same ratio in size
738         if (matchedSize == null) {
739             for (int i = mPreviewSizes.size() - 1; i >= 0; i--) {
740                 if (mPreviewSizes.get(i).width * recordSize.height ==
741                         mPreviewSizes.get(i).height * recordSize.width) {
742                     matchedSize = mCamera.new Size(mPreviewSizes.get(i).width,
743                             mPreviewSizes.get(i).height);
744                     break;
745                 }
746             }
747         }
748         //Third try to find one with similar if not the same apect ratio
749         if (matchedSize == null) {
750             for (int i = mPreviewSizes.size() - 1; i >= 0; i--) {
751                 if (Math.abs((float)mPreviewSizes.get(i).width * recordSize.height /
752                         mPreviewSizes.get(i).height / recordSize.width - 1) < 0.12) {
753                     matchedSize = mCamera.new Size(mPreviewSizes.get(i).width,
754                             mPreviewSizes.get(i).height);
755                     break;
756                 }
757             }
758         }
759         // Last resort, just use the first preview size
760         if (matchedSize == null) {
761             matchedSize = mCamera.new Size(mPreviewSizes.get(0).width,
762                     mPreviewSizes.get(0).height);
763         }
764 
765         if (VERBOSE) {
766             Log.v(TAG, "matchPreviewRecordSize " + matchedSize.width + "x" + matchedSize.height);
767         }
768 
769         return matchedSize;
770     }
771 
setUpCamera(int id)772     private void setUpCamera(int id) {
773         shutdownCamera();
774 
775         mCurrentCameraId = id;
776         try {
777             mCamera = Camera.open(id);
778         }
779         catch (Exception e) {
780             Log.e(TAG, "camera is not available", e);
781             failTest("camera not available" + e.getMessage());
782             return;
783         }
784         mIsExternalCamera = isExternalCamera(id);
785 
786         Camera.Parameters p = mCamera.getParameters();
787         if (VERBOSE) {
788             Log.v(TAG, "setUpCamera: setUpCamera got camera parameters");
789         }
790 
791         // Get preview resolutions
792         List<Size> unsortedSizes = p.getSupportedPreviewSizes();
793 
794         class SizeCompare implements Comparator<Size> {
795             @Override
796             public int compare(Size lhs, Size rhs) {
797                 if (lhs.width < rhs.width) return -1;
798                 if (lhs.width > rhs.width) return 1;
799                 if (lhs.height < rhs.height) return -1;
800                 if (lhs.height > rhs.height) return 1;
801                 return 0;
802             }
803         };
804 
805         SizeCompare s = new SizeCompare();
806         TreeSet<Size> sortedResolutions = new TreeSet<Size>(s);
807         sortedResolutions.addAll(unsortedSizes);
808 
809         mPreviewSizes = new ArrayList<Size>(sortedResolutions);
810 
811         ArrayList<VideoSizeNamePair> availableVideoSizes = getVideoSizeNamePairs(id);
812         String[] availableVideoSizeNames = new String[availableVideoSizes.size()];
813         mVideoSizeIds = new ArrayList<Integer>();
814         mVideoSizeNames = new ArrayList<String>();
815         for (int i = 0; i < availableVideoSizes.size(); i++) {
816             availableVideoSizeNames[i] = availableVideoSizes.get(i).getSizeName();
817             mVideoSizeIds.add(availableVideoSizes.get(i).getSizeId());
818             mVideoSizeNames.add(availableVideoSizeNames[i]);
819         }
820 
821         mResolutionSpinner.setAdapter(
822             new ArrayAdapter<String>(
823                 this, R.layout.cf_format_list_item, availableVideoSizeNames));
824 
825         // Update untested
826         mUntestedCameras.remove("All combinations for Camera " + id + "\n");
827 
828         for (int videoSizeIdIndex = 0;
829                 videoSizeIdIndex < mVideoSizeIds.size(); videoSizeIdIndex++) {
830             CameraCombination combination = new CameraCombination(
831                 id, videoSizeIdIndex, mVideoSizeNames.get(videoSizeIdIndex));
832 
833             if (!mTestedCombinations.contains(combination)) {
834                 mUntestedCombinations.add(combination);
835             }
836         }
837 
838         // Set initial values
839         mCurrentVideoSizeId = mVideoSizeIds.get(0);
840         mCurrentVideoSizeName = mVideoSizeNames.get(0);
841         mNextPreviewSize = matchPreviewRecordSize();
842         mResolutionSpinner.setSelection(0);
843 
844         // Set up correct display orientation
845         CameraInfo info = new CameraInfo();
846         Camera.getCameraInfo(id, info);
847         int rotation = getWindowManager().getDefaultDisplay().getRotation();
848         int degrees = 0;
849         switch (rotation) {
850             case Surface.ROTATION_0: degrees = 0; break;
851             case Surface.ROTATION_90: degrees = 90; break;
852             case Surface.ROTATION_180: degrees = 180; break;
853             case Surface.ROTATION_270: degrees = 270; break;
854         }
855 
856         if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
857             mVideoRotation = (info.orientation + degrees) % 360;
858             mPreviewRotation = (360 - mVideoRotation) % 360;  // compensate the mirror
859         } else {  // back-facing
860             mVideoRotation = (info.orientation - degrees + 360) % 360;
861             mPreviewRotation = mVideoRotation;
862         }
863         if (mPreviewRotation != 0 && mPreviewRotation != 180) {
864             Log.w(TAG,
865                 "Display orientation correction is not 0 or 180, as expected!");
866         }
867 
868         mCamera.setDisplayOrientation(mPreviewRotation);
869 
870         // Start up preview if display is ready
871         if (mPreviewTexture != null) {
872             startPreview();
873         }
874     }
875 
shutdownCamera()876     private void shutdownCamera() {
877         if (mCamera != null) {
878             mCamera.setPreviewCallback(null);
879             mCamera.stopPreview();
880             mCamera.release();
881             mCamera = null;
882         }
883     }
884 
885     /**
886      * starts capturing and drawing frames on screen
887      */
startPreview()888     private void startPreview() {
889 
890         mCamera.stopPreview();
891 
892         Matrix transform = new Matrix();
893         float widthRatio = mNextPreviewSize.width / (float)mPreviewTexWidth;
894         float heightRatio = mNextPreviewSize.height / (float)mPreviewTexHeight;
895         if (VERBOSE) {
896             Log.v(TAG, "startPreview: widthRatio=" + widthRatio + " " + "heightRatio=" +
897                     heightRatio);
898         }
899 
900         if (heightRatio < widthRatio) {
901             transform.setScale(1, heightRatio / widthRatio);
902             transform.postTranslate(0,
903                     mPreviewTexHeight * (1 - heightRatio / widthRatio) / 2);
904             if (VERBOSE) {
905                 Log.v(TAG, "startPreview: shrink vertical by " + heightRatio / widthRatio);
906             }
907         } else {
908             transform.setScale(widthRatio / heightRatio, 1);
909             transform.postTranslate(mPreviewTexWidth * (1 - widthRatio / heightRatio) / 2, 0);
910             if (VERBOSE) {
911                 Log.v(TAG, "startPreview: shrink horizontal by " + widthRatio / heightRatio);
912             }
913         }
914 
915         mPreviewView.setTransform(transform);
916 
917         mPreviewSize = mNextPreviewSize;
918 
919         Camera.Parameters p = mCamera.getParameters();
920         p.setPreviewSize(mPreviewSize.width, mPreviewSize.height);
921         mCamera.setParameters(p);
922 
923         try {
924             mCamera.setPreviewTexture(mPreviewTexture);
925             if (mPreviewTexture == null) {
926                 Log.e(TAG, "preview texture is null.");
927             }
928             if (VERBOSE) {
929                 Log.v(TAG, "startPreview: set preview texture in startPreview");
930             }
931             mCamera.startPreview();
932             if (VERBOSE) {
933                 Log.v(TAG, "startPreview: started preview in startPreview");
934             }
935         } catch (IOException ioe) {
936             Log.e(TAG, "Unable to start up preview", ioe);
937             // Show a dialog box to tell user test failed
938             failTest("Unable to start preview.");
939         }
940     }
941 
failTest(String failMessage)942     private void failTest(String failMessage) {
943         DialogInterface.OnClickListener dialogClickListener =
944                 new DialogInterface.OnClickListener() {
945                     @Override
946                     public void onClick(DialogInterface dialog, int which) {
947                         switch (which) {
948                             case DialogInterface.BUTTON_POSITIVE:
949                                 setTestResultAndFinish(/* passed */false);
950                                 break;
951                             case DialogInterface.BUTTON_NEGATIVE:
952                                 break;
953                         }
954                     }
955                 };
956 
957         AlertDialog.Builder builder = new AlertDialog.Builder(CameraVideoActivity.this);
958         builder.setMessage(getString(R.string.dialog_fail_test) + ". " + failMessage)
959                 .setPositiveButton(R.string.fail_quit, dialogClickListener)
960                 .setNegativeButton(R.string.cancel, dialogClickListener)
961                 .show();
962     }
963 
isExternalCamera(int cameraId)964     private boolean isExternalCamera(int cameraId) {
965         CameraManager manager = (CameraManager) this.getSystemService(Context.CAMERA_SERVICE);
966         try {
967             String cameraIdStr = manager.getCameraIdList()[cameraId];
968             CameraCharacteristics characteristics =
969                     manager.getCameraCharacteristics(cameraIdStr);
970 
971             if (characteristics.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL) ==
972                             CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_EXTERNAL) {
973                 // External camera doesn't support FOV informations
974                 return true;
975             }
976         } catch (CameraAccessException e) {
977             Toast.makeText(this, "Could not access camera " + cameraId +
978                     ": " + e.getMessage(), Toast.LENGTH_LONG).show();
979         }
980         return false;
981     }
982 
983 }
984