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