1 /*
2  * Copyright (C) 2012 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5  * in compliance with the License. You may obtain a copy of the License at
6  *
7  * http://www.apache.org/licenses/LICENSE-2.0
8  *
9  * Unless required by applicable law or agreed to in writing, software distributed under the License
10  * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11  * or implied. See the License for the specific language governing permissions and limitations under
12  * the License.
13  */
14 package com.android.cts.verifier.camera.orientation;
15 
16 import android.content.Intent;
17 import android.graphics.Bitmap;
18 import android.graphics.BitmapFactory;
19 import android.graphics.ImageFormat;
20 import android.graphics.Matrix;
21 import android.hardware.Camera;
22 import android.os.Bundle;
23 import android.os.Handler;
24 import android.util.Log;
25 import android.view.SurfaceHolder;
26 import android.view.SurfaceView;
27 import android.view.View;
28 import android.view.View.OnClickListener;
29 import android.widget.Button;
30 import android.widget.ImageButton;
31 import android.widget.ImageView;
32 import android.widget.LinearLayout.LayoutParams;
33 import android.widget.TextView;
34 
35 import com.android.cts.verifier.PassFailButtons;
36 import com.android.cts.verifier.R;
37 import com.android.cts.verifier.TestResult;
38 
39 import java.io.IOException;
40 import java.util.ArrayList;
41 import java.util.Comparator;
42 import java.util.List;
43 import java.util.TreeSet;
44 
45 /**
46  * Tests for manual verification of the CDD-required camera output formats
47  * for preview callbacks
48  */
49 public class CameraOrientationActivity extends PassFailButtons.Activity
50 implements OnClickListener, SurfaceHolder.Callback {
51 
52     private static final String TAG = "CameraOrientation";
53     private static final int STATE_OFF = 0;
54     private static final int STATE_PREVIEW = 1;
55     private static final int STATE_CAPTURE = 2;
56     private static final int NUM_ORIENTATIONS = 4;
57     private static final String STAGE_INDEX_EXTRA = "stageIndex";
58 
59     private ImageButton mPassButton;
60     private ImageButton mFailButton;
61     private Button mTakePictureButton;
62 
63     private SurfaceView mCameraView;
64     private ImageView mFormatView;
65     private SurfaceHolder mSurfaceHolder;
66     private Camera mCamera;
67     private List<Camera.Size> mPreviewSizes;
68     private Camera.Size mOptimalSize;
69     private List<Integer> mPreviewOrientations;
70     private int mNextPreviewOrientation;
71     private int mNumCameras;
72     private int mCurrentCameraId = -1;
73     private int mState = STATE_OFF;
74     private boolean mSizeAdjusted;
75 
76     private StringBuilder mReportBuilder = new StringBuilder();
77     private final TreeSet<String> mTestedCombinations = new TreeSet<String>();
78     private final TreeSet<String> mUntestedCombinations = new TreeSet<String>();
79 
80     @Override
onCreate(Bundle savedInstanceState)81     public void onCreate(Bundle savedInstanceState) {
82         super.onCreate(savedInstanceState);
83 
84         setContentView(R.layout.co_main);
85         setPassFailButtonClickListeners();
86         setInfoResources(R.string.camera_orientation, R.string.co_info, -1);
87         mNumCameras = Camera.getNumberOfCameras();
88 
89         mPassButton         = (ImageButton) findViewById(R.id.pass_button);
90         mFailButton         = (ImageButton) findViewById(R.id.fail_button);
91         mTakePictureButton  = (Button) findViewById(R.id.take_picture_button);
92         mFormatView         = (ImageView) findViewById(R.id.format_view);
93         mCameraView         = (SurfaceView) findViewById(R.id.camera_view);
94 
95         mFormatView.setOnClickListener(this);
96         mCameraView.setOnClickListener(this);
97         mTakePictureButton.setOnClickListener(this);
98 
99         mSurfaceHolder = mCameraView.getHolder();
100         mSurfaceHolder.addCallback(this);
101 
102         mPreviewOrientations = new ArrayList<Integer>();
103         mPreviewOrientations.add(0);
104         mPreviewOrientations.add(90);
105         mPreviewOrientations.add(180);
106         mPreviewOrientations.add(270);
107 
108         // This activity is reused multiple times
109         // to test each camera/orientation combination
110         final int stageIndex = getIntent().getIntExtra(STAGE_INDEX_EXTRA, 0);
111         Settings settings = getSettings(stageIndex);
112 
113         // Hitting the pass button goes to the next test activity.
114         // Only the last one uses the PassFailButtons click callback function,
115         // which gracefully terminates the activity.
116         if (stageIndex + 1 < mNumCameras * NUM_ORIENTATIONS) {
117             setPassButtonGoesToNextStage(stageIndex);
118         }
119 
120         String[] availableOrientations = new String[NUM_ORIENTATIONS];
121         for (int i=0; i<availableOrientations.length; i++) {
122             // append degree symbol
123             availableOrientations[i] = Integer.toString(i * 90) + "\u00b0";
124         }
125 
126         resetButtons();
127 
128         // Set initial values
129         mSizeAdjusted = false;
130         mCurrentCameraId = settings.mCameraId;
131         TextView cameraLabel = (TextView) findViewById(R.id.camera_text);
132         cameraLabel.setText(
133                 getString(R.string.co_camera_label)
134                 + " " + (mCurrentCameraId+1) + " of " + mNumCameras);
135 
136         mNextPreviewOrientation = settings.mOrientation;
137         TextView orientationLabel =
138                 (TextView) findViewById(R.id.orientation_text);
139         orientationLabel.setText(
140                 getString(R.string.co_orientation_label)
141                 + " "
142                 + Integer.toString(mNextPreviewOrientation+1)
143                 + " of "
144                 + Integer.toString(NUM_ORIENTATIONS)
145                 + ": "
146                 + mPreviewOrientations.get(mNextPreviewOrientation) + "\u00b0"
147                 + " "
148                 + getString(R.string.co_orientation_direction_label)
149                 );
150 
151         TextView instructionLabel =
152                 (TextView) findViewById(R.id.instruction_text);
153         instructionLabel.setText(R.string.co_instruction_text_photo_label);
154 
155         mTakePictureButton.setEnabled(false);
156         setUpCamera(mCurrentCameraId);
157     }
158 
159     @Override
onResume()160     public void onResume() {
161         super.onResume();
162         setUpCamera(mCurrentCameraId);
163     }
164 
165     @Override
onPause()166     public void onPause() {
167         super.onPause();
168         shutdownCamera();
169     }
170 
171     @Override
getTestDetails()172     public String getTestDetails() {
173         return mReportBuilder.toString();
174     }
175 
setUpCamera(int id)176     private void setUpCamera(int id) {
177         shutdownCamera();
178 
179         Log.v(TAG, "Setting up Camera " + id);
180         mCurrentCameraId = id;
181 
182         try {
183             mCamera = Camera.open(id);
184         } catch (Exception e) {
185             Log.e(TAG, "Error opening camera");
186         }
187 
188         Camera.Parameters p = mCamera.getParameters();
189 
190         // Get preview resolutions
191         List<Camera.Size> unsortedSizes = p.getSupportedPreviewSizes();
192         class SizeCompare implements Comparator<Camera.Size> {
193             @Override
194             public int compare(Camera.Size lhs, Camera.Size rhs) {
195                 if (lhs.width < rhs.width) return -1;
196                 if (lhs.width > rhs.width) return 1;
197                 if (lhs.height < rhs.height) return -1;
198                 if (lhs.height > rhs.height) return 1;
199                 return 0;
200             }
201         }
202         SizeCompare s = new SizeCompare();
203         TreeSet<Camera.Size> sortedResolutions = new TreeSet<Camera.Size>(s);
204         sortedResolutions.addAll(unsortedSizes);
205         mPreviewSizes = new ArrayList<Camera.Size>(sortedResolutions);
206 
207         startPreview();
208     }
209 
shutdownCamera()210     private void shutdownCamera() {
211         if (mCamera != null) {
212             mCamera.setPreviewCallback(null);
213             mCamera.stopPreview();
214             mCamera.release();
215             mCamera = null;
216             mState = STATE_OFF;
217         }
218     }
219 
startPreview()220     private void startPreview() {
221         if (mState != STATE_OFF) {
222             // Stop for a while to drain callbacks
223             mCamera.setPreviewCallback(null);
224             mCamera.stopPreview();
225             mState = STATE_OFF;
226             Handler h = new Handler();
227             Runnable mDelayedPreview = new Runnable() {
228                 @Override
229                 public void run() {
230                     startPreview();
231                 }
232             };
233             h.postDelayed(mDelayedPreview, 300);
234             return;
235         }
236 
237         mCamera.setPreviewCallback(mPreviewCallback);
238 
239         try {
240             mCamera.setPreviewDisplay(mCameraView.getHolder());
241         } catch (IOException ioe) {
242             Log.e(TAG, "Unable to connect camera to display");
243         }
244 
245         Camera.Parameters p = mCamera.getParameters();
246         Log.v(TAG, "Initializing picture format");
247         p.setPictureFormat(ImageFormat.JPEG);
248         mOptimalSize = getOptimalPreviewSize(mPreviewSizes, 640, 480);
249         Log.v(TAG, "Initializing picture size to "
250                 + mOptimalSize.width + "x" + mOptimalSize.height);
251         p.setPictureSize(mOptimalSize.width, mOptimalSize.height);
252         Log.v(TAG, "Initializing preview size to "
253                 + mOptimalSize.width + "x" + mOptimalSize.height);
254         p.setPreviewSize(mOptimalSize.width, mOptimalSize.height);
255 
256         Log.v(TAG, "Setting camera parameters");
257         mCamera.setParameters(p);
258         Log.v(TAG, "Setting color filter");
259         mFormatView.setColorFilter(null);
260         Log.v(TAG, "Starting preview");
261         try {
262             mCamera.startPreview();
263         } catch (Exception e) {
264             Log.d(TAG, "Cannot start preview", e);
265         }
266 
267         // set preview orientation
268         int degrees = mPreviewOrientations.get(mNextPreviewOrientation);
269         mCamera.setDisplayOrientation(degrees);
270 
271         android.hardware.Camera.CameraInfo info =
272                 new android.hardware.Camera.CameraInfo();
273         android.hardware.Camera.getCameraInfo(mCurrentCameraId, info);
274         if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
275             TextView cameraExtraLabel =
276                     (TextView) findViewById(R.id.instruction_extra_text);
277             cameraExtraLabel.setText(
278                     getString(R.string.co_instruction_text_extra_label));
279         }
280 
281         mState = STATE_PREVIEW;
282     }
283 
284     @Override
onClick(View view)285     public void onClick(View view) {
286         Log.v(TAG, "Click detected");
287 
288         if (view == mFormatView || view == mTakePictureButton) {
289             if(mState == STATE_PREVIEW) {
290                 mTakePictureButton.setEnabled(false);
291                 Log.v(TAG, "Taking picture");
292                 mCamera.takePicture(null, null, null, mCameraCallback);
293                 mState = STATE_CAPTURE;
294             }
295         }
296 
297         if(view == mPassButton || view == mFailButton) {
298             final int stageIndex =
299                     getIntent().getIntExtra(STAGE_INDEX_EXTRA, 0);
300             String[] cameraNames = new String[mNumCameras];
301             int counter = 0;
302             for (int i = 0; i < mNumCameras; i++) {
303                 cameraNames[i] = "Camera " + i;
304 
305                 for(int j = 0; j < mPreviewOrientations.size(); j++) {
306                     String combination = cameraNames[i] + ", "
307                             + mPreviewOrientations.get(j)
308                             + "\u00b0"
309                             + "\n";
310 
311                     if(counter < stageIndex) {
312                         // test already passed, or else wouldn't have made
313                         // it to current stageIndex
314                         mTestedCombinations.add(combination);
315                     }
316 
317                     if(counter == stageIndex) {
318                         // current test configuration
319                         if(view == mPassButton) {
320                             mTestedCombinations.add(combination);
321                         }
322                         else if(view == mFailButton) {
323                             mUntestedCombinations.add(combination);
324                         }
325                     }
326 
327                     if(counter > stageIndex) {
328                         // test not passed yet, since haven't made it to
329                         // stageIndex
330                         mUntestedCombinations.add(combination);
331                     }
332 
333                     counter++;
334                 }
335             }
336 
337             mReportBuilder = new StringBuilder();
338             mReportBuilder.append("Passed combinations:\n");
339             for (String combination : mTestedCombinations) {
340                 mReportBuilder.append(combination);
341             }
342             mReportBuilder.append("Failed/untested combinations:\n");
343             for (String combination : mUntestedCombinations) {
344                 mReportBuilder.append(combination);
345             }
346 
347             if(view == mPassButton) {
348                 TestResult.setPassedResult(this, "CameraOrientationActivity",
349                         getTestDetails());
350             }
351             if(view == mFailButton) {
352                 TestResult.setFailedResult(this, "CameraOrientationActivity",
353                         getTestDetails());
354             }
355 
356             // restart activity to test next orientation
357             Intent intent = new Intent(CameraOrientationActivity.this,
358                     CameraOrientationActivity.class);
359             intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP
360                     | Intent.FLAG_ACTIVITY_FORWARD_RESULT);
361             intent.putExtra(STAGE_INDEX_EXTRA, stageIndex + 1);
362             startActivity(intent);
363         }
364     }
365 
resetButtons()366     private void resetButtons() {
367         enablePassFailButtons(false);
368     }
369 
enablePassFailButtons(boolean enable)370     private void enablePassFailButtons(boolean enable) {
371         mPassButton.setEnabled(enable);
372         mFailButton.setEnabled(enable);
373     }
374 
375     // find a supported size with ratio less than tolerance threshold, and
376     // which is closest to height and width of given dimensions without
377     // being larger than either of given dimensions
getOptimalPreviewSize(List<Camera.Size> sizes, int w, int h)378     private Camera.Size getOptimalPreviewSize(List<Camera.Size> sizes, int w,
379             int h) {
380         final double ASPECT_TOLERANCE = 0.1;
381         double targetRatio = (double) 640 / (double) 480;
382         if (sizes == null) return null;
383 
384         Camera.Size optimalSize = null;
385         int minDiff = Integer.MAX_VALUE;
386         int curDiff;
387 
388         int targetHeight = h;
389         int targetWidth = w;
390 
391         boolean aspectRatio = true;
392         boolean maintainCeiling = true;
393         while(true) {
394             for (Camera.Size size : sizes) {
395                 if(aspectRatio) {
396                     double ratio = (double) size.width / size.height;
397                     if (Math.abs(ratio - targetRatio) > ASPECT_TOLERANCE) {
398                         continue;
399                     }
400                 }
401                 curDiff = Math.abs(size.height - targetHeight) +
402                         Math.abs(size.width - targetWidth);
403                 if (maintainCeiling && curDiff < minDiff
404                         && size.height <= targetHeight
405                         && size.width <= targetWidth) {
406                     optimalSize = size;
407                     minDiff = curDiff;
408                 } else if (maintainCeiling == false
409                                && curDiff < minDiff) {
410                     //try to get as close as possible
411                     optimalSize = size;
412                     minDiff = curDiff;
413                 }
414             }
415             if (optimalSize == null && aspectRatio == true) {
416                 // Cannot find a match, so repeat search and
417                 // ignore aspect ratio requirement
418                 aspectRatio = false;
419             } else if (maintainCeiling == true) {
420                 //Camera resolutions are greater than ceiling provided
421                 //lets try to get as close as we can
422                 maintainCeiling = false;
423             } else {
424                 break;
425             }
426         }
427 
428         return optimalSize;
429     }
430 
431     @Override
surfaceChanged(SurfaceHolder holder, int format, int width, int height)432     public void surfaceChanged(SurfaceHolder holder, int format, int width,
433             int height) {
434         startPreview();
435     }
436 
setTestedConfiguration(int cameraId, int orientation)437     private void setTestedConfiguration(int cameraId, int orientation) {
438         String combination = "Camera " + cameraId + ", "
439                 + orientation
440                 + "\u00b0"
441                 + "\n";
442         if (!mTestedCombinations.contains(combination)) {
443             mTestedCombinations.add(combination);
444             mUntestedCombinations.remove(combination);
445         }
446     }
447 
448     @Override
surfaceCreated(SurfaceHolder holder)449     public void surfaceCreated(SurfaceHolder holder) {
450         // Auto-generated method stub
451     }
452 
453     @Override
surfaceDestroyed(SurfaceHolder holder)454     public void surfaceDestroyed(SurfaceHolder holder) {
455         // Auto-generated method stub
456     }
457 
458     private final Camera.PreviewCallback mPreviewCallback =
459             new Camera.PreviewCallback() {
460         @Override
461         public void onPreviewFrame(byte[] data, Camera camera) {
462             // adjust camera preview to match output image's aspect ratio
463             if(!mSizeAdjusted && mState == STATE_PREVIEW) {
464                 int viewWidth = mFormatView.getWidth();
465                 int viewHeight = mFormatView.getHeight();
466                 int newWidth, newHeight;
467 
468                 if (mPreviewOrientations.get(mNextPreviewOrientation) == 0
469                     || mPreviewOrientations.get(mNextPreviewOrientation) == 180) {
470                     // make preview width same as output image width,
471                     // then calculate height using output image's height/width ratio
472                     newWidth = viewWidth;
473                     newHeight = (int) (viewWidth * ((double) mOptimalSize.height /
474                             (double) mOptimalSize.width));
475                 }
476                 else {
477                     newHeight = viewHeight;
478                     newWidth = (int) (viewHeight * ((double) mOptimalSize.height /
479                             (double) mOptimalSize.width));
480                 }
481 
482                 LayoutParams layoutParams = new LayoutParams(newWidth, newHeight);
483                 mCameraView.setLayoutParams(layoutParams);
484                 mSizeAdjusted = true;
485                 mTakePictureButton.setEnabled(true);
486             }
487         }
488     };
489 
490     private final Camera.PictureCallback mCameraCallback =
491             new Camera.PictureCallback() {
492         @Override
493         public void onPictureTaken(byte[] data, Camera mCamera) {
494             if (data != null) {
495                 Bitmap inputImage;
496                 inputImage = BitmapFactory.decodeByteArray(data, 0, data.length);
497 
498                 int degrees = mPreviewOrientations.get(mNextPreviewOrientation);
499                 android.hardware.Camera.CameraInfo info =
500                         new android.hardware.Camera.CameraInfo();
501                 android.hardware.Camera.getCameraInfo(mCurrentCameraId, info);
502                 float mirrorX[];
503                 if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
504                     // mirror the image along vertical axis
505                     mirrorX = new float[] {-1, 0, 0, 0, 1, 1, 0, 0, 1};
506                     degrees = (360 - degrees) % 360; // compensate the mirror
507                 } else {
508                     // leave image the same via identity matrix
509                     mirrorX = new float[] {1, 0, 0, 0, 1, 0, 0, 0, 1};
510                 }
511 
512                 // use matrix to transform the image
513                 Matrix matrixMirrorX = new Matrix();
514                 matrixMirrorX.setValues(mirrorX);
515                 Matrix mat = new Matrix();
516                 mat.postRotate(degrees);
517                 mat.postConcat(matrixMirrorX);
518 
519                 Bitmap inputImageAdjusted = Bitmap.createBitmap(inputImage,
520                         0,
521                         0,
522                         inputImage.getWidth(),
523                         inputImage.getHeight(),
524                         mat,
525                         true);
526                 mFormatView.setImageBitmap(inputImageAdjusted);
527 
528                 Log.v(TAG, "Output image set");
529                 enablePassFailButtons(true);
530 
531                 TextView instructionLabel =
532                         (TextView) findViewById(R.id.instruction_text);
533                 instructionLabel.setText(
534                         R.string.co_instruction_text_passfail_label);
535             }
536 
537             startPreview();
538         }
539     };
540 
setPassButtonGoesToNextStage(final int stageIndex)541     private void setPassButtonGoesToNextStage(final int stageIndex) {
542         findViewById(R.id.pass_button).setOnClickListener(this);
543     }
544 
getSettings(int stageIndex)545     private Settings getSettings(int stageIndex) {
546         int curCameraId = stageIndex / NUM_ORIENTATIONS;
547         int curOrientation = stageIndex % NUM_ORIENTATIONS;
548         return new Settings(stageIndex, curCameraId, curOrientation);
549     }
550 
551     // Bundle of settings for testing a particular
552     // camera/orientation combination
553     class Settings {
554         int mCameraId;
555         int mOrientation;
556 
Settings(int stageIndex, int cameraId, int orientation)557         Settings(int stageIndex, int cameraId, int orientation) {
558             mCameraId = cameraId;
559             mOrientation = orientation;
560         }
561     }
562 }
563