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