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