1 /*
2  * Copyright (C) 2012 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.testingcamera;
18 
19 import android.Manifest;
20 import android.annotation.SuppressLint;
21 import android.app.Activity;
22 import android.app.FragmentManager;
23 import android.content.pm.PackageManager;
24 import android.content.res.Resources;
25 import android.graphics.ImageFormat;
26 import android.hardware.Camera;
27 import android.hardware.Camera.Parameters;
28 import android.hardware.Camera.ErrorCallback;
29 import android.media.CamcorderProfile;
30 import android.media.MediaRecorder;
31 import android.media.MediaScannerConnection;
32 import android.net.Uri;
33 import android.os.Bundle;
34 import android.os.Environment;
35 import android.os.Handler;
36 import android.os.SystemClock;
37 import android.view.OrientationEventListener;
38 import android.view.View;
39 import android.view.Surface;
40 import android.view.SurfaceHolder;
41 import android.view.SurfaceView;
42 import android.view.View.OnClickListener;
43 import android.widget.AdapterView;
44 import android.widget.AdapterView.OnItemSelectedListener;
45 import android.widget.ArrayAdapter;
46 import android.widget.Button;
47 import android.widget.CheckBox;
48 import android.widget.LinearLayout;
49 import android.widget.LinearLayout.LayoutParams;
50 import android.widget.SeekBar;
51 import android.widget.Spinner;
52 import android.widget.TextView;
53 import android.widget.ToggleButton;
54 import android.renderscript.RenderScript;
55 import android.text.Layout;
56 import android.text.method.ScrollingMovementMethod;
57 import android.util.Log;
58 import android.util.SparseArray;
59 
60 import java.io.File;
61 import java.io.IOException;
62 import java.io.PrintWriter;
63 import java.io.StringWriter;
64 import java.text.SimpleDateFormat;
65 import java.util.ArrayList;
66 import java.util.Date;
67 import java.util.HashSet;
68 import java.util.List;
69 import java.util.Set;
70 
71 /**
72  * A simple test application for the camera API.
73  *
74  * The goal of this application is to allow all camera API features to be
75  * exercised, and all information provided by the API to be shown.
76  */
77 public class TestingCamera extends Activity
78     implements SurfaceHolder.Callback, Camera.PreviewCallback,
79         Camera.ErrorCallback {
80 
81     /** UI elements */
82     private SurfaceView mPreviewView;
83     private SurfaceHolder mPreviewHolder;
84     private LinearLayout mPreviewColumn;
85 
86     private SurfaceView mCallbackView;
87     private SurfaceHolder mCallbackHolder;
88 
89     private Spinner mCameraSpinner;
90     private CheckBox mKeepOpenCheckBox;
91     private Button mInfoButton;
92     private Spinner mPreviewSizeSpinner;
93     private Spinner mPreviewFrameRateSpinner;
94     private ToggleButton mPreviewToggle;
95     private ToggleButton mHDRToggle;
96     private Spinner mAutofocusModeSpinner;
97     private Button mAutofocusButton;
98     private Button mCancelAutofocusButton;
99     private TextView mFlashModeSpinnerLabel;
100     private Spinner mFlashModeSpinner;
101     private ToggleButton mExposureLockToggle;
102     private Spinner mSnapshotSizeSpinner;
103     private Button  mTakePictureButton;
104     private Spinner mCamcorderProfileSpinner;
105     private Spinner mVideoRecordSizeSpinner;
106     private Spinner mVideoFrameRateSpinner;
107     private ToggleButton mRecordToggle;
108     private CheckBox mRecordHandoffCheckBox;
109     private ToggleButton mRecordStabilizationToggle;
110     private ToggleButton mRecordHintToggle;
111     private ToggleButton mLockCameraToggle;
112     private Spinner mCallbackFormatSpinner;
113     private ToggleButton mCallbackToggle;
114     private TextView mColorEffectSpinnerLabel;
115     private Spinner mColorEffectSpinner;
116     private SeekBar mZoomSeekBar;
117 
118     private TextView mLogView;
119 
120     SnapshotDialogFragment mSnapshotDialog = null;
121 
122     private Set<View> mOpenOnlyControls = new HashSet<View>();
123     private Set<View> mPreviewOnlyControls = new HashSet<View>();
124 
125     private SparseArray<String> mFormatNames;
126 
127     /** Camera state */
128     private int mCameraId;
129     private Camera mCamera;
130     private Camera.Parameters mParams;
131     private List<Camera.Size> mPreviewSizes;
132     private int mPreviewSize = 0;
133     private List<Integer> mPreviewFrameRates;
134     private int mPreviewFrameRate = 0;
135     private List<Integer> mPreviewFormats;
136     private int mPreviewFormat = 0;
137     private List<String> mAfModes;
138     private int mAfMode = 0;
139     private List<String> mFlashModes;
140     private int mFlashMode = 0;
141     private List<Camera.Size> mSnapshotSizes;
142     private int mSnapshotSize = 0;
143     private List<CamcorderProfile> mCamcorderProfiles;
144     private int mCamcorderProfile = 0;
145     private List<Camera.Size> mVideoRecordSizes;
146     private int mVideoRecordSize = 0;
147     private List<Integer> mVideoFrameRates;
148     private int mVideoFrameRate = 0;
149     private List<String> mColorEffects;
150     private int mColorEffect = 0;
151     private int mZoom = 0;
152 
153     private MediaRecorder mRecorder;
154     private File mRecordingFile;
155 
156     private RenderScript mRS;
157 
158     private boolean mCallbacksEnabled = false;
159     private CallbackProcessor mCallbackProcessor = null;
160     long mLastCallbackTimestamp = -1;
161     float mCallbackAvgFrameDuration = 30;
162     int mCallbackFrameCount = 0;
163     private static final float MEAN_FPS_HISTORY_COEFF = 0.9f;
164     private static final float MEAN_FPS_MEASUREMENT_COEFF = 0.1f;
165     private static final int   FPS_REPORTING_PERIOD = 200; // frames
166     private static final int CALLBACK_BUFFER_COUNT = 3;
167 
168     private static final int CAMERA_UNINITIALIZED = 0;
169     private static final int CAMERA_OPEN = 1;
170     private static final int CAMERA_PREVIEW = 2;
171     private static final int CAMERA_TAKE_PICTURE = 3;
172     private static final int CAMERA_RECORD = 4;
173     private int mState = CAMERA_UNINITIALIZED;
174 
175     private static final int NO_CAMERA_ID = -1;
176 
177     /** Misc variables */
178 
179     private static final String TAG = "TestingCamera";
180     private static final int PERMISSIONS_REQUEST_CAMERA = 1;
181     private static final int PERMISSIONS_REQUEST_RECORDING = 2;
182     static final int PERMISSIONS_REQUEST_SNAPSHOT = 3;
183     private OrientationEventHandler mOrientationHandler;
184 
185     /** Activity lifecycle */
186 
187     @Override
onCreate(Bundle savedInstanceState)188     public void onCreate(Bundle savedInstanceState) {
189         super.onCreate(savedInstanceState);
190 
191         setContentView(R.layout.main);
192 
193         mPreviewColumn = (LinearLayout) findViewById(R.id.preview_column);
194 
195         mPreviewView = (SurfaceView) findViewById(R.id.preview);
196         mPreviewView.getHolder().addCallback(this);
197 
198         mCallbackView = (SurfaceView)findViewById(R.id.callback_view);
199 
200         mCameraSpinner = (Spinner) findViewById(R.id.camera_spinner);
201         mCameraSpinner.setOnItemSelectedListener(mCameraSpinnerListener);
202 
203         mKeepOpenCheckBox = (CheckBox) findViewById(R.id.keep_open_checkbox);
204 
205         mInfoButton = (Button) findViewById(R.id.info_button);
206         mInfoButton.setOnClickListener(mInfoButtonListener);
207         mOpenOnlyControls.add(mInfoButton);
208 
209         mPreviewSizeSpinner = (Spinner) findViewById(R.id.preview_size_spinner);
210         mPreviewSizeSpinner.setOnItemSelectedListener(mPreviewSizeListener);
211         mOpenOnlyControls.add(mPreviewSizeSpinner);
212 
213         mPreviewFrameRateSpinner = (Spinner) findViewById(R.id.preview_frame_rate_spinner);
214         mPreviewFrameRateSpinner.setOnItemSelectedListener(mPreviewFrameRateListener);
215         mOpenOnlyControls.add(mPreviewFrameRateSpinner);
216 
217         mHDRToggle = (ToggleButton) findViewById(R.id.hdr_mode);
218         mHDRToggle.setOnClickListener(mHDRToggleListener);
219         mOpenOnlyControls.add(mHDRToggle);
220 
221         mPreviewToggle = (ToggleButton) findViewById(R.id.start_preview);
222         mPreviewToggle.setOnClickListener(mPreviewToggleListener);
223         mOpenOnlyControls.add(mPreviewToggle);
224 
225         mAutofocusModeSpinner = (Spinner) findViewById(R.id.af_mode_spinner);
226         mAutofocusModeSpinner.setOnItemSelectedListener(mAutofocusModeListener);
227         mOpenOnlyControls.add(mAutofocusModeSpinner);
228 
229         mAutofocusButton = (Button) findViewById(R.id.af_button);
230         mAutofocusButton.setOnClickListener(mAutofocusButtonListener);
231         mPreviewOnlyControls.add(mAutofocusButton);
232 
233         mCancelAutofocusButton = (Button) findViewById(R.id.af_cancel_button);
234         mCancelAutofocusButton.setOnClickListener(mCancelAutofocusButtonListener);
235         mPreviewOnlyControls.add(mCancelAutofocusButton);
236 
237         mFlashModeSpinnerLabel = (TextView) findViewById(R.id.flash_mode_spinner_label);
238 
239         mFlashModeSpinner = (Spinner) findViewById(R.id.flash_mode_spinner);
240         mFlashModeSpinner.setOnItemSelectedListener(mFlashModeListener);
241         mOpenOnlyControls.add(mFlashModeSpinner);
242 
243         mExposureLockToggle = (ToggleButton) findViewById(R.id.exposure_lock);
244         mExposureLockToggle.setOnClickListener(mExposureLockToggleListener);
245         mOpenOnlyControls.add(mExposureLockToggle);
246 
247         mZoomSeekBar = (SeekBar) findViewById(R.id.zoom_seekbar);
248         mZoomSeekBar.setOnSeekBarChangeListener(mZoomSeekBarListener);
249 
250         mSnapshotSizeSpinner = (Spinner) findViewById(R.id.snapshot_size_spinner);
251         mSnapshotSizeSpinner.setOnItemSelectedListener(mSnapshotSizeListener);
252         mOpenOnlyControls.add(mSnapshotSizeSpinner);
253 
254         mTakePictureButton = (Button) findViewById(R.id.take_picture);
255         mTakePictureButton.setOnClickListener(mTakePictureListener);
256         mPreviewOnlyControls.add(mTakePictureButton);
257 
258         mCamcorderProfileSpinner = (Spinner) findViewById(R.id.camcorder_profile_spinner);
259         mCamcorderProfileSpinner.setOnItemSelectedListener(mCamcorderProfileListener);
260         mOpenOnlyControls.add(mCamcorderProfileSpinner);
261 
262         mVideoRecordSizeSpinner = (Spinner) findViewById(R.id.video_record_size_spinner);
263         mVideoRecordSizeSpinner.setOnItemSelectedListener(mVideoRecordSizeListener);
264         mOpenOnlyControls.add(mVideoRecordSizeSpinner);
265 
266         mVideoFrameRateSpinner = (Spinner) findViewById(R.id.video_frame_rate_spinner);
267         mVideoFrameRateSpinner.setOnItemSelectedListener(mVideoFrameRateListener);
268         mOpenOnlyControls.add(mVideoFrameRateSpinner);
269 
270         mRecordToggle = (ToggleButton) findViewById(R.id.start_record);
271         mRecordToggle.setOnClickListener(mRecordToggleListener);
272         mPreviewOnlyControls.add(mRecordToggle);
273 
274         mRecordHandoffCheckBox = (CheckBox) findViewById(R.id.record_handoff_checkbox);
275 
276         mRecordStabilizationToggle = (ToggleButton) findViewById(R.id.record_stabilization);
277         mRecordStabilizationToggle.setOnClickListener(mRecordStabilizationToggleListener);
278         mOpenOnlyControls.add(mRecordStabilizationToggle);
279 
280         mRecordHintToggle = (ToggleButton) findViewById(R.id.record_hint);
281         mRecordHintToggle.setOnClickListener(mRecordHintToggleListener);
282         mOpenOnlyControls.add(mRecordHintToggle);
283 
284         mLockCameraToggle = (ToggleButton) findViewById(R.id.lock_camera);
285         mLockCameraToggle.setOnClickListener(mLockCameraToggleListener);
286         mLockCameraToggle.setChecked(true); // ON by default
287         mOpenOnlyControls.add(mLockCameraToggle);
288 
289         mCallbackFormatSpinner = (Spinner) findViewById(R.id.callback_format_spinner);
290         mCallbackFormatSpinner.setOnItemSelectedListener(mCallbackFormatListener);
291         mOpenOnlyControls.add(mCallbackFormatSpinner);
292 
293         mCallbackToggle = (ToggleButton) findViewById(R.id.enable_callbacks);
294         mCallbackToggle.setOnClickListener(mCallbackToggleListener);
295         mOpenOnlyControls.add(mCallbackToggle);
296 
297         mColorEffectSpinnerLabel = (TextView) findViewById(R.id.color_effect_spinner_label);
298 
299         mColorEffectSpinner = (Spinner) findViewById(R.id.color_effect_spinner);
300         mColorEffectSpinner.setOnItemSelectedListener(mColorEffectListener);
301         mOpenOnlyControls.add(mColorEffectSpinner);
302 
303         mLogView = (TextView) findViewById(R.id.log);
304         mLogView.setMovementMethod(new ScrollingMovementMethod());
305 
306         mOpenOnlyControls.addAll(mPreviewOnlyControls);
307 
308         mFormatNames = new SparseArray<String>(7);
309         mFormatNames.append(ImageFormat.JPEG, "JPEG");
310         mFormatNames.append(ImageFormat.NV16, "NV16");
311         mFormatNames.append(ImageFormat.NV21, "NV21");
312         mFormatNames.append(ImageFormat.RGB_565, "RGB_565");
313         mFormatNames.append(ImageFormat.UNKNOWN, "UNKNOWN");
314         mFormatNames.append(ImageFormat.YUY2, "YUY2");
315         mFormatNames.append(ImageFormat.YV12, "YV12");
316 
317         int numCameras = Camera.getNumberOfCameras();
318         String[] cameraNames = new String[numCameras + 1];
319         cameraNames[0] = "None";
320         for (int i = 0; i < numCameras; i++) {
321             cameraNames[i + 1] = "Camera " + i;
322         }
323 
324         mCameraSpinner.setAdapter(
325                 new ArrayAdapter<String>(this,
326                         R.layout.spinner_item, cameraNames));
327         if (numCameras > 0) {
328             mCameraId = 0;
329             mCameraSpinner.setSelection(mCameraId + 1);
330         } else {
331             resetCamera();
332             mCameraSpinner.setSelection(0);
333         }
334 
335         mRS = RenderScript.create(this);
336 
337         mOrientationHandler = new OrientationEventHandler(this);
338     }
339 
340     private static class OrientationEventHandler extends OrientationEventListener {
341         private TestingCamera mActivity;
342         private int mCurrentRotation = -1;
OrientationEventHandler(TestingCamera activity)343         OrientationEventHandler(TestingCamera activity) {
344             super(activity);
345             mActivity = activity;
346         }
347 
348         @Override
onOrientationChanged(int orientation)349         public void onOrientationChanged(int orientation) {
350             if (mActivity != null) {
351                 int rotation = mActivity.getWindowManager().getDefaultDisplay().getRotation();
352                 if (mCurrentRotation != rotation) {
353                     mCurrentRotation = rotation;
354                     mActivity.runOnUiThread(new Runnable() {
355                         @Override
356                         public void run() {
357                             mActivity.setCameraDisplayOrientation();
358                             mActivity.resizePreview();
359                         }
360                     });
361                 }
362             }
363         }
364     }
365 
366     @Override
onResume()367     public void onResume() {
368         super.onResume();
369         log("onResume: Setting up");
370         setUpCamera();
371         mOrientationHandler.enable();
372     }
373 
374     @Override
onPause()375     public void onPause() {
376         super.onPause();
377         if (mState == CAMERA_RECORD) {
378             stopRecording(false);
379         }
380         if (mKeepOpenCheckBox.isChecked()) {
381             log("onPause: Not releasing camera");
382 
383             if (mState == CAMERA_PREVIEW) {
384                 mCamera.stopPreview();
385                 mState = CAMERA_OPEN;
386             }
387         } else {
388             log("onPause: Releasing camera");
389 
390             if (mCamera != null) {
391                 mCamera.release();
392             }
393             mState = CAMERA_UNINITIALIZED;
394         }
395         mOrientationHandler.disable();
396     }
397 
398     @Override
onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults)399     public void onRequestPermissionsResult (int requestCode, String[] permissions,
400             int[] grantResults) {
401         if (requestCode == PERMISSIONS_REQUEST_CAMERA) {
402             if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
403                 log("Camera permission granted");
404                 setUpCamera();
405             } else {
406                 log("Camera permission denied, can't do anything");
407                 finish();
408             }
409         } else if (requestCode == PERMISSIONS_REQUEST_RECORDING) {
410             mRecordToggle.setChecked(false);
411             for (int i = 0; i < grantResults.length; i++) {
412                 if (grantResults[i] == PackageManager.PERMISSION_DENIED) {
413                     log("Recording permission " + permissions[i] + " denied");
414                     return;
415                 }
416                 log("Recording permissions granted");
417                 setUpCamera();
418             }
419         } else if (requestCode == PERMISSIONS_REQUEST_SNAPSHOT) {
420             if (mSnapshotDialog != null) {
421                 mSnapshotDialog.onRequestPermissionsResult(requestCode, permissions,
422                     grantResults);
423             }
424         }
425 
426     }
427 
428     /** SurfaceHolder.Callback methods */
429     @Override
surfaceChanged(SurfaceHolder holder, int format, int width, int height)430     public void surfaceChanged(SurfaceHolder holder,
431             int format,
432             int width,
433             int height) {
434         if (holder == mPreviewView.getHolder()) {
435             if (mState >= CAMERA_OPEN) {
436                 final int previewWidth =
437                         mPreviewSizes.get(mPreviewSize).width;
438                 final int previewHeight =
439                         mPreviewSizes.get(mPreviewSize).height;
440 
441                 if ( Math.abs((float)previewWidth / previewHeight -
442                         (float)width/height) > 0.01f) {
443                     Handler h = new Handler();
444                     h.post(new Runnable() {
445                         @Override
446                         public void run() {
447                             layoutPreview();
448                         }
449                     });
450                 }
451             }
452 
453             if (mPreviewHolder != null || mState == CAMERA_UNINITIALIZED) {
454                 return;
455             }
456             log("Surface holder available: " + width + " x " + height);
457             mPreviewHolder = holder;
458             try {
459                 if (mCamera != null) {
460                     mCamera.setPreviewDisplay(holder);
461                 }
462             } catch (IOException e) {
463                 logE("Unable to set up preview!");
464             }
465         } else if (holder == mCallbackView.getHolder()) {
466             mCallbackHolder = holder;
467         }
468     }
469 
470     @Override
surfaceCreated(SurfaceHolder holder)471     public void surfaceCreated(SurfaceHolder holder) {
472 
473     }
474 
475     @Override
surfaceDestroyed(SurfaceHolder holder)476     public void surfaceDestroyed(SurfaceHolder holder) {
477         mPreviewHolder = null;
478     }
479 
setCameraDisplayOrientation()480     public void setCameraDisplayOrientation() {
481         android.hardware.Camera.CameraInfo info =
482                 new android.hardware.Camera.CameraInfo();
483         android.hardware.Camera.getCameraInfo(mCameraId, info);
484         int rotation = getWindowManager().getDefaultDisplay()
485                 .getRotation();
486         int degrees = 0;
487         switch (rotation) {
488             case Surface.ROTATION_0: degrees = 0; break;
489             case Surface.ROTATION_90: degrees = 90; break;
490             case Surface.ROTATION_180: degrees = 180; break;
491             case Surface.ROTATION_270: degrees = 270; break;
492         }
493 
494         int result;
495         if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
496             result = (info.orientation + degrees) % 360;
497             result = (360 - result) % 360;  // compensate the mirror
498         } else {  // back-facing
499             result = (info.orientation - degrees + 360) % 360;
500         }
501         log(String.format(
502             "Camera sensor orientation %d, UI rotation %d, facing %s. Final orientation %d",
503             info.orientation, rotation,
504             info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT ? "FRONT" : "BACK",
505             result));
506         mCamera.setDisplayOrientation(result);
507     }
508 
509     /** UI controls enable/disable for all open-only controls */
enableOpenOnlyControls(boolean enabled)510     private void enableOpenOnlyControls(boolean enabled) {
511         for (View v : mOpenOnlyControls) {
512                 v.setEnabled(enabled);
513         }
514     }
515 
516     /** UI controls enable/disable for all preview-only controls */
enablePreviewOnlyControls(boolean enabled)517     private void enablePreviewOnlyControls(boolean enabled) {
518         for (View v : mPreviewOnlyControls) {
519                 v.setEnabled(enabled);
520         }
521     }
522 
523     /** UI listeners */
524 
525     private AdapterView.OnItemSelectedListener mCameraSpinnerListener =
526                 new AdapterView.OnItemSelectedListener() {
527         @Override
528         public void onItemSelected(AdapterView<?> parent,
529                         View view, int pos, long id) {
530             int cameraId = pos - 1;
531             if (mCameraId != cameraId) {
532                 resetCamera();
533                 mCameraId = cameraId;
534                 mPreviewToggle.setChecked(false);
535                 setUpCamera();
536             }
537         }
538 
539         @Override
540         public void onNothingSelected(AdapterView<?> parent) {
541 
542         }
543     };
544 
545     private OnClickListener mInfoButtonListener = new OnClickListener() {
546         @Override
547         public void onClick(View v) {
548             if (mCameraId != NO_CAMERA_ID) {
549                 FragmentManager fm = getFragmentManager();
550                 InfoDialogFragment infoDialog = new InfoDialogFragment();
551                 infoDialog.updateInfo(mCameraId, mCamera);
552                 infoDialog.show(fm, "info_dialog_fragment");
553             }
554         }
555     };
556 
557     private AdapterView.OnItemSelectedListener mPreviewSizeListener =
558         new AdapterView.OnItemSelectedListener() {
559         @Override
560         public void onItemSelected(AdapterView<?> parent,
561                 View view, int pos, long id) {
562             if (pos == mPreviewSize) return;
563             if (mState == CAMERA_PREVIEW) {
564                 log("Stopping preview and callbacks to switch resolutions");
565                 stopCallbacks();
566                 mCamera.stopPreview();
567             }
568 
569             mPreviewSize = pos;
570             int width = mPreviewSizes.get(mPreviewSize).width;
571             int height = mPreviewSizes.get(mPreviewSize).height;
572             mParams.setPreviewSize(width, height);
573 
574             log("Setting preview size to " + width + "x" + height);
575 
576             mCamera.setParameters(mParams);
577             resizePreview();
578 
579             if (mState == CAMERA_PREVIEW) {
580                 log("Restarting preview");
581                 mCamera.startPreview();
582             }
583         }
584 
585         @Override
586         public void onNothingSelected(AdapterView<?> parent) {
587 
588         }
589     };
590 
591     private AdapterView.OnItemSelectedListener mPreviewFrameRateListener =
592                 new AdapterView.OnItemSelectedListener() {
593         @Override
594         public void onItemSelected(AdapterView<?> parent,
595                         View view, int pos, long id) {
596             if (pos == mPreviewFrameRate) return;
597             mPreviewFrameRate = pos;
598             mParams.setPreviewFrameRate(mPreviewFrameRates.get(mPreviewFrameRate));
599 
600             log("Setting preview frame rate to " + ((TextView)view).getText());
601 
602             mCamera.setParameters(mParams);
603         }
604 
605         @Override
606         public void onNothingSelected(AdapterView<?> parent) {
607 
608         }
609     };
610 
611     private View.OnClickListener mHDRToggleListener =
612             new View.OnClickListener() {
613         @Override
614         public void onClick(View v) {
615             if (mState == CAMERA_TAKE_PICTURE) {
616                 logE("Can't change preview state while taking picture!");
617                 return;
618             }
619 
620             if (mHDRToggle.isChecked()) {
621                 log("Turning on HDR");
622                 mParams.setSceneMode(Camera.Parameters.SCENE_MODE_HDR);
623             } else {
624                 log("Turning off HDR");
625                 mParams.setSceneMode(Camera.Parameters.SCENE_MODE_AUTO);
626             }
627             mCamera.setParameters(mParams);
628         }
629     };
630 
631     private View.OnClickListener mPreviewToggleListener =
632             new View.OnClickListener() {
633         @Override
634         public void onClick(View v) {
635             if (mState == CAMERA_TAKE_PICTURE) {
636                 logE("Can't change preview state while taking picture!");
637                 return;
638             }
639             if (mPreviewToggle.isChecked()) {
640                 log("Starting preview");
641                 mCamera.startPreview();
642                 mState = CAMERA_PREVIEW;
643                 enablePreviewOnlyControls(true);
644             } else {
645                 log("Stopping preview");
646                 mCamera.stopPreview();
647                 mState = CAMERA_OPEN;
648 
649                 enablePreviewOnlyControls(false);
650             }
651         }
652     };
653 
654     private OnItemSelectedListener mAutofocusModeListener =
655                 new OnItemSelectedListener() {
656         @Override
657         public void onItemSelected(AdapterView<?> parent,
658                         View view, int pos, long id) {
659             if (pos == mAfMode) return;
660 
661             mAfMode = pos;
662             String focusMode = mAfModes.get(mAfMode);
663             log("Setting focus mode to " + focusMode);
664             if (focusMode == Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE ||
665                         focusMode == Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO) {
666                 mCamera.setAutoFocusMoveCallback(mAutofocusMoveCallback);
667             }
668             mParams.setFocusMode(focusMode);
669 
670             mCamera.setParameters(mParams);
671         }
672 
673         @Override
674         public void onNothingSelected(AdapterView<?> arg0) {
675 
676         }
677     };
678 
679     private OnClickListener mAutofocusButtonListener =
680             new View.OnClickListener() {
681         @Override
682         public void onClick(View v) {
683             log("Triggering autofocus");
684             mCamera.autoFocus(mAutofocusCallback);
685         }
686     };
687 
688     private OnClickListener mCancelAutofocusButtonListener =
689             new View.OnClickListener() {
690         @Override
691         public void onClick(View v) {
692             log("Cancelling autofocus");
693             mCamera.cancelAutoFocus();
694         }
695     };
696 
697     private Camera.AutoFocusCallback mAutofocusCallback =
698             new Camera.AutoFocusCallback() {
699         @Override
700         public void onAutoFocus(boolean success, Camera camera) {
701             log("Autofocus completed: " + (success ? "success" : "failure") );
702         }
703     };
704 
705     private Camera.AutoFocusMoveCallback mAutofocusMoveCallback =
706             new Camera.AutoFocusMoveCallback() {
707         @Override
708         public void onAutoFocusMoving(boolean start, Camera camera) {
709             log("Autofocus movement: " + (start ? "starting" : "stopped") );
710         }
711     };
712 
713     private OnItemSelectedListener mFlashModeListener =
714                 new OnItemSelectedListener() {
715         @Override
716         public void onItemSelected(AdapterView<?> parent,
717                         View view, int pos, long id) {
718             if (pos == mFlashMode) return;
719 
720             mFlashMode = pos;
721             String flashMode = mFlashModes.get(mFlashMode);
722             log("Setting flash mode to " + flashMode);
723             mParams.setFlashMode(flashMode);
724             mCamera.setParameters(mParams);
725         }
726 
727         @Override
728         public void onNothingSelected(AdapterView<?> arg0) {
729 
730         }
731     };
732 
733 
734     private AdapterView.OnItemSelectedListener mSnapshotSizeListener =
735             new AdapterView.OnItemSelectedListener() {
736         @Override
737         public void onItemSelected(AdapterView<?> parent,
738                 View view, int pos, long id) {
739             if (pos == mSnapshotSize) return;
740 
741             mSnapshotSize = pos;
742             int width = mSnapshotSizes.get(mSnapshotSize).width;
743             int height = mSnapshotSizes.get(mSnapshotSize).height;
744             log("Setting snapshot size to " + width + " x " + height);
745 
746             mParams.setPictureSize(width, height);
747 
748             mCamera.setParameters(mParams);
749         }
750 
751         @Override
752         public void onNothingSelected(AdapterView<?> parent) {
753 
754         }
755     };
756 
757     private View.OnClickListener mTakePictureListener =
758             new View.OnClickListener() {
759         @Override
760         public void onClick(View v) {
761             log("Taking picture");
762             if (mState == CAMERA_PREVIEW) {
763                 mState = CAMERA_TAKE_PICTURE;
764                 enablePreviewOnlyControls(false);
765                 mPreviewToggle.setChecked(false);
766 
767                 mCamera.takePicture(mShutterCb, mRawCb, mPostviewCb, mJpegCb);
768             } else {
769                 logE("Can't take picture while not running preview!");
770             }
771         }
772     };
773 
774     private AdapterView.OnItemSelectedListener mCamcorderProfileListener =
775                 new AdapterView.OnItemSelectedListener() {
776         @Override
777         public void onItemSelected(AdapterView<?> parent,
778                         View view, int pos, long id) {
779             if (pos != mCamcorderProfile) {
780                 log("Setting camcorder profile to " + ((TextView)view).getText());
781                 mCamcorderProfile = pos;
782             }
783 
784             // Additionally change video recording size to match
785             mVideoRecordSize = 0; // "default", in case it's not found
786             int width = mCamcorderProfiles.get(pos).videoFrameWidth;
787             int height = mCamcorderProfiles.get(pos).videoFrameHeight;
788             for (int i = 0; i < mVideoRecordSizes.size(); i++) {
789                 Camera.Size s = mVideoRecordSizes.get(i);
790                 if (width == s.width && height == s.height) {
791                     mVideoRecordSize = i;
792                     break;
793                 }
794             }
795             log("Setting video record size to " + mVideoRecordSize);
796             mVideoRecordSizeSpinner.setSelection(mVideoRecordSize);
797         }
798 
799         @Override
800         public void onNothingSelected(AdapterView<?> parent) {
801 
802         }
803     };
804 
805     private AdapterView.OnItemSelectedListener mVideoRecordSizeListener =
806                 new AdapterView.OnItemSelectedListener() {
807         @Override
808         public void onItemSelected(AdapterView<?> parent,
809                         View view, int pos, long id) {
810             if (pos == mVideoRecordSize) return;
811 
812             log("Setting video record size to " + ((TextView)view).getText());
813             mVideoRecordSize = pos;
814         }
815 
816         @Override
817         public void onNothingSelected(AdapterView<?> parent) {
818 
819         }
820     };
821 
822     private AdapterView.OnItemSelectedListener mVideoFrameRateListener =
823                 new AdapterView.OnItemSelectedListener() {
824         @Override
825         public void onItemSelected(AdapterView<?> parent,
826                         View view, int pos, long id) {
827             if (pos == mVideoFrameRate) return;
828 
829             log("Setting video frame rate to " + ((TextView)view).getText());
830             mVideoFrameRate = pos;
831         }
832 
833         @Override
834         public void onNothingSelected(AdapterView<?> parent) {
835 
836         }
837     };
838 
839     private View.OnClickListener mRecordToggleListener =
840             new View.OnClickListener() {
841         @Override
842         public void onClick(View v) {
843             if (!mLockCameraToggle.isChecked()) {
844                 logE("Re-lock camera before recording");
845                 return;
846             }
847 
848             mPreviewToggle.setEnabled(false);
849             if (mState == CAMERA_PREVIEW) {
850                 startRecording();
851             } else if (mState == CAMERA_RECORD) {
852                 stopRecording(false);
853             } else {
854                 logE("Can't toggle recording in current state!");
855             }
856             mPreviewToggle.setEnabled(true);
857         }
858     };
859 
860     private View.OnClickListener mRecordStabilizationToggleListener =
861             new View.OnClickListener() {
862         @Override
863         public void onClick(View v) {
864             boolean on = ((ToggleButton) v).isChecked();
865             mParams.setVideoStabilization(on);
866 
867             mCamera.setParameters(mParams);
868         }
869     };
870 
871     private View.OnClickListener mRecordHintToggleListener =
872             new View.OnClickListener() {
873         @Override
874         public void onClick(View v) {
875             boolean on = ((ToggleButton) v).isChecked();
876             mParams.setRecordingHint(on);
877 
878             mCamera.setParameters(mParams);
879         }
880     };
881 
882     private View.OnClickListener mLockCameraToggleListener =
883             new View.OnClickListener() {
884         @Override
885         public void onClick(View v) {
886 
887             if (mState == CAMERA_RECORD) {
888                 logE("Stop recording before toggling lock");
889                 return;
890             }
891 
892             boolean on = ((ToggleButton) v).isChecked();
893 
894             if (on) {
895                 mCamera.lock();
896                 log("Locked camera");
897             } else {
898                 mCamera.unlock();
899                 log("Unlocked camera");
900             }
901         }
902     };
903 
904     private Camera.ShutterCallback mShutterCb = new Camera.ShutterCallback() {
905         @Override
906         public void onShutter() {
907             log("Shutter callback received");
908         }
909     };
910 
911     private Camera.PictureCallback mRawCb = new Camera.PictureCallback() {
912         @Override
913         public void onPictureTaken(byte[] data, Camera camera) {
914             log("Raw callback received");
915         }
916     };
917 
918     private Camera.PictureCallback mPostviewCb = new Camera.PictureCallback() {
919         @Override
920         public void onPictureTaken(byte[] data, Camera camera) {
921             log("Postview callback received");
922         }
923     };
924 
925     private Camera.PictureCallback mJpegCb = new Camera.PictureCallback() {
926         @Override
927         public void onPictureTaken(byte[] data, Camera camera) {
928             log("JPEG picture callback received");
929             FragmentManager fm = getFragmentManager();
930             mSnapshotDialog = new SnapshotDialogFragment();
931 
932             mSnapshotDialog.updateImage(data);
933             mSnapshotDialog.show(fm, "snapshot_dialog_fragment");
934 
935             mPreviewToggle.setEnabled(true);
936 
937             mState = CAMERA_OPEN;
938         }
939     };
940 
941     private AdapterView.OnItemSelectedListener mCallbackFormatListener =
942             new AdapterView.OnItemSelectedListener() {
943         public void onItemSelected(AdapterView<?> parent,
944                         View view, int pos, long id) {
945             mPreviewFormat = pos;
946 
947             log("Setting preview format to " +
948                     mFormatNames.get(mPreviewFormats.get(mPreviewFormat)));
949 
950             switch (mState) {
951             case CAMERA_UNINITIALIZED:
952                 return;
953             case CAMERA_OPEN:
954                 break;
955             case CAMERA_PREVIEW:
956                 if (mCallbacksEnabled) {
957                     log("Stopping preview and callbacks to switch formats");
958                     stopCallbacks();
959                     mCamera.stopPreview();
960                 }
961                 break;
962             case CAMERA_RECORD:
963                 logE("Can't update format while recording active");
964                 return;
965             }
966 
967             mParams.setPreviewFormat(mPreviewFormats.get(mPreviewFormat));
968             mCamera.setParameters(mParams);
969 
970             if (mCallbacksEnabled) {
971                 if (mState == CAMERA_PREVIEW) {
972                     mCamera.startPreview();
973                 }
974             }
975 
976             configureCallbacks(mCallbackView.getWidth(), mCallbackView.getHeight());
977         }
978 
979         public void onNothingSelected(AdapterView<?> parent) {
980 
981         }
982     };
983 
984     private View.OnClickListener mCallbackToggleListener =
985                 new View.OnClickListener() {
986         public void onClick(View v) {
987             if (mCallbacksEnabled) {
988                 log("Disabling preview callbacks");
989                 stopCallbacks();
990                 mCallbacksEnabled = false;
991                 resizePreview();
992                 mCallbackView.setVisibility(View.GONE);
993 
994             } else {
995                 log("Enabling preview callbacks");
996                 mCallbacksEnabled = true;
997                 resizePreview();
998                 mCallbackView.setVisibility(View.VISIBLE);
999             }
1000         }
1001     };
1002 
1003 
1004     // Internal methods
1005 
setUpCamera()1006     void setUpCamera() {
1007         if (mCameraId == NO_CAMERA_ID) return;
1008 
1009         log("Setting up camera " + mCameraId);
1010         logIndent(1);
1011 
1012         if (mState < CAMERA_OPEN) {
1013             log("Opening camera " + mCameraId);
1014 
1015             if (checkSelfPermission(Manifest.permission.CAMERA)
1016                     != PackageManager.PERMISSION_GRANTED) {
1017                 log("Requested camera permission");
1018                 requestPermissions(new String[] {Manifest.permission.CAMERA},
1019                         PERMISSIONS_REQUEST_CAMERA);
1020                 return;
1021             }
1022 
1023 
1024             try {
1025                 mCamera = Camera.open(mCameraId);
1026             } catch (RuntimeException e) {
1027                 logE("Exception opening camera: " + e.getMessage());
1028                 resetCamera();
1029                 mCameraSpinner.setSelection(0);
1030                 logIndent(-1);
1031                 return;
1032             }
1033             mState = CAMERA_OPEN;
1034         }
1035 
1036         mCamera.setErrorCallback(this);
1037 
1038         setCameraDisplayOrientation();
1039         mParams = mCamera.getParameters();
1040         mHDRToggle.setEnabled(false);
1041         if (mParams != null) {
1042             List<String> sceneModes = mParams.getSupportedSceneModes();
1043             if (sceneModes != null) {
1044                 for (String mode : sceneModes) {
1045                     if (Camera.Parameters.SCENE_MODE_HDR.equals(mode)){
1046                         mHDRToggle.setEnabled(true);
1047                         break;
1048                     }
1049                 }
1050             } else {
1051                 Log.i(TAG, "Supported scene modes is null");
1052             }
1053         }
1054 
1055         // Set up preview size selection
1056 
1057         log("Configuring camera");
1058         logIndent(1);
1059 
1060         updatePreviewSizes(mParams);
1061         updatePreviewFrameRate(mCameraId);
1062         updatePreviewFormats(mParams);
1063         updateAfModes(mParams);
1064         updateFlashModes(mParams);
1065         updateSnapshotSizes(mParams);
1066         updateCamcorderProfile(mCameraId);
1067         updateVideoRecordSize(mCameraId);
1068         updateVideoFrameRate(mCameraId);
1069         updateColorEffects(mParams);
1070 
1071         // Trigger updating video record size to match camcorder profile
1072         if (mCamcorderProfile >= 0) {
1073             mCamcorderProfileSpinner.setSelection(mCamcorderProfile);
1074         }
1075 
1076         if (mParams.isVideoStabilizationSupported()) {
1077             log("Video stabilization is supported");
1078             mRecordStabilizationToggle.setEnabled(true);
1079         } else {
1080             log("Video stabilization not supported");
1081             mRecordStabilizationToggle.setEnabled(false);
1082         }
1083 
1084         if (mParams.isAutoExposureLockSupported()) {
1085             log("Auto-Exposure locking is supported");
1086             mExposureLockToggle.setEnabled(true);
1087         } else {
1088             log("Auto-Exposure locking is not supported");
1089             mExposureLockToggle.setEnabled(false);
1090         }
1091 
1092         if (mParams.isZoomSupported()) {
1093             int maxZoom = mParams.getMaxZoom();
1094             mZoomSeekBar.setMax(maxZoom);
1095             log("Zoom is supported, set max to " + maxZoom);
1096             mZoomSeekBar.setEnabled(true);
1097         } else {
1098             log("Zoom is not supported");
1099             mZoomSeekBar.setEnabled(false);
1100         }
1101 
1102         // Update parameters based on above updates
1103         mCamera.setParameters(mParams);
1104 
1105         if (mPreviewHolder != null) {
1106             log("Setting preview display");
1107             try {
1108                 mCamera.setPreviewDisplay(mPreviewHolder);
1109             } catch(IOException e) {
1110                 Log.e(TAG, "Unable to set up preview!");
1111             }
1112         }
1113 
1114         logIndent(-1);
1115 
1116         enableOpenOnlyControls(true);
1117 
1118         resizePreview();
1119         if (mPreviewToggle.isChecked()) {
1120             log("Starting preview" );
1121             mCamera.startPreview();
1122             mState = CAMERA_PREVIEW;
1123         } else {
1124             mState = CAMERA_OPEN;
1125             enablePreviewOnlyControls(false);
1126         }
1127         logIndent(-1);
1128     }
1129 
resetCamera()1130     private void resetCamera() {
1131         if (mState >= CAMERA_OPEN) {
1132             log("Closing old camera");
1133             mCamera.release();
1134         }
1135         mCamera = null;
1136         mCameraId = NO_CAMERA_ID;
1137         mState = CAMERA_UNINITIALIZED;
1138 
1139         enableOpenOnlyControls(false);
1140     }
1141 
updateAfModes(Parameters params)1142     private void updateAfModes(Parameters params) {
1143         mAfModes = params.getSupportedFocusModes();
1144 
1145         mAutofocusModeSpinner.setAdapter(
1146                 new ArrayAdapter<String>(this, R.layout.spinner_item,
1147                         mAfModes.toArray(new String[0])));
1148 
1149         mAfMode = 0;
1150 
1151         params.setFocusMode(mAfModes.get(mAfMode));
1152 
1153         log("Setting AF mode to " + mAfModes.get(mAfMode));
1154     }
1155 
updateFlashModes(Parameters params)1156     private void updateFlashModes(Parameters params) {
1157         mFlashModes = params.getSupportedFlashModes();
1158 
1159         if (mFlashModes != null) {
1160             mFlashModeSpinnerLabel.setVisibility(View.VISIBLE);
1161             mFlashModeSpinner.setVisibility(View.VISIBLE);
1162             mFlashModeSpinner.setAdapter(
1163                     new ArrayAdapter<String>(this, R.layout.spinner_item,
1164                             mFlashModes.toArray(new String[0])));
1165 
1166             mFlashMode = 0;
1167 
1168             params.setFlashMode(mFlashModes.get(mFlashMode));
1169 
1170             log("Setting Flash mode to " + mFlashModes.get(mFlashMode));
1171         } else {
1172             // this camera has no flash
1173             mFlashModeSpinnerLabel.setVisibility(View.GONE);
1174             mFlashModeSpinner.setVisibility(View.GONE);
1175         }
1176     }
1177 
1178     private View.OnClickListener mExposureLockToggleListener =
1179             new View.OnClickListener() {
1180         public void onClick(View v) {
1181             boolean on = ((ToggleButton) v).isChecked();
1182             log("Auto-Exposure was " + mParams.getAutoExposureLock());
1183             mParams.setAutoExposureLock(on);
1184             log("Auto-Exposure is now " + mParams.getAutoExposureLock());
1185         }
1186     };
1187 
1188     private final SeekBar.OnSeekBarChangeListener mZoomSeekBarListener =
1189             new SeekBar.OnSeekBarChangeListener() {
1190         @Override
1191         public void onProgressChanged(SeekBar seekBar, int progress,
1192                 boolean fromUser) {
1193             mZoom = progress;
1194             mParams.setZoom(mZoom);
1195             mCamera.setParameters(mParams);
1196         }
1197         @Override
1198         public void onStartTrackingTouch(SeekBar seekBar) { }
1199         @Override
1200         public void onStopTrackingTouch(SeekBar seekBar) {
1201             log("Zoom set to " + mZoom + " / " + mParams.getMaxZoom() + " (" +
1202                     ((float)(mParams.getZoomRatios().get(mZoom))/100) + "x)");
1203         }
1204     };
1205 
updatePreviewSizes(Camera.Parameters params)1206     private void updatePreviewSizes(Camera.Parameters params) {
1207         mPreviewSizes = params.getSupportedPreviewSizes();
1208 
1209         String[] availableSizeNames = new String[mPreviewSizes.size()];
1210         int i = 0;
1211         for (Camera.Size previewSize: mPreviewSizes) {
1212             availableSizeNames[i++] =
1213                 Integer.toString(previewSize.width) + " x " +
1214                 Integer.toString(previewSize.height);
1215         }
1216         mPreviewSizeSpinner.setAdapter(
1217                 new ArrayAdapter<String>(
1218                         this, R.layout.spinner_item, availableSizeNames));
1219 
1220         mPreviewSize = 0;
1221 
1222         int width = mPreviewSizes.get(mPreviewSize).width;
1223         int height = mPreviewSizes.get(mPreviewSize).height;
1224         params.setPreviewSize(width, height);
1225         log("Setting preview size to " + width + " x " + height);
1226     }
1227 
updatePreviewFrameRate(int cameraId)1228     private void updatePreviewFrameRate(int cameraId) {
1229         List<Integer> frameRates = mParams.getSupportedPreviewFrameRates();
1230         int defaultPreviewFrameRate = mParams.getPreviewFrameRate();
1231 
1232         List<String> frameRateStrings = new ArrayList<String>();
1233         mPreviewFrameRates = new ArrayList<Integer>();
1234 
1235         int currentIndex = 0;
1236         for (Integer frameRate : frameRates) {
1237             mPreviewFrameRates.add(frameRate);
1238             if(frameRate == defaultPreviewFrameRate) {
1239                 frameRateStrings.add(frameRate.toString() + " (Default)");
1240                 mPreviewFrameRate = currentIndex;
1241             } else {
1242                 frameRateStrings.add(frameRate.toString());
1243             }
1244             currentIndex++;
1245         }
1246 
1247         String[] nameArray = (String[])frameRateStrings.toArray(new String[0]);
1248         mPreviewFrameRateSpinner.setAdapter(
1249                 new ArrayAdapter<String>(
1250                         this, R.layout.spinner_item, nameArray));
1251 
1252         mPreviewFrameRateSpinner.setSelection(mPreviewFrameRate);
1253         log("Setting preview frame rate to " + nameArray[mPreviewFrameRate]);
1254     }
1255 
updatePreviewFormats(Camera.Parameters params)1256     private void updatePreviewFormats(Camera.Parameters params) {
1257         mPreviewFormats = params.getSupportedPreviewFormats();
1258 
1259         String[] availableFormatNames = new String[mPreviewFormats.size()];
1260         int i = 0;
1261         for (Integer previewFormat: mPreviewFormats) {
1262             availableFormatNames[i++] = mFormatNames.get(previewFormat);
1263         }
1264         mCallbackFormatSpinner.setAdapter(
1265                 new ArrayAdapter<String>(
1266                         this, R.layout.spinner_item, availableFormatNames));
1267 
1268         mPreviewFormat = 0;
1269         mCallbacksEnabled = false;
1270         mCallbackToggle.setChecked(false);
1271         mCallbackView.setVisibility(View.GONE);
1272 
1273         params.setPreviewFormat(mPreviewFormats.get(mPreviewFormat));
1274         log("Setting preview format to " +
1275                 mFormatNames.get(mPreviewFormats.get(mPreviewFormat)));
1276     }
1277 
updateSnapshotSizes(Camera.Parameters params)1278     private void updateSnapshotSizes(Camera.Parameters params) {
1279         String[] availableSizeNames;
1280         mSnapshotSizes = params.getSupportedPictureSizes();
1281 
1282         availableSizeNames = new String[mSnapshotSizes.size()];
1283         int i = 0;
1284         for (Camera.Size snapshotSize : mSnapshotSizes) {
1285             availableSizeNames[i++] =
1286                 Integer.toString(snapshotSize.width) + " x " +
1287                 Integer.toString(snapshotSize.height);
1288         }
1289         mSnapshotSizeSpinner.setAdapter(
1290                 new ArrayAdapter<String>(
1291                         this, R.layout.spinner_item, availableSizeNames));
1292 
1293         mSnapshotSize = 0;
1294 
1295         int snapshotWidth = mSnapshotSizes.get(mSnapshotSize).width;
1296         int snapshotHeight = mSnapshotSizes.get(mSnapshotSize).height;
1297         params.setPictureSize(snapshotWidth, snapshotHeight);
1298         log("Setting snapshot size to " + snapshotWidth + " x " + snapshotHeight);
1299     }
1300 
updateCamcorderProfile(int cameraId)1301     private void updateCamcorderProfile(int cameraId) {
1302         // Have to query all of these individually,
1303         final int PROFILES[] = new int[] {
1304             CamcorderProfile.QUALITY_2160P,
1305             CamcorderProfile.QUALITY_1080P,
1306             CamcorderProfile.QUALITY_480P,
1307             CamcorderProfile.QUALITY_720P,
1308             CamcorderProfile.QUALITY_CIF,
1309             CamcorderProfile.QUALITY_HIGH,
1310             CamcorderProfile.QUALITY_LOW,
1311             CamcorderProfile.QUALITY_QCIF,
1312             CamcorderProfile.QUALITY_QVGA,
1313             CamcorderProfile.QUALITY_TIME_LAPSE_2160P,
1314             CamcorderProfile.QUALITY_TIME_LAPSE_1080P,
1315             CamcorderProfile.QUALITY_TIME_LAPSE_480P,
1316             CamcorderProfile.QUALITY_TIME_LAPSE_720P,
1317             CamcorderProfile.QUALITY_TIME_LAPSE_CIF,
1318             CamcorderProfile.QUALITY_TIME_LAPSE_HIGH,
1319             CamcorderProfile.QUALITY_TIME_LAPSE_LOW,
1320             CamcorderProfile.QUALITY_TIME_LAPSE_QCIF,
1321             CamcorderProfile.QUALITY_TIME_LAPSE_QVGA
1322         };
1323 
1324         final String PROFILE_NAMES[] = new String[] {
1325             "2160P",
1326             "1080P",
1327             "480P",
1328             "720P",
1329             "CIF",
1330             "HIGH",
1331             "LOW",
1332             "QCIF",
1333             "QVGA",
1334             "TIME_LAPSE_2160P",
1335             "TIME_LAPSE_1080P",
1336             "TIME_LAPSE_480P",
1337             "TIME_LAPSE_720P",
1338             "TIME_LAPSE_CIF",
1339             "TIME_LAPSE_HIGH",
1340             "TIME_LAPSE_LOW",
1341             "TIME_LAPSE_QCIF",
1342             "TIME_LAPSE_QVGA"
1343         };
1344 
1345         List<String> availableCamcorderProfileNames = new ArrayList<String>();
1346         mCamcorderProfiles = new ArrayList<CamcorderProfile>();
1347 
1348         for (int i = 0; i < PROFILES.length; i++) {
1349             if (CamcorderProfile.hasProfile(cameraId, PROFILES[i])) {
1350                 availableCamcorderProfileNames.add(PROFILE_NAMES[i]);
1351                 mCamcorderProfiles.add(CamcorderProfile.get(cameraId, PROFILES[i]));
1352             }
1353         }
1354 
1355         String[] nameArray = (String[])availableCamcorderProfileNames.toArray(new String[0]);
1356         mCamcorderProfileSpinner.setAdapter(
1357                 new ArrayAdapter<String>(
1358                         this, R.layout.spinner_item, nameArray));
1359 
1360         if (availableCamcorderProfileNames.size() == 0) {
1361             log("Camera " +  cameraId + " doesn't support camcorder profile");
1362             mCamcorderProfile = -1;
1363             return;
1364         }
1365 
1366         mCamcorderProfile = 0;
1367         log("Setting camcorder profile to " + nameArray[mCamcorderProfile]);
1368 
1369     }
1370 
updateVideoRecordSize(int cameraId)1371     private void updateVideoRecordSize(int cameraId) {
1372         List<Camera.Size> videoSizes = mParams.getSupportedVideoSizes();
1373         if (videoSizes == null) { // TODO: surface this to the user
1374             log("Failed to get video size list, using preview sizes instead");
1375             videoSizes = mParams.getSupportedPreviewSizes();
1376         }
1377 
1378         List<String> availableVideoRecordSizes = new ArrayList<String>();
1379         mVideoRecordSizes = new ArrayList<Camera.Size>();
1380 
1381         availableVideoRecordSizes.add("Default");
1382         mVideoRecordSizes.add(mCamera.new Size(0,0));
1383 
1384         for (Camera.Size s : videoSizes) {
1385               availableVideoRecordSizes.add(s.width + "x" + s.height);
1386               mVideoRecordSizes.add(s);
1387         }
1388         String[] nameArray = (String[])availableVideoRecordSizes.toArray(new String[0]);
1389         mVideoRecordSizeSpinner.setAdapter(
1390                 new ArrayAdapter<String>(
1391                         this, R.layout.spinner_item, nameArray));
1392 
1393         mVideoRecordSize = 0;
1394         log("Setting video record profile to " + nameArray[mVideoRecordSize]);
1395     }
1396 
updateVideoFrameRate(int cameraId)1397     private void updateVideoFrameRate(int cameraId) {
1398         // Use preview framerates as video framerates
1399         List<Integer> frameRates = mParams.getSupportedPreviewFrameRates();
1400 
1401         List<String> frameRateStrings = new ArrayList<String>();
1402         mVideoFrameRates = new ArrayList<Integer>();
1403 
1404         frameRateStrings.add("Default");
1405         mVideoFrameRates.add(0);
1406 
1407         for (Integer frameRate : frameRates) {
1408             frameRateStrings.add(frameRate.toString());
1409             mVideoFrameRates.add(frameRate);
1410         }
1411         String[] nameArray = (String[])frameRateStrings.toArray(new String[0]);
1412         mVideoFrameRateSpinner.setAdapter(
1413                 new ArrayAdapter<String>(
1414                         this, R.layout.spinner_item, nameArray));
1415 
1416         mVideoFrameRate = 0;
1417         log("Setting recording frame rate to " + nameArray[mVideoFrameRate]);
1418     }
1419 
resizePreview()1420     void resizePreview() {
1421         // Reset preview layout parameters, to trigger layout pass
1422         // This will eventually call layoutPreview below
1423         Resources res = getResources();
1424         mPreviewView.setLayoutParams(
1425                 new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, 0,
1426                         mCallbacksEnabled ?
1427                         res.getInteger(R.integer.preview_with_callback_weight):
1428                         res.getInteger(R.integer.preview_only_weight) ));
1429     }
1430 
layoutPreview()1431     void layoutPreview() {
1432         int rotation = getWindowManager().getDefaultDisplay().getRotation();
1433         int width = mPreviewSizes.get(mPreviewSize).width;
1434         int height = mPreviewSizes.get(mPreviewSize).height;
1435         switch (rotation) {
1436             case Surface.ROTATION_0:
1437             case Surface.ROTATION_180:
1438                 // Portrait
1439                 // Switch the preview size so that the longer edge aligns with the taller
1440                 // dimension.
1441                 if (width > height) {
1442                     int tmp = height;
1443                     height = width;
1444                     width = tmp;
1445                 }
1446                 break;
1447             case Surface.ROTATION_90:
1448             case Surface.ROTATION_270:
1449                 // Landscape
1450                 // Possibly somewhat unlikely case but we should try to handle it too.
1451                 if (height > width) {
1452                     int tmp = height;
1453                     height = width;
1454                     width = tmp;
1455                 }
1456                 break;
1457         }
1458 
1459         float previewAspect = ((float) width) / height;
1460 
1461         int viewHeight = mPreviewView.getHeight();
1462         int viewWidth = mPreviewView.getWidth();
1463         float viewAspect = ((float) viewWidth) / viewHeight;
1464         if ( previewAspect > viewAspect) {
1465             viewHeight = (int) (viewWidth / previewAspect);
1466         } else {
1467             viewWidth = (int) (viewHeight * previewAspect);
1468         }
1469         mPreviewView.setLayoutParams(
1470                 new LayoutParams(viewWidth, viewHeight));
1471         log("Setting layout params viewWidth: " + viewWidth + " viewHeight: " + viewHeight +
1472                 " display rotation: " + rotation);
1473 
1474         if (mCallbacksEnabled) {
1475             int callbackHeight = mCallbackView.getHeight();
1476             int callbackWidth = mCallbackView.getWidth();
1477             float callbackAspect = ((float) callbackWidth) / callbackHeight;
1478             if ( previewAspect > callbackAspect) {
1479                 callbackHeight = (int) (callbackWidth / previewAspect);
1480             } else {
1481                 callbackWidth = (int) (callbackHeight * previewAspect);
1482             }
1483             mCallbackView.setLayoutParams(
1484                     new LayoutParams(callbackWidth, callbackHeight));
1485             configureCallbacks(callbackWidth, callbackHeight);
1486         }
1487     }
1488 
1489 
configureCallbacks(int callbackWidth, int callbackHeight)1490     private void configureCallbacks(int callbackWidth, int callbackHeight) {
1491         if (mState >= CAMERA_OPEN && mCallbacksEnabled) {
1492             mCamera.setPreviewCallbackWithBuffer(null);
1493             int width = mPreviewSizes.get(mPreviewSize).width;
1494             int height = mPreviewSizes.get(mPreviewSize).height;
1495             int format = mPreviewFormats.get(mPreviewFormat);
1496 
1497             mCallbackProcessor = new CallbackProcessor(width, height, format,
1498                     getResources(), mCallbackView,
1499                     callbackWidth, callbackHeight, mRS);
1500 
1501             int size = getCallbackBufferSize(width, height, format);
1502             log("Configuring callbacks:" + width + " x " + height +
1503                     " , format " + format);
1504             for (int i = 0; i < CALLBACK_BUFFER_COUNT; i++) {
1505                 mCamera.addCallbackBuffer(new byte[size]);
1506             }
1507             mCamera.setPreviewCallbackWithBuffer(this);
1508         }
1509         mLastCallbackTimestamp = -1;
1510         mCallbackFrameCount = 0;
1511         mCallbackAvgFrameDuration = 30;
1512     }
1513 
stopCallbacks()1514     private void stopCallbacks() {
1515         if (mState >= CAMERA_OPEN) {
1516             mCamera.setPreviewCallbackWithBuffer(null);
1517             if (mCallbackProcessor != null) {
1518                 if (!mCallbackProcessor.stop()) {
1519                     logE("Can't stop preview callback processing!");
1520                 }
1521             }
1522         }
1523     }
1524 
1525     @Override
onPreviewFrame(byte[] data, Camera camera)1526     public void onPreviewFrame(byte[] data, Camera camera) {
1527         long timestamp = SystemClock.elapsedRealtime();
1528         if (mLastCallbackTimestamp != -1) {
1529             long frameDuration = timestamp - mLastCallbackTimestamp;
1530             mCallbackAvgFrameDuration =
1531                     mCallbackAvgFrameDuration * MEAN_FPS_HISTORY_COEFF +
1532                     frameDuration * MEAN_FPS_MEASUREMENT_COEFF;
1533         }
1534         mLastCallbackTimestamp = timestamp;
1535         if (mState < CAMERA_PREVIEW || !mCallbacksEnabled) {
1536             mCamera.addCallbackBuffer(data);
1537             return;
1538         }
1539         mCallbackFrameCount++;
1540         if (mCallbackFrameCount % FPS_REPORTING_PERIOD == 0) {
1541             log("Got " + FPS_REPORTING_PERIOD + " callback frames, fps "
1542                     + 1e3/mCallbackAvgFrameDuration);
1543         }
1544         mCallbackProcessor.displayCallback(data);
1545 
1546         mCamera.addCallbackBuffer(data);
1547     }
1548 
1549     @Override
onError(int error, Camera camera)1550     public void onError(int error, Camera camera) {
1551         String errorName;
1552         switch (error) {
1553         case Camera.CAMERA_ERROR_SERVER_DIED:
1554             errorName = "SERVER_DIED";
1555             break;
1556         case Camera.CAMERA_ERROR_UNKNOWN:
1557             errorName = "UNKNOWN";
1558             break;
1559         default:
1560             errorName = "?";
1561             break;
1562         }
1563         logE("Camera error received: " + errorName + " (" + error + ")" );
1564         logE("Shutting down camera");
1565         resetCamera();
1566         mCameraSpinner.setSelection(0);
1567     }
1568 
1569     static final int MEDIA_TYPE_IMAGE = 0;
1570     static final int MEDIA_TYPE_VIDEO = 1;
1571     @SuppressLint("SimpleDateFormat")
getOutputMediaFile(int type)1572     File getOutputMediaFile(int type){
1573         // To be safe, you should check that the SDCard is mounted
1574         // using Environment.getExternalStorageState() before doing this.
1575 
1576         String state = Environment.getExternalStorageState();
1577         if (!Environment.MEDIA_MOUNTED.equals(state)) {
1578                 return null;
1579         }
1580 
1581         File mediaStorageDir = new File(Environment.getExternalStoragePublicDirectory(
1582                   Environment.DIRECTORY_DCIM), "TestingCamera");
1583         // This location works best if you want the created images to be shared
1584         // between applications and persist after your app has been uninstalled.
1585 
1586         // Create the storage directory if it does not exist
1587         if (! mediaStorageDir.exists()){
1588             if (! mediaStorageDir.mkdirs()){
1589                 logE("Failed to create directory for pictures/video");
1590                 return null;
1591             }
1592         }
1593 
1594         // Create a media file name
1595         String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
1596         File mediaFile;
1597         if (type == MEDIA_TYPE_IMAGE){
1598             mediaFile = new File(mediaStorageDir.getPath() + File.separator +
1599             "IMG_"+ timeStamp + ".jpg");
1600         } else if(type == MEDIA_TYPE_VIDEO) {
1601             mediaFile = new File(mediaStorageDir.getPath() + File.separator +
1602             "VID_"+ timeStamp + ".mp4");
1603         } else {
1604             return null;
1605         }
1606 
1607         return mediaFile;
1608     }
1609 
notifyMediaScannerOfFile(File newFile, final MediaScannerConnection.OnScanCompletedListener listener)1610     void notifyMediaScannerOfFile(File newFile,
1611                 final MediaScannerConnection.OnScanCompletedListener listener) {
1612         final Handler h = new Handler();
1613         MediaScannerConnection.scanFile(this,
1614                 new String[] { newFile.toString() },
1615                 null,
1616                 new MediaScannerConnection.OnScanCompletedListener() {
1617                     @Override
1618                     public void onScanCompleted(final String path, final Uri uri) {
1619                         h.post(new Runnable() {
1620                             @Override
1621                             public void run() {
1622                                 log("MediaScanner notified: " +
1623                                         path + " -> " + uri);
1624                                 if (listener != null)
1625                                     listener.onScanCompleted(path, uri);
1626                             }
1627                         });
1628                     }
1629                 });
1630     }
1631 
deleteFile(File badFile)1632     private void deleteFile(File badFile) {
1633         if (badFile.exists()) {
1634             boolean success = badFile.delete();
1635             if (success) log("Deleted file " + badFile.toString());
1636             else log("Unable to delete file " + badFile.toString());
1637         }
1638     }
1639 
1640     private static final int BIT_RATE_1080P = 16000000;
1641     private static final int BIT_RATE_MIN = 64000;
1642     private static final int BIT_RATE_MAX = 40000000;
1643 
getVideoBitRate(Camera.Size sz)1644     private int getVideoBitRate(Camera.Size sz) {
1645         int rate = BIT_RATE_1080P;
1646         float scaleFactor = sz.height * sz.width / (float)(1920 * 1080);
1647         rate = (int)(rate * scaleFactor);
1648 
1649         // Clamp to the MIN, MAX range.
1650         return Math.max(BIT_RATE_MIN, Math.min(BIT_RATE_MAX, rate));
1651     }
1652 
startRecording()1653     private void startRecording() {
1654         log("Starting recording");
1655 
1656         if  ((checkSelfPermission(Manifest.permission.RECORD_AUDIO)
1657                 != PackageManager.PERMISSION_GRANTED)
1658             || (checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)
1659                 != PackageManager.PERMISSION_GRANTED)) {
1660             log("Requesting recording permissions (audio, storage)");
1661             requestPermissions(new String[] {
1662                     Manifest.permission.RECORD_AUDIO,
1663                     Manifest.permission.WRITE_EXTERNAL_STORAGE},
1664                 PERMISSIONS_REQUEST_RECORDING);
1665             return;
1666         }
1667 
1668         logIndent(1);
1669         log("Configuring MediaRecoder");
1670 
1671         mRecordHandoffCheckBox.setEnabled(false);
1672         if (mRecordHandoffCheckBox.isChecked()) {
1673             mCamera.release();
1674         } else {
1675             mCamera.unlock();
1676         }
1677 
1678         if (mRecorder != null) {
1679             mRecorder.release();
1680         }
1681 
1682         mRecorder = new MediaRecorder();
1683         mRecorder.setOnErrorListener(mRecordingErrorListener);
1684         mRecorder.setOnInfoListener(mRecordingInfoListener);
1685         if (!mRecordHandoffCheckBox.isChecked()) {
1686             mRecorder.setCamera(mCamera);
1687         }
1688         mRecorder.setPreviewDisplay(mPreviewHolder.getSurface());
1689 
1690         mRecorder.setAudioSource(MediaRecorder.AudioSource.CAMCORDER);
1691         mRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA);
1692 
1693         Camera.Size videoRecordSize = mVideoRecordSizes.get(mVideoRecordSize);
1694         if (mCamcorderProfile >= 0) {
1695             mRecorder.setProfile(mCamcorderProfiles.get(mCamcorderProfile));
1696         } else {
1697             mRecorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP);
1698             mRecorder.setVideoEncodingBitRate(getVideoBitRate(videoRecordSize));
1699             mRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264);
1700             mRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
1701         }
1702 
1703         if (videoRecordSize.width > 0 && videoRecordSize.height > 0) {
1704             mRecorder.setVideoSize(videoRecordSize.width, videoRecordSize.height);
1705         }
1706         if (mVideoFrameRates.get(mVideoFrameRate) > 0) {
1707             mRecorder.setVideoFrameRate(mVideoFrameRates.get(mVideoFrameRate));
1708         }
1709         File outputFile = getOutputMediaFile(MEDIA_TYPE_VIDEO);
1710         log("File name:" + outputFile.toString());
1711         mRecorder.setOutputFile(outputFile.toString());
1712 
1713         boolean ready = false;
1714         log("Preparing MediaRecorder");
1715         try {
1716             mRecorder.prepare();
1717             ready = true;
1718         } catch (Exception e) {
1719             StringWriter writer = new StringWriter();
1720             e.printStackTrace(new PrintWriter(writer));
1721             logE("Exception preparing MediaRecorder:\n" + writer.toString());
1722         }
1723 
1724         if (ready) {
1725             try {
1726                 log("Starting MediaRecorder");
1727                 mRecorder.start();
1728                 mState = CAMERA_RECORD;
1729                 log("Recording active");
1730                 mRecordingFile = outputFile;
1731             } catch (Exception e) {
1732                 StringWriter writer = new StringWriter();
1733                 e.printStackTrace(new PrintWriter(writer));
1734                 logE("Exception starting MediaRecorder:\n" + writer.toString());
1735                 ready = false;
1736             }
1737         }
1738 
1739         if (!ready) {
1740             mRecordToggle.setChecked(false);
1741             mRecordHandoffCheckBox.setEnabled(true);
1742 
1743             if (mRecordHandoffCheckBox.isChecked()) {
1744                 mState = CAMERA_UNINITIALIZED;
1745                 setUpCamera();
1746             }
1747         }
1748         logIndent(-1);
1749     }
1750 
1751     private MediaRecorder.OnErrorListener mRecordingErrorListener =
1752             new MediaRecorder.OnErrorListener() {
1753         @Override
1754         public void onError(MediaRecorder mr, int what, int extra) {
1755             logE("MediaRecorder reports error: " + what + ", extra "
1756                     + extra);
1757             if (mState == CAMERA_RECORD) {
1758                 stopRecording(true);
1759             }
1760         }
1761     };
1762 
1763     private MediaRecorder.OnInfoListener mRecordingInfoListener =
1764             new MediaRecorder.OnInfoListener() {
1765         @Override
1766         public void onInfo(MediaRecorder mr, int what, int extra) {
1767             log("MediaRecorder reports info: " + what + ", extra "
1768                     + extra);
1769         }
1770     };
1771 
stopRecording(boolean error)1772     private void stopRecording(boolean error) {
1773         log("Stopping recording");
1774         mRecordHandoffCheckBox.setEnabled(true);
1775         mRecordToggle.setChecked(false);
1776         if (mRecorder != null) {
1777             try {
1778                 mRecorder.stop();
1779             } catch (RuntimeException e) {
1780                 // this can happen if there were no frames received by recorder
1781                 logE("Could not create output file");
1782                 error = true;
1783             }
1784 
1785             if (mRecordHandoffCheckBox.isChecked()) {
1786                 mState = CAMERA_UNINITIALIZED;
1787                 setUpCamera();
1788             } else {
1789                 mCamera.lock();
1790                 mState = CAMERA_PREVIEW;
1791             }
1792 
1793             if (!error) {
1794                 notifyMediaScannerOfFile(mRecordingFile, null);
1795             } else {
1796                 deleteFile(mRecordingFile);
1797             }
1798             mRecordingFile = null;
1799         } else {
1800             logE("Recorder is unexpectedly null!");
1801         }
1802     }
1803 
getCallbackBufferSize(int width, int height, int format)1804     static int getCallbackBufferSize(int width, int height, int format) {
1805         int size = -1;
1806         switch (format) {
1807         case ImageFormat.NV21:
1808             size = width * height * 3 / 2;
1809             break;
1810         case ImageFormat.YV12:
1811             int y_stride = (int) (Math.ceil( width / 16.) * 16);
1812             int y_size = y_stride * height;
1813             int c_stride = (int) (Math.ceil(y_stride / 32.) * 16);
1814             int c_size = c_stride * height/2;
1815             size = y_size + c_size * 2;
1816             break;
1817         case ImageFormat.NV16:
1818         case ImageFormat.RGB_565:
1819         case ImageFormat.YUY2:
1820             size = 2 * width * height;
1821             break;
1822         case ImageFormat.JPEG:
1823             Log.e(TAG, "JPEG callback buffers not supported!");
1824             size = 0;
1825             break;
1826         case ImageFormat.UNKNOWN:
1827             Log.e(TAG, "Unknown-format callback buffers not supported!");
1828             size = 0;
1829             break;
1830         }
1831         return size;
1832     }
1833 
1834     private OnItemSelectedListener mColorEffectListener =
1835                 new OnItemSelectedListener() {
1836         @Override
1837         public void onItemSelected(AdapterView<?> parent,
1838                         View view, int pos, long id) {
1839             if (pos == mColorEffect) return;
1840 
1841             mColorEffect = pos;
1842             String colorEffect = mColorEffects.get(mColorEffect);
1843             log("Setting color effect to " + colorEffect);
1844             mParams.setColorEffect(colorEffect);
1845             mCamera.setParameters(mParams);
1846         }
1847 
1848         @Override
1849         public void onNothingSelected(AdapterView<?> arg0) {
1850         }
1851     };
1852 
updateColorEffects(Parameters params)1853     private void updateColorEffects(Parameters params) {
1854         mColorEffects = params.getSupportedColorEffects();
1855         if (mColorEffects != null) {
1856             mColorEffectSpinnerLabel.setVisibility(View.VISIBLE);
1857             mColorEffectSpinner.setVisibility(View.VISIBLE);
1858             mColorEffectSpinner.setAdapter(
1859                     new ArrayAdapter<String>(this, R.layout.spinner_item,
1860                             mColorEffects.toArray(new String[0])));
1861             mColorEffect = 0;
1862             params.setColorEffect(mColorEffects.get(mColorEffect));
1863             log("Setting Color Effect to " + mColorEffects.get(mColorEffect));
1864         } else {
1865             mColorEffectSpinnerLabel.setVisibility(View.GONE);
1866             mColorEffectSpinner.setVisibility(View.GONE);
1867         }
1868     }
1869 
1870     private int mLogIndentLevel = 0;
1871     private String mLogIndent = "\t";
1872     /** Increment or decrement log indentation level */
logIndent(int delta)1873     synchronized void logIndent(int delta) {
1874         mLogIndentLevel += delta;
1875         if (mLogIndentLevel < 0) mLogIndentLevel = 0;
1876         char[] mLogIndentArray = new char[mLogIndentLevel + 1];
1877         for (int i = -1; i < mLogIndentLevel; i++) {
1878             mLogIndentArray[i + 1] = '\t';
1879         }
1880         mLogIndent = new String(mLogIndentArray);
1881     }
1882 
1883     @SuppressLint("SimpleDateFormat")
1884     SimpleDateFormat mDateFormatter = new SimpleDateFormat("HH:mm:ss.SSS");
1885     /** Log both to log text view and to device logcat */
log(String logLine)1886     void log(String logLine) {
1887         Log.d(TAG, logLine);
1888         logAndScrollToBottom(logLine, mLogIndent);
1889     }
1890 
logE(String logLine)1891     void logE(String logLine) {
1892         Log.e(TAG, logLine);
1893         logAndScrollToBottom(logLine, mLogIndent + "!!! ");
1894     }
1895 
logAndScrollToBottom(String logLine, String logIndent)1896     synchronized private void logAndScrollToBottom(String logLine, String logIndent) {
1897         StringBuffer logEntry = new StringBuffer(32);
1898         logEntry.append("\n").append(mDateFormatter.format(new Date())).append(logIndent);
1899         logEntry.append(logLine);
1900         mLogView.append(logEntry);
1901         final Layout layout = mLogView.getLayout();
1902         if (layout != null){
1903             int scrollDelta = layout.getLineBottom(mLogView.getLineCount() - 1)
1904                 - mLogView.getScrollY() - mLogView.getHeight();
1905             if(scrollDelta > 0) {
1906                 mLogView.scrollBy(0, scrollDelta);
1907             }
1908         }
1909     }
1910 }
1911