• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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                                         mPassButton.setEnabled(true);
550                                         if (VERBOSE) {
551                                             Log.v(TAG, "run: test success");
552                                         }
553                                     }
554                                 }
555                             };
556                             h.postDelayed(mDelayedPreview, VIDEO_LENGTH);
557                         }
558 
559                     }
560                 }
561         );
562     }
563 
564     private class VideoSizeNamePair {
565         private int sizeId;
566         private String sizeName;
567 
VideoSizeNamePair(int id, String name)568         public VideoSizeNamePair(int id, String name) {
569             sizeId = id;
570             sizeName = name;
571         }
572 
getSizeId()573         public int getSizeId() {
574             return sizeId;
575         }
576 
getSizeName()577         public String getSizeName() {
578             return sizeName;
579         }
580     }
581 
getVideoSizeNamePairs(int cameraId)582     private ArrayList<VideoSizeNamePair> getVideoSizeNamePairs(int cameraId) {
583         int[] qualityArray = {
584                 CamcorderProfile.QUALITY_LOW,
585                 CamcorderProfile.QUALITY_HIGH,
586                 CamcorderProfile.QUALITY_QCIF,  // 176x144
587                 CamcorderProfile.QUALITY_QVGA,  // 320x240
588                 CamcorderProfile.QUALITY_CIF,   // 352x288
589                 CamcorderProfile.QUALITY_480P,  // 720x480
590                 CamcorderProfile.QUALITY_720P,  // 1280x720
591                 CamcorderProfile.QUALITY_1080P, // 1920x1080 or 1920x1088
592                 CamcorderProfile.QUALITY_2160P
593         };
594 
595         final Camera.Size skip = mCamera.new Size(-1, -1);
596         Camera.Size[] videoSizeArray = {
597                 skip,
598                 skip,
599                 mCamera.new Size(176, 144),
600                 mCamera.new Size(320, 240),
601                 mCamera.new Size(352, 288),
602                 mCamera.new Size(720, 480),
603                 mCamera.new Size(1280, 720),
604                 skip,
605                 skip
606         };
607 
608         String[] nameArray = {
609                 "LOW",
610                 "HIGH",
611                 "QCIF",
612                 "QVGA",
613                 "CIF",
614                 "480P",
615                 "720P",
616                 "1080P",
617                 "2160P"
618         };
619 
620         ArrayList<VideoSizeNamePair> availableSizes =
621                 new ArrayList<VideoSizeNamePair> ();
622 
623         Camera.Parameters p = mCamera.getParameters();
624         List<Camera.Size> supportedVideoSizes = p.getSupportedVideoSizes();
625         for (int i = 0; i < qualityArray.length; i++) {
626             if (mIsExternalCamera) {
627                 Camera.Size videoSz = videoSizeArray[i];
628                 if (videoSz.equals(skip)) {
629                     continue;
630                 }
631                 if (supportedVideoSizes.contains(videoSz)) {
632                     VideoSizeNamePair pair = new VideoSizeNamePair(qualityArray[i], nameArray[i]);
633                     availableSizes.add(pair);
634                 }
635             } else {
636                 if (CamcorderProfile.hasProfile(cameraId, qualityArray[i])) {
637                     VideoSizeNamePair pair = new VideoSizeNamePair(qualityArray[i], nameArray[i]);
638                     availableSizes.add(pair);
639                 }
640             }
641         }
642         return availableSizes;
643     }
644 
645     static class ResolutionQuality {
646         private int videoSizeId;
647         private int width;
648         private int height;
649 
ResolutionQuality()650         public ResolutionQuality() {
651             // intentionally left blank
652         }
ResolutionQuality(int newSizeId, int newWidth, int newHeight)653         public ResolutionQuality(int newSizeId, int newWidth, int newHeight) {
654             videoSizeId = newSizeId;
655             width = newWidth;
656             height = newHeight;
657         }
658     }
659 
findRecordSize(int cameraId)660     private Size findRecordSize(int cameraId) {
661         int[] possibleQuality = {
662                 CamcorderProfile.QUALITY_LOW,
663                 CamcorderProfile.QUALITY_HIGH,
664                 CamcorderProfile.QUALITY_QCIF,
665                 CamcorderProfile.QUALITY_QVGA,
666                 CamcorderProfile.QUALITY_CIF,
667                 CamcorderProfile.QUALITY_480P,
668                 CamcorderProfile.QUALITY_720P,
669                 CamcorderProfile.QUALITY_1080P,
670                 CamcorderProfile.QUALITY_2160P
671         };
672 
673         final Camera.Size skip = mCamera.new Size(-1, -1);
674         Camera.Size[] videoSizeArray = {
675                 skip,
676                 skip,
677                 mCamera.new Size(176, 144),
678                 mCamera.new Size(320, 240),
679                 mCamera.new Size(352, 288),
680                 mCamera.new Size(720, 480),
681                 mCamera.new Size(1280, 720),
682                 skip,
683                 skip
684         };
685 
686         ArrayList<ResolutionQuality> qualityList = new ArrayList<ResolutionQuality>();
687         Camera.Parameters p = mCamera.getParameters();
688         List<Camera.Size> supportedVideoSizes = p.getSupportedVideoSizes();
689         for (int i = 0; i < possibleQuality.length; i++) {
690             if (mIsExternalCamera) {
691                 Camera.Size videoSz = videoSizeArray[i];
692                 if (videoSz.equals(skip)) {
693                     continue;
694                 }
695                 if (supportedVideoSizes.contains(videoSz)) {
696                     qualityList.add(new ResolutionQuality(possibleQuality[i],
697                             videoSz.width, videoSz.height));
698                 }
699             } else {
700                 if (CamcorderProfile.hasProfile(cameraId, possibleQuality[i])) {
701                     CamcorderProfile profile = CamcorderProfile.get(cameraId, possibleQuality[i]);
702                     qualityList.add(new ResolutionQuality(possibleQuality[i],
703                             profile.videoFrameWidth, profile.videoFrameHeight));
704                 }
705             }
706         }
707 
708         Size recordSize = null;
709         for (int i = 0; i < qualityList.size(); i++) {
710             if (mCurrentVideoSizeId == qualityList.get(i).videoSizeId) {
711                 recordSize = mCamera.new Size(qualityList.get(i).width,
712                         qualityList.get(i).height);
713                 break;
714             }
715         }
716 
717         if (recordSize == null) {
718             Log.e(TAG, "findRecordSize: did not find a match");
719             failTest("Cannot find video size");
720         }
721         return recordSize;
722     }
723 
724     // Match preview size with current recording size mCurrentVideoSizeId
matchPreviewRecordSize()725     private Size matchPreviewRecordSize() {
726         Size recordSize = findRecordSize(mCurrentCameraId);
727 
728         Size matchedSize = null;
729         // First try to find exact match in size
730         for (int i = 0; i < mPreviewSizes.size(); i++) {
731             if (mPreviewSizes.get(i).equals(recordSize)) {
732                 matchedSize = mCamera.new Size(recordSize.width, recordSize.height);
733                 break;
734             }
735         }
736         // Second try to find same ratio in size
737         if (matchedSize == null) {
738             for (int i = mPreviewSizes.size() - 1; i >= 0; i--) {
739                 if (mPreviewSizes.get(i).width * recordSize.height ==
740                         mPreviewSizes.get(i).height * recordSize.width) {
741                     matchedSize = mCamera.new Size(mPreviewSizes.get(i).width,
742                             mPreviewSizes.get(i).height);
743                     break;
744                 }
745             }
746         }
747         //Third try to find one with similar if not the same apect ratio
748         if (matchedSize == null) {
749             for (int i = mPreviewSizes.size() - 1; i >= 0; i--) {
750                 if (Math.abs((float)mPreviewSizes.get(i).width * recordSize.height /
751                         mPreviewSizes.get(i).height / recordSize.width - 1) < 0.12) {
752                     matchedSize = mCamera.new Size(mPreviewSizes.get(i).width,
753                             mPreviewSizes.get(i).height);
754                     break;
755                 }
756             }
757         }
758         // Last resort, just use the first preview size
759         if (matchedSize == null) {
760             matchedSize = mCamera.new Size(mPreviewSizes.get(0).width,
761                     mPreviewSizes.get(0).height);
762         }
763 
764         if (VERBOSE) {
765             Log.v(TAG, "matchPreviewRecordSize " + matchedSize.width + "x" + matchedSize.height);
766         }
767 
768         return matchedSize;
769     }
770 
setUpCamera(int id)771     private void setUpCamera(int id) {
772         shutdownCamera();
773 
774         mCurrentCameraId = id;
775         try {
776             mCamera = Camera.open(id);
777         }
778         catch (Exception e) {
779             Log.e(TAG, "camera is not available", e);
780             failTest("camera not available" + e.getMessage());
781             return;
782         }
783         mIsExternalCamera = isExternalCamera(id);
784 
785         Camera.Parameters p = mCamera.getParameters();
786         if (VERBOSE) {
787             Log.v(TAG, "setUpCamera: setUpCamera got camera parameters");
788         }
789 
790         // Get preview resolutions
791         List<Size> unsortedSizes = p.getSupportedPreviewSizes();
792 
793         class SizeCompare implements Comparator<Size> {
794             @Override
795             public int compare(Size lhs, Size rhs) {
796                 if (lhs.width < rhs.width) return -1;
797                 if (lhs.width > rhs.width) return 1;
798                 if (lhs.height < rhs.height) return -1;
799                 if (lhs.height > rhs.height) return 1;
800                 return 0;
801             }
802         };
803 
804         SizeCompare s = new SizeCompare();
805         TreeSet<Size> sortedResolutions = new TreeSet<Size>(s);
806         sortedResolutions.addAll(unsortedSizes);
807 
808         mPreviewSizes = new ArrayList<Size>(sortedResolutions);
809 
810         ArrayList<VideoSizeNamePair> availableVideoSizes = getVideoSizeNamePairs(id);
811         String[] availableVideoSizeNames = new String[availableVideoSizes.size()];
812         mVideoSizeIds = new ArrayList<Integer>();
813         mVideoSizeNames = new ArrayList<String>();
814         for (int i = 0; i < availableVideoSizes.size(); i++) {
815             availableVideoSizeNames[i] = availableVideoSizes.get(i).getSizeName();
816             mVideoSizeIds.add(availableVideoSizes.get(i).getSizeId());
817             mVideoSizeNames.add(availableVideoSizeNames[i]);
818         }
819 
820         mResolutionSpinner.setAdapter(
821             new ArrayAdapter<String>(
822                 this, R.layout.cf_format_list_item, availableVideoSizeNames));
823 
824         // Update untested
825         mUntestedCameras.remove("All combinations for Camera " + id + "\n");
826 
827         for (int videoSizeIdIndex = 0;
828                 videoSizeIdIndex < mVideoSizeIds.size(); videoSizeIdIndex++) {
829             CameraCombination combination = new CameraCombination(
830                 id, videoSizeIdIndex, mVideoSizeNames.get(videoSizeIdIndex));
831 
832             if (!mTestedCombinations.contains(combination)) {
833                 mUntestedCombinations.add(combination);
834             }
835         }
836 
837         // Set initial values
838         mCurrentVideoSizeId = mVideoSizeIds.get(0);
839         mCurrentVideoSizeName = mVideoSizeNames.get(0);
840         mNextPreviewSize = matchPreviewRecordSize();
841         mResolutionSpinner.setSelection(0);
842 
843         // Set up correct display orientation
844         CameraInfo info = new CameraInfo();
845         Camera.getCameraInfo(id, info);
846         int rotation = getWindowManager().getDefaultDisplay().getRotation();
847         int degrees = 0;
848         switch (rotation) {
849             case Surface.ROTATION_0: degrees = 0; break;
850             case Surface.ROTATION_90: degrees = 90; break;
851             case Surface.ROTATION_180: degrees = 180; break;
852             case Surface.ROTATION_270: degrees = 270; break;
853         }
854 
855         if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
856             mVideoRotation = (info.orientation + degrees) % 360;
857             mPreviewRotation = (360 - mVideoRotation) % 360;  // compensate the mirror
858         } else {  // back-facing
859             mVideoRotation = (info.orientation - degrees + 360) % 360;
860             mPreviewRotation = mVideoRotation;
861         }
862         if (mPreviewRotation != 0 && mPreviewRotation != 180) {
863             Log.w(TAG,
864                 "Display orientation correction is not 0 or 180, as expected!");
865         }
866 
867         mCamera.setDisplayOrientation(mPreviewRotation);
868 
869         // Start up preview if display is ready
870         if (mPreviewTexture != null) {
871             startPreview();
872         }
873     }
874 
shutdownCamera()875     private void shutdownCamera() {
876         if (mCamera != null) {
877             mCamera.setPreviewCallback(null);
878             mCamera.stopPreview();
879             mCamera.release();
880             mCamera = null;
881         }
882     }
883 
884     /**
885      * starts capturing and drawing frames on screen
886      */
startPreview()887     private void startPreview() {
888 
889         mCamera.stopPreview();
890 
891         Matrix transform = new Matrix();
892         float widthRatio = mNextPreviewSize.width / (float)mPreviewTexWidth;
893         float heightRatio = mNextPreviewSize.height / (float)mPreviewTexHeight;
894         if (VERBOSE) {
895             Log.v(TAG, "startPreview: widthRatio=" + widthRatio + " " + "heightRatio=" +
896                     heightRatio);
897         }
898 
899         if (heightRatio < widthRatio) {
900             transform.setScale(1, heightRatio / widthRatio);
901             transform.postTranslate(0,
902                     mPreviewTexHeight * (1 - heightRatio / widthRatio) / 2);
903             if (VERBOSE) {
904                 Log.v(TAG, "startPreview: shrink vertical by " + heightRatio / widthRatio);
905             }
906         } else {
907             transform.setScale(widthRatio / heightRatio, 1);
908             transform.postTranslate(mPreviewTexWidth * (1 - widthRatio / heightRatio) / 2, 0);
909             if (VERBOSE) {
910                 Log.v(TAG, "startPreview: shrink horizontal by " + widthRatio / heightRatio);
911             }
912         }
913 
914         mPreviewView.setTransform(transform);
915 
916         mPreviewSize = mNextPreviewSize;
917 
918         Camera.Parameters p = mCamera.getParameters();
919         p.setPreviewSize(mPreviewSize.width, mPreviewSize.height);
920         mCamera.setParameters(p);
921 
922         try {
923             mCamera.setPreviewTexture(mPreviewTexture);
924             if (mPreviewTexture == null) {
925                 Log.e(TAG, "preview texture is null.");
926             }
927             if (VERBOSE) {
928                 Log.v(TAG, "startPreview: set preview texture in startPreview");
929             }
930             mCamera.startPreview();
931             if (VERBOSE) {
932                 Log.v(TAG, "startPreview: started preview in startPreview");
933             }
934         } catch (IOException ioe) {
935             Log.e(TAG, "Unable to start up preview", ioe);
936             // Show a dialog box to tell user test failed
937             failTest("Unable to start preview.");
938         }
939     }
940 
failTest(String failMessage)941     private void failTest(String failMessage) {
942         DialogInterface.OnClickListener dialogClickListener =
943                 new DialogInterface.OnClickListener() {
944                     @Override
945                     public void onClick(DialogInterface dialog, int which) {
946                         switch (which) {
947                             case DialogInterface.BUTTON_POSITIVE:
948                                 setTestResultAndFinish(/* passed */false);
949                                 break;
950                             case DialogInterface.BUTTON_NEGATIVE:
951                                 break;
952                         }
953                     }
954                 };
955 
956         AlertDialog.Builder builder = new AlertDialog.Builder(CameraVideoActivity.this);
957         builder.setMessage(getString(R.string.dialog_fail_test) + ". " + failMessage)
958                 .setPositiveButton(R.string.fail_quit, dialogClickListener)
959                 .setNegativeButton(R.string.cancel, dialogClickListener)
960                 .show();
961     }
962 
isExternalCamera(int cameraId)963     private boolean isExternalCamera(int cameraId) {
964         CameraManager manager = (CameraManager) this.getSystemService(Context.CAMERA_SERVICE);
965         try {
966             String cameraIdStr = manager.getCameraIdList()[cameraId];
967             CameraCharacteristics characteristics =
968                     manager.getCameraCharacteristics(cameraIdStr);
969 
970             if (characteristics.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL) ==
971                             CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_EXTERNAL) {
972                 // External camera doesn't support FOV informations
973                 return true;
974             }
975         } catch (CameraAccessException e) {
976             Toast.makeText(this, "Could not access camera " + cameraId +
977                     ": " + e.getMessage(), Toast.LENGTH_LONG).show();
978         }
979         return false;
980     }
981 
982 }
983