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