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