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