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