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 
17 package com.android.cts.verifier.camera.fov;
18 
19 import android.app.Activity;
20 import android.app.AlertDialog;
21 import android.app.Dialog;
22 import android.content.Context;
23 import android.content.DialogInterface;
24 import android.content.Intent;
25 import android.graphics.Color;
26 import android.hardware.Camera;
27 import android.hardware.Camera.PictureCallback;
28 import android.hardware.Camera.ShutterCallback;
29 import android.hardware.camera2.CameraAccessException;
30 import android.hardware.camera2.CameraCharacteristics;
31 import android.hardware.camera2.CameraManager;
32 import android.os.Bundle;
33 import android.os.PowerManager;
34 import android.os.PowerManager.WakeLock;
35 import android.util.Log;
36 import android.view.Surface;
37 import android.view.SurfaceHolder;
38 import android.view.SurfaceView;
39 import android.view.View;
40 import android.view.View.OnClickListener;
41 import android.widget.AdapterView;
42 import android.widget.AdapterView.OnItemSelectedListener;
43 import android.widget.ArrayAdapter;
44 import android.widget.Button;
45 import android.widget.Spinner;
46 import android.widget.TextView;
47 import android.widget.Toast;
48 
49 import com.android.cts.verifier.R;
50 import com.android.cts.verifier.TestResult;
51 
52 import java.io.File;
53 import java.io.FileOutputStream;
54 import java.io.IOException;
55 import java.util.ArrayList;
56 import java.util.List;
57 
58 /**
59  * An activity for showing the camera preview and taking a picture.
60  */
61 public class PhotoCaptureActivity extends Activity
62         implements PictureCallback, SurfaceHolder.Callback {
63     private static final String TAG = PhotoCaptureActivity.class.getSimpleName();
64     private static final int FOV_REQUEST_CODE = 1006;
65     private static final String PICTURE_FILENAME = "photo.jpg";
66     private static float mReportedFovDegrees = 0;
67     private float mReportedFovPrePictureTaken = -1;
68 
69     private SurfaceView mPreview;
70     private SurfaceHolder mSurfaceHolder;
71     private Spinner mResolutionSpinner;
72     private List<SelectableResolution> mSupportedResolutions;
73     private ArrayAdapter<SelectableResolution> mAdapter;
74 
75     private SelectableResolution mSelectedResolution;
76     private Camera mCamera;
77     private Size mSurfaceSize;
78     private boolean mCameraInitialized = false;
79     private boolean mPreviewActive = false;
80     private boolean mTakingPicture = false;
81     private int mResolutionSpinnerIndex = -1;
82     private WakeLock mWakeLock;
83     private long shutterStartTime;
84     private int mPreviewOrientation;
85     private int mJpegOrientation;
86 
87     private ArrayList<Integer> mPreviewSizeCamerasToProcess = new ArrayList<Integer>();
88 
89     private Dialog mActiveDialog;
90 
91     /**
92      * Selected preview size per camera. If null, preview size should be
93      * automatically detected.
94      */
95     private Size[] mPreviewSizes = null;
96 
getPictureFile(Context context)97     public static File getPictureFile(Context context) {
98         return new File(context.getExternalCacheDir(), PICTURE_FILENAME);
99     }
100 
getReportedFovDegrees()101     public static float getReportedFovDegrees() {
102         return mReportedFovDegrees;
103     }
104 
105     @Override
onCreate(Bundle savedInstanceState)106     protected void onCreate(Bundle savedInstanceState) {
107         super.onCreate(savedInstanceState);
108         setContentView(R.layout.camera_fov_calibration_photo_capture);
109 
110         int cameraToBeTested = 0;
111         for (int cameraId = 0; cameraId < Camera.getNumberOfCameras(); ++cameraId) {
112             if (!isExternalCamera(cameraId)) {
113                 cameraToBeTested++;
114             }
115         }
116 
117         mPreview = (SurfaceView) findViewById(R.id.camera_fov_camera_preview);
118         mSurfaceHolder = mPreview.getHolder();
119         mSurfaceHolder.addCallback(this);
120 
121         // This is required for older versions of Android hardware.
122         mSurfaceHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
123 
124         TextView textView = (TextView) findViewById(R.id.camera_fov_tap_to_take_photo);
125         textView.setTextColor(Color.WHITE);
126 
127         Button setupButton = (Button) findViewById(R.id.camera_fov_settings_button);
128         setupButton.setOnClickListener(new OnClickListener() {
129 
130             @Override
131             public void onClick(View v) {
132                 startActivity(new Intent(
133                         PhotoCaptureActivity.this, CalibrationPreferenceActivity.class));
134             }
135         });
136 
137         Button changePreviewSizeButton = (Button) findViewById(
138                 R.id.camera_fov_change_preview_size_button);
139         changePreviewSizeButton.setOnClickListener(new OnClickListener() {
140             @Override
141             public void onClick(View v) {
142                 // Stop camera until preview sizes have been obtained.
143                 if (mCamera != null) {
144                     mCamera.stopPreview();
145                     mCamera.release();
146                     mCamera = null;
147                 }
148 
149                 mPreviewSizeCamerasToProcess.clear();
150                 mPreviewSizes =  new Size[Camera.getNumberOfCameras()];
151                 for (int cameraId = 0; cameraId < Camera.getNumberOfCameras(); ++cameraId) {
152                     if (!isExternalCamera(cameraId)) {
153                         mPreviewSizeCamerasToProcess.add(cameraId);
154                     }
155                 }
156                 showNextDialogToChoosePreviewSize();
157             }
158         });
159 
160         View previewView = findViewById(R.id.camera_fov_preview_overlay);
161         previewView.setOnClickListener(new OnClickListener() {
162             @Override
163             public void onClick(View v) {
164                 if (mPreviewActive && !mTakingPicture) {
165                     mTakingPicture = true;
166                     shutterStartTime = System.currentTimeMillis();
167 
168                     mCamera.takePicture(new ShutterCallback() {
169                         @Override
170                         public void onShutter() {
171                             long dT = System.currentTimeMillis() - shutterStartTime;
172                             Log.d("CTS", "Shutter Lag: " + dT);
173                         }
174                     }, null, PhotoCaptureActivity.this);
175                 }
176             }
177         });
178 
179         mResolutionSpinner = (Spinner) findViewById(R.id.camera_fov_resolution_selector);
180         mResolutionSpinner.setOnItemSelectedListener(new OnItemSelectedListener() {
181             @Override
182             public void onItemSelected(
183                     AdapterView<?> parent, View view, int position, long id) {
184                 if (mSupportedResolutions != null) {
185                     SelectableResolution resolution = mSupportedResolutions.get(position);
186                     switchToCamera(resolution, false);
187 
188                     // It should be guaranteed that the FOV is correctly updated after setParameters().
189                     mReportedFovPrePictureTaken = mCamera.getParameters().getHorizontalViewAngle();
190 
191                     mResolutionSpinnerIndex = position;
192                     startPreview();
193                 }
194             }
195 
196             @Override
197             public void onNothingSelected(AdapterView<?> arg0) {}
198         });
199 
200         if (cameraToBeTested == 0) {
201             Log.i(TAG, "No cameras needs to be tested. Setting test pass.");
202             Toast.makeText(this, "No cameras needs to be tested. Test pass.",
203                     Toast.LENGTH_LONG).show();
204 
205             TestResult.setPassedResult(this, getClass().getName(),
206                     "All cameras are external, test skipped!");
207             finish();
208         }
209     }
210 
211     @Override
onResume()212     protected void onResume() {
213         super.onResume();
214         // Keep the device from going to sleep.
215         PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
216         mWakeLock = pm.newWakeLock(PowerManager.FULL_WAKE_LOCK, TAG);
217         mWakeLock.acquire();
218 
219         if (mSupportedResolutions == null) {
220             mSupportedResolutions = new ArrayList<SelectableResolution>();
221             int numCameras = Camera.getNumberOfCameras();
222             for (int cameraId = 0; cameraId < numCameras; ++cameraId) {
223                 if (isExternalCamera(cameraId)) {
224                     continue;
225                 }
226 
227                 Camera camera = Camera.open(cameraId);
228 
229                 // Get the supported picture sizes and fill the spinner.
230                 List<Camera.Size> supportedSizes =
231                         camera.getParameters().getSupportedPictureSizes();
232                 for (Camera.Size size : supportedSizes) {
233                     mSupportedResolutions.add(
234                             new SelectableResolution(cameraId, size.width, size.height));
235                 }
236                 camera.release();
237             }
238         }
239 
240         // Find the first untested entry.
241         for (mResolutionSpinnerIndex = 0;
242                 mResolutionSpinnerIndex < mSupportedResolutions.size();
243                 mResolutionSpinnerIndex++) {
244             if (!mSupportedResolutions.get(mResolutionSpinnerIndex).tested) {
245                 break;
246             }
247         }
248 
249         mAdapter = new ArrayAdapter<SelectableResolution>(
250                 this, android.R.layout.simple_spinner_dropdown_item,
251                 mSupportedResolutions);
252         mResolutionSpinner.setAdapter(mAdapter);
253 
254         mResolutionSpinner.setSelection(mResolutionSpinnerIndex);
255         setResult(RESULT_CANCELED);
256     }
257 
258     @Override
onPause()259     public void onPause() {
260         if (mCamera != null) {
261             if (mPreviewActive) {
262                 mCamera.stopPreview();
263             }
264 
265             mCamera.release();
266             mCamera = null;
267         }
268         mPreviewActive = false;
269         mWakeLock.release();
270         super.onPause();
271     }
272 
273     @Override
onPictureTaken(byte[] data, Camera camera)274     public void onPictureTaken(byte[] data, Camera camera) {
275         File pictureFile = getPictureFile(this);
276         Camera.Parameters params = mCamera.getParameters();
277         mReportedFovDegrees = params.getHorizontalViewAngle();
278 
279         // Show error if FOV does not match the value reported before takePicture().
280         if (mReportedFovPrePictureTaken != mReportedFovDegrees) {
281             mSupportedResolutions.get(mResolutionSpinnerIndex).tested = true;
282             mSupportedResolutions.get(mResolutionSpinnerIndex).passed = false;
283 
284             AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this);
285             dialogBuilder.setTitle(R.string.camera_fov_reported_fov_problem);
286             dialogBuilder.setNeutralButton(
287                     android.R.string.ok, new DialogInterface.OnClickListener() {
288                 @Override
289                 public void onClick(DialogInterface dialog, int which) {
290                     if (mActiveDialog != null) {
291                         mActiveDialog.dismiss();
292                         mActiveDialog = null;
293                         initializeCamera();
294                     }
295                 }
296             });
297 
298             String message  = getResources().getString(R.string.camera_fov_reported_fov_problem_message);
299             dialogBuilder.setMessage(String.format(message, mReportedFovPrePictureTaken, mReportedFovDegrees));
300             mActiveDialog = dialogBuilder.show();
301             mTakingPicture = false;
302             return;
303         }
304 
305         try {
306             FileOutputStream fos = new FileOutputStream(pictureFile);
307             fos.write(data);
308             fos.close();
309             Log.d(TAG, "File saved to " + pictureFile.getAbsolutePath());
310 
311             // Start activity which will use the taken picture to determine the
312             // FOV.
313             startActivityForResult(new Intent(this, DetermineFovActivity.class),
314                     FOV_REQUEST_CODE + mResolutionSpinnerIndex, null);
315         } catch (IOException e) {
316             Log.e(TAG, "Could not save picture file.", e);
317             Toast.makeText(this, "Could not save picture file: " + e.getMessage(),
318                     Toast.LENGTH_LONG).show();
319         }
320         mTakingPicture = false;
321     }
322 
323     @Override
onActivityResult(int requestCode, int resultCode, Intent data)324     protected void onActivityResult(int requestCode, int resultCode, Intent data) {
325         if (resultCode != RESULT_OK) {
326             return;
327         }
328         int testIndex = requestCode - FOV_REQUEST_CODE;
329         SelectableResolution res = mSupportedResolutions.get(testIndex);
330         res.tested = true;
331         float reportedFOV = CtsTestHelper.getReportedFOV(data);
332         float measuredFOV = CtsTestHelper.getMeasuredFOV(data);
333         res.measuredFOV = measuredFOV;
334         if (CtsTestHelper.isResultPassed(reportedFOV, measuredFOV)) {
335             res.passed = true;
336         }
337 
338         boolean allTested = true;
339         for (int i = 0; i < mSupportedResolutions.size(); i++) {
340             if (!mSupportedResolutions.get(i).tested) {
341                 allTested = false;
342                 break;
343             }
344         }
345         if (!allTested) {
346             mAdapter.notifyDataSetChanged();
347             return;
348         }
349 
350         boolean allPassed = true;
351         for (int i = 0; i < mSupportedResolutions.size(); i++) {
352             if (!mSupportedResolutions.get(i).passed) {
353                 allPassed = false;
354                 break;
355             }
356         }
357         if (allPassed) {
358             TestResult.setPassedResult(this, getClass().getName(),
359                     CtsTestHelper.getTestDetails(mSupportedResolutions));
360         } else {
361             TestResult.setFailedResult(this, getClass().getName(),
362                     CtsTestHelper.getTestDetails(mSupportedResolutions));
363         }
364         finish();
365     }
366 
367     @Override
surfaceChanged( SurfaceHolder holder, int format, int width, int height)368     public void surfaceChanged(
369             SurfaceHolder holder, int format, int width, int height) {
370         mSurfaceSize = new Size(width, height);
371         initializeCamera();
372     }
373 
374     @Override
surfaceCreated(SurfaceHolder holder)375     public void surfaceCreated(SurfaceHolder holder) {
376         // Nothing to do.
377     }
378 
379     @Override
surfaceDestroyed(SurfaceHolder holder)380     public void surfaceDestroyed(SurfaceHolder holder) {
381         // Nothing to do.
382     }
383 
showNextDialogToChoosePreviewSize()384     private void showNextDialogToChoosePreviewSize() {
385         final int cameraId = mPreviewSizeCamerasToProcess.remove(0);
386 
387         Camera camera = Camera.open(cameraId);
388         final List<Camera.Size> sizes = camera.getParameters()
389                 .getSupportedPreviewSizes();
390         String[] choices = new String[sizes.size()];
391         for (int i = 0; i < sizes.size(); ++i) {
392             Camera.Size size = sizes.get(i);
393             choices[i] = size.width + " x " + size.height;
394         }
395 
396         final AlertDialog.Builder builder = new AlertDialog.Builder(this);
397         String dialogTitle = String.format(
398                 getResources().getString(R.string.camera_fov_choose_preview_size_for_camera),
399                 cameraId);
400         builder.setTitle(
401                 dialogTitle).
402                 setOnCancelListener(new DialogInterface.OnCancelListener() {
403                     @Override
404                     public void onCancel(DialogInterface arg0) {
405                         // User cancelled preview size selection.
406                         mPreviewSizes = null;
407                         switchToCamera(mSelectedResolution, true);
408                     }
409                 }).
410                 setSingleChoiceItems(choices, 0, new DialogInterface.OnClickListener() {
411                     @Override
412                     public void onClick(DialogInterface dialog, int which) {
413                         Camera.Size size = sizes.get(which);
414                         mPreviewSizes[cameraId] = new Size(
415                                 size.width, size.height);
416                         dialog.dismiss();
417 
418                         if (mPreviewSizeCamerasToProcess.isEmpty()) {
419                             // We're done, re-initialize camera.
420                             switchToCamera(mSelectedResolution, true);
421                         } else {
422                             // Process other cameras.
423                             showNextDialogToChoosePreviewSize();
424                         }
425                     }
426                 }).create().show();
427         camera.release();
428     }
429 
initializeCamera()430     private void initializeCamera() {
431         initializeCamera(true);
432     }
433 
initializeCamera(boolean startPreviewAfterInit)434     private void initializeCamera(boolean startPreviewAfterInit) {
435         if (mCamera == null || mSurfaceHolder.getSurface() == null) {
436             return;
437         }
438 
439         try {
440             mCamera.setPreviewDisplay(mSurfaceHolder);
441         } catch (Throwable t) {
442             Log.e(TAG, "Could not set preview display", t);
443             Toast.makeText(this, t.getMessage(), Toast.LENGTH_LONG).show();
444             return;
445         }
446 
447         calculateOrientations(this, mSelectedResolution.cameraId, mCamera);
448         Camera.Parameters params = setCameraParams(mCamera);
449 
450         // Either use chosen preview size for current camera or automatically
451         // choose preview size based on view dimensions.
452         Size selectedPreviewSize = null;
453         if (mPreviewSizes != null) {
454             selectedPreviewSize = mPreviewSizes[mSelectedResolution.cameraId];
455         } else if (mSurfaceSize != null) {
456             selectedPreviewSize = getBestPreviewSize(
457                     mSurfaceSize.width, mSurfaceSize.height, params);
458         }
459 
460         if (selectedPreviewSize != null) {
461             params.setPreviewSize(selectedPreviewSize.width, selectedPreviewSize.height);
462             mCamera.setParameters(params);
463             mCameraInitialized = true;
464         }
465 
466         if (startPreviewAfterInit) {
467             if (selectedPreviewSize == null) {
468                 Log.w(TAG, "Preview started without setting preview size");
469             }
470             startPreview();
471         }
472     }
473 
startPreview()474     private void startPreview() {
475         if (mCameraInitialized && mCamera != null) {
476             mCamera.setDisplayOrientation(mPreviewOrientation);
477             mCamera.startPreview();
478             mPreviewActive = true;
479         }
480     }
481 
switchToCamera(SelectableResolution resolution, boolean startPreview)482     private void switchToCamera(SelectableResolution resolution, boolean startPreview) {
483         if (mCamera != null) {
484             mCamera.stopPreview();
485             mCamera.release();
486         }
487 
488         mSelectedResolution = resolution;
489         mCamera = Camera.open(mSelectedResolution.cameraId);
490 
491         initializeCamera(startPreview);
492     }
493 
494     /**
495      * Get the best supported focus mode.
496      *
497      * @param camera - Android camera object.
498      * @return the best supported focus mode.
499      */
getFocusMode(Camera camera)500     private static String getFocusMode(Camera camera) {
501         List<String> modes = camera.getParameters().getSupportedFocusModes();
502         if (modes != null) {
503             if (modes.contains(Camera.Parameters.FOCUS_MODE_INFINITY)) {
504                 Log.v(TAG, "Using Focus mode infinity");
505                 return Camera.Parameters.FOCUS_MODE_INFINITY;
506             }
507             if (modes.contains(Camera.Parameters.FOCUS_MODE_FIXED)) {
508                 Log.v(TAG, "Using Focus mode fixed");
509                 return Camera.Parameters.FOCUS_MODE_FIXED;
510             }
511         }
512         Log.v(TAG, "Using Focus mode auto.");
513         return Camera.Parameters.FOCUS_MODE_AUTO;
514     }
515 
516     /**
517      * Set the common camera parameters on the given camera and returns the
518      * parameter object for further modification, if needed.
519      */
setCameraParams(Camera camera)520     private Camera.Parameters setCameraParams(Camera camera) {
521         // The picture size is taken and set from the spinner selection
522         // callback.
523         Camera.Parameters params = camera.getParameters();
524         params.setJpegThumbnailSize(0, 0);
525         params.setJpegQuality(100);
526         params.setRotation(mJpegOrientation);
527         params.setFocusMode(getFocusMode(camera));
528         params.setZoom(0);
529         params.setPictureSize(mSelectedResolution.width, mSelectedResolution.height);
530         return params;
531     }
532 
getBestPreviewSize( int width, int height, Camera.Parameters parameters)533     private Size getBestPreviewSize(
534             int width, int height, Camera.Parameters parameters) {
535         Size result = null;
536 
537         for (Camera.Size size : parameters.getSupportedPreviewSizes()) {
538             if (size.width <= width && size.height <= height) {
539                 if (result == null) {
540                     result = new Size(size.width, size.height);
541                 } else {
542                     int resultArea = result.width * result.height;
543                     int newArea = size.width * size.height;
544 
545                     if (newArea > resultArea) {
546                         result = new Size(size.width, size.height);
547                     }
548                 }
549             }
550         }
551         return result;
552     }
553 
calculateOrientations(Activity activity, int cameraId, android.hardware.Camera camera)554     private void calculateOrientations(Activity activity,
555             int cameraId, android.hardware.Camera camera) {
556         android.hardware.Camera.CameraInfo info =
557                 new android.hardware.Camera.CameraInfo();
558         android.hardware.Camera.getCameraInfo(cameraId, info);
559         int rotation = activity.getWindowManager().getDefaultDisplay()
560                 .getRotation();
561         int degrees = 0;
562         switch (rotation) {
563             case Surface.ROTATION_0: degrees = 0; break;
564             case Surface.ROTATION_90: degrees = 90; break;
565             case Surface.ROTATION_180: degrees = 180; break;
566             case Surface.ROTATION_270: degrees = 270; break;
567         }
568 
569         if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
570             mJpegOrientation = (info.orientation + degrees) % 360;
571             mPreviewOrientation = (360 - mJpegOrientation) % 360;  // compensate the mirror
572         } else {  // back-facing
573             mJpegOrientation = (info.orientation - degrees + 360) % 360;
574             mPreviewOrientation = mJpegOrientation;
575         }
576     }
577 
isExternalCamera(int cameraId)578     private boolean isExternalCamera(int cameraId) {
579         CameraManager manager = (CameraManager) this.getSystemService(Context.CAMERA_SERVICE);
580         try {
581             String cameraIdStr = manager.getCameraIdList()[cameraId];
582             CameraCharacteristics characteristics =
583                     manager.getCameraCharacteristics(cameraIdStr);
584 
585             if (characteristics.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL) ==
586                             CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_EXTERNAL) {
587                 // External camera doesn't support FOV informations
588                 return true;
589             }
590         } catch (CameraAccessException e) {
591             Toast.makeText(this, "Could not access camera " + cameraId +
592                     ": " + e.getMessage(), Toast.LENGTH_LONG).show();
593         }
594         return false;
595     }
596 }
597