1 /*
2  * Copyright (C) 2013 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.testingcamera2.v1;
18 
19 import android.app.Activity;
20 import android.content.res.Configuration;
21 import android.graphics.Bitmap;
22 import android.graphics.BitmapFactory;
23 import android.graphics.ImageFormat;
24 import android.hardware.camera2.CameraCharacteristics;
25 import android.hardware.camera2.CameraCaptureSession;
26 import android.hardware.camera2.CameraDevice;
27 import android.hardware.camera2.CaptureFailure;
28 import android.hardware.camera2.CaptureRequest;
29 import android.hardware.camera2.CaptureResult;
30 import android.hardware.camera2.TotalCaptureResult;
31 import android.media.Image;
32 import android.media.MediaMuxer;
33 import android.os.AsyncTask;
34 import android.os.Bundle;
35 import android.os.Handler;
36 import android.util.Log;
37 import android.util.Range;
38 import android.view.OrientationEventListener;
39 import android.view.SurfaceHolder;
40 import android.view.SurfaceView;
41 import android.view.View;
42 import android.view.ViewGroup.LayoutParams;
43 import android.view.WindowManager;
44 import android.widget.AdapterView;
45 import android.widget.AdapterView.OnItemSelectedListener;
46 import android.widget.ArrayAdapter;
47 import android.widget.Button;
48 import android.widget.CompoundButton;
49 import android.widget.CheckBox;
50 import android.widget.ImageView;
51 import android.widget.RadioGroup;
52 import android.widget.SeekBar;
53 import android.widget.SeekBar.OnSeekBarChangeListener;
54 import android.widget.Spinner;
55 import android.widget.TextView;
56 import android.widget.ToggleButton;
57 
58 import java.nio.ByteBuffer;
59 import java.util.ArrayList;
60 import java.util.Arrays;
61 import java.util.HashSet;
62 import java.util.List;
63 import java.util.Set;
64 
65 import com.android.testingcamera2.R;
66 
67 public class TestingCamera2 extends Activity implements SurfaceHolder.Callback {
68 
69     private static final String TAG = "TestingCamera2";
70     private static final boolean VERBOSE = Log.isLoggable(TAG, Log.VERBOSE);
71     private CameraOps mCameraOps;
72     private static final int mSeekBarMax = 100;
73     private static final long MAX_EXPOSURE = 200000000L; // 200ms
74     private static final long MIN_EXPOSURE = 100000L; // 100us
75     private static final long MAX_FRAME_DURATION = 1000000000L; // 1s
76     // Manual control change step size
77     private static final int STEP_SIZE = 100;
78     // Min and max sensitivity ISO values
79     private static final int MIN_SENSITIVITY = 100;
80     private static final int MAX_SENSITIVITY = 1600;
81     private static final int ORIENTATION_UNINITIALIZED = -1;
82 
83     private int mLastOrientation = ORIENTATION_UNINITIALIZED;
84     private OrientationEventListener mOrientationEventListener;
85     private SurfaceView mPreviewView;
86     private ImageView mStillView;
87 
88     private SurfaceHolder mCurrentPreviewHolder = null;
89 
90     private Button mInfoButton;
91     private Button mFlushButton;
92     private ToggleButton mFocusLockToggle;
93     private Spinner mFocusModeSpinner;
94     private CheckBox mUseMediaCodecCheckBox;
95 
96     private SeekBar mSensitivityBar;
97     private SeekBar mExposureBar;
98     private SeekBar mFrameDurationBar;
99 
100     private TextView mSensitivityInfoView;
101     private TextView mExposureInfoView;
102     private TextView mFrameDurationInfoView;
103     private TextView mCaptureResultView;
104     private ToggleButton mRecordingToggle;
105     private ToggleButton mManualCtrlToggle;
106 
107     private CameraControls mCameraControl = null;
108     private final Set<View> mManualControls = new HashSet<View>();
109     private final Set<View> mAutoControls = new HashSet<View>();
110 
111 
112 
113     Handler mMainHandler;
114     boolean mUseMediaCodec;
115 
116     @Override
onCreate(Bundle savedInstanceState)117     public void onCreate(Bundle savedInstanceState) {
118         super.onCreate(savedInstanceState);
119 
120         getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
121                 WindowManager.LayoutParams.FLAG_FULLSCREEN);
122 
123         setContentView(R.layout.main);
124 
125         mPreviewView = (SurfaceView) findViewById(R.id.preview_view);
126         mPreviewView.getHolder().addCallback(this);
127 
128         mStillView = (ImageView) findViewById(R.id.still_view);
129 
130         mInfoButton  = (Button) findViewById(R.id.info_button);
131         mInfoButton.setOnClickListener(mInfoButtonListener);
132         mFlushButton  = (Button) findViewById(R.id.flush_button);
133         mFlushButton.setOnClickListener(mFlushButtonListener);
134 
135         mFocusLockToggle = (ToggleButton) findViewById(R.id.focus_button);
136         mFocusLockToggle.setOnClickListener(mFocusLockToggleListener);
137         mFocusModeSpinner = (Spinner) findViewById(R.id.focus_mode_spinner);
138         mAutoControls.add(mFocusLockToggle);
139 
140         mRecordingToggle = (ToggleButton) findViewById(R.id.start_recording);
141         mRecordingToggle.setOnClickListener(mRecordingToggleListener);
142         mUseMediaCodecCheckBox = (CheckBox) findViewById(R.id.use_media_codec);
143         mUseMediaCodecCheckBox.setOnCheckedChangeListener(mUseMediaCodecListener);
144         mUseMediaCodecCheckBox.setChecked(mUseMediaCodec);
145 
146         mManualCtrlToggle = (ToggleButton) findViewById(R.id.manual_control);
147         mManualCtrlToggle.setOnClickListener(mControlToggleListener);
148 
149         mSensitivityBar = (SeekBar) findViewById(R.id.sensitivity_seekbar);
150         mSensitivityBar.setOnSeekBarChangeListener(mSensitivitySeekBarListener);
151         mSensitivityBar.setMax(mSeekBarMax);
152         mManualControls.add(mSensitivityBar);
153 
154         mExposureBar = (SeekBar) findViewById(R.id.exposure_time_seekbar);
155         mExposureBar.setOnSeekBarChangeListener(mExposureSeekBarListener);
156         mExposureBar.setMax(mSeekBarMax);
157         mManualControls.add(mExposureBar);
158 
159         mFrameDurationBar = (SeekBar) findViewById(R.id.frame_duration_seekbar);
160         mFrameDurationBar.setOnSeekBarChangeListener(mFrameDurationSeekBarListener);
161         mFrameDurationBar.setMax(mSeekBarMax);
162         mManualControls.add(mFrameDurationBar);
163 
164         mSensitivityInfoView = (TextView) findViewById(R.id.sensitivity_bar_label);
165         mExposureInfoView = (TextView) findViewById(R.id.exposure_time_bar_label);
166         mFrameDurationInfoView = (TextView) findViewById(R.id.frame_duration_bar_label);
167         mCaptureResultView = (TextView) findViewById(R.id.capture_result_info_label);
168 
169         enableManualControls(false);
170         mCameraControl = new CameraControls();
171 
172         // Get UI handler
173         mMainHandler = new Handler();
174 
175         try {
176             mCameraOps = CameraOps.create(this, mCameraOpsListener, mMainHandler);
177         } catch(ApiFailureException e) {
178             logException("Cannot create camera ops!",e);
179         }
180 
181         mOrientationEventListener = new OrientationEventListener(this) {
182             @Override
183             public void onOrientationChanged(int orientation) {
184                 if (orientation == ORIENTATION_UNKNOWN) {
185                     orientation = 0;
186                 }
187                 mCameraOps.updateOrientation(orientation);
188             }
189         };
190         mOrientationEventListener.enable();
191         // Process the initial configuration (for i.e. initial orientation)
192         // We need this because #onConfigurationChanged doesn't get called when the app launches
193         maybeUpdateConfiguration(getResources().getConfiguration());
194     }
195 
196     @Override
onResume()197     public void onResume() {
198         super.onResume();
199         try {
200             if (VERBOSE) Log.v(TAG, String.format("onResume"));
201 
202             mCameraOps.minimalPreviewConfig(mPreviewView.getHolder());
203             mCurrentPreviewHolder = mPreviewView.getHolder();
204         } catch (ApiFailureException e) {
205             logException("Can't configure preview surface: ",e);
206         }
207     }
208 
209     @Override
onPause()210     public void onPause() {
211         super.onPause();
212         try {
213             if (VERBOSE) Log.v(TAG, String.format("onPause"));
214 
215             mCameraOps.closeDevice();
216         } catch (ApiFailureException e) {
217             logException("Can't close device: ",e);
218         }
219         mCurrentPreviewHolder = null;
220     }
221 
222     @Override
onDestroy()223     protected void onDestroy() {
224         mOrientationEventListener.disable();
225         super.onDestroy();
226     }
227 
228     @Override
onConfigurationChanged(Configuration newConfig)229     public void onConfigurationChanged(Configuration newConfig) {
230         super.onConfigurationChanged(newConfig);
231 
232         if (VERBOSE) {
233             Log.v(TAG, String.format("onConfiguredChanged: orientation %x",
234                     newConfig.orientation));
235         }
236 
237         maybeUpdateConfiguration(newConfig);
238     }
239 
maybeUpdateConfiguration(Configuration newConfig)240     private void maybeUpdateConfiguration(Configuration newConfig) {
241         if (VERBOSE) {
242             Log.v(TAG, String.format("handleConfiguration: orientation %x",
243                     newConfig.orientation));
244         }
245 
246         if (mLastOrientation != newConfig.orientation) {
247             mLastOrientation = newConfig.orientation;
248             updatePreviewOrientation();
249         }
250     }
251 
updatePreviewOrientation()252     private void updatePreviewOrientation() {
253         LayoutParams params = mPreviewView.getLayoutParams();
254         int width = params.width;
255         int height = params.height;
256 
257         if (VERBOSE) {
258             Log.v(TAG, String.format(
259                     "onConfiguredChanged: current layout is %dx%d", width,
260                     height));
261         }
262         /**
263          * Force wide aspect ratios for landscape
264          * Force narrow aspect ratios for portrait
265          */
266         if (mLastOrientation == Configuration.ORIENTATION_LANDSCAPE) {
267             if (height > width) {
268                 int tmp = width;
269                 width = height;
270                 height = tmp;
271             }
272         } else if (mLastOrientation == Configuration.ORIENTATION_PORTRAIT) {
273             if (width > height) {
274                 int tmp = width;
275                 width = height;
276                 height = tmp;
277             }
278         }
279 
280         if (width != params.width && height != params.height) {
281             if (VERBOSE) {
282                 Log.v(TAG, String.format(
283                         "onConfiguredChanged: updating preview size to %dx%d", width,
284                         height));
285             }
286             params.width = width;
287             params.height = height;
288 
289             mPreviewView.setLayoutParams(params);
290         }
291     }
292 
293     /** SurfaceHolder.Callback methods */
294     @Override
surfaceChanged(SurfaceHolder holder, int format, int width, int height)295     public void surfaceChanged(SurfaceHolder holder,
296             int format,
297             int width,
298             int height) {
299         if (VERBOSE) {
300             Log.v(TAG, String.format("surfaceChanged: format %x, width %d, height %d", format,
301                     width, height));
302         }
303         if (mCurrentPreviewHolder != null && holder == mCurrentPreviewHolder) {
304             try {
305                 mCameraOps.minimalPreview(holder, mCameraControl);
306             } catch (ApiFailureException e) {
307                 logException("Can't start minimal preview: ", e);
308             }
309         }
310     }
311 
312     @Override
surfaceCreated(SurfaceHolder holder)313     public void surfaceCreated(SurfaceHolder holder) {
314 
315     }
316 
317     @Override
surfaceDestroyed(SurfaceHolder holder)318     public void surfaceDestroyed(SurfaceHolder holder) {
319     }
320 
321     private final Button.OnClickListener mInfoButtonListener = new Button.OnClickListener() {
322         @Override
323         public void onClick(View v) {
324             final Handler uiHandler = new Handler();
325             AsyncTask.execute(new Runnable() {
326                 @Override
327                 public void run() {
328                     try {
329                         mCameraOps.minimalJpegCapture(mCaptureCallback, mCaptureResultListener,
330                                 uiHandler, mCameraControl);
331                         if (mCurrentPreviewHolder != null) {
332                             mCameraOps.minimalPreview(mCurrentPreviewHolder, mCameraControl);
333                         }
334                     } catch (ApiFailureException e) {
335                         logException("Can't take a JPEG! ", e);
336                     }
337                 }
338             });
339         }
340     };
341 
342     private final Button.OnClickListener mFlushButtonListener = new Button.OnClickListener() {
343         @Override
344         public void onClick(View v) {
345             AsyncTask.execute(new Runnable() {
346                 @Override
347                 public void run() {
348                     try {
349                         mCameraOps.flush();
350                     } catch (ApiFailureException e) {
351                         logException("Can't flush!", e);
352                     }
353                 }
354             });
355         }
356     };
357 
358     /**
359      * UI controls enable/disable for all manual controls
360      */
enableManualControls(boolean enabled)361     private void enableManualControls(boolean enabled) {
362         for (View v : mManualControls) {
363             v.setEnabled(enabled);
364         }
365 
366         for (View v : mAutoControls) {
367             v.setEnabled(!enabled);
368         }
369     }
370 
371     private final CameraOps.CaptureCallback mCaptureCallback = new CameraOps.CaptureCallback() {
372         @Override
373         public void onCaptureAvailable(Image capture) {
374             if (capture.getFormat() != ImageFormat.JPEG) {
375                 Log.e(TAG, "Unexpected format: " + capture.getFormat());
376                 return;
377             }
378             ByteBuffer jpegBuffer = capture.getPlanes()[0].getBuffer();
379             byte[] jpegData = new byte[jpegBuffer.capacity()];
380             jpegBuffer.get(jpegData);
381 
382             Bitmap b = BitmapFactory.decodeByteArray(jpegData, 0, jpegData.length);
383             mStillView.setImageBitmap(b);
384         }
385     };
386 
387     // TODO: this callback is not called for each capture, need figure out why.
388     private final CameraOps.CaptureResultListener mCaptureResultListener =
389             new CameraOps.CaptureResultListener() {
390 
391                 @Override
392                 public void onCaptureStarted(CameraCaptureSession session, CaptureRequest request,
393                         long timestamp, long frameNumber) {
394                 }
395 
396                 @Override
397                 public void onCaptureCompleted(
398                         CameraCaptureSession session, CaptureRequest request,
399                         TotalCaptureResult result) {
400                     Log.i(TAG, "Capture result is available");
401                     Integer reqCtrlMode;
402                     Integer resCtrlMode;
403                     if (request == null || result ==null) {
404                         Log.e(TAG, "request/result is invalid");
405                         return;
406                     }
407                     Log.i(TAG, "Capture complete");
408                     final StringBuffer info = new StringBuffer("Capture Result:\n");
409 
410                     reqCtrlMode = request.get(CaptureRequest.CONTROL_MODE);
411                     resCtrlMode = result.get(CaptureResult.CONTROL_MODE);
412                     info.append("Control mode: request " + reqCtrlMode + ". result " + resCtrlMode);
413                     info.append("\n");
414 
415                     Integer reqSen = request.get(CaptureRequest.SENSOR_SENSITIVITY);
416                     Integer resSen = result.get(CaptureResult.SENSOR_SENSITIVITY);
417                     info.append("Sensitivity: request " + reqSen + ". result " + resSen);
418                     info.append("\n");
419 
420                     Long reqExp = request.get(CaptureRequest.SENSOR_EXPOSURE_TIME);
421                     Long resExp = result.get(CaptureResult.SENSOR_EXPOSURE_TIME);
422                     info.append("Exposure: request " + reqExp + ". result " + resExp);
423                     info.append("\n");
424 
425                     Long reqFD = request.get(CaptureRequest.SENSOR_FRAME_DURATION);
426                     Long resFD = result.get(CaptureResult.SENSOR_FRAME_DURATION);
427                     info.append("Frame duration: request " + reqFD + ". result " + resFD);
428                     info.append("\n");
429 
430                     List<CaptureResult.Key<?>> resultKeys = result.getKeys();
431                     if (VERBOSE) {
432                         CaptureResult.Key<?>[] arrayKeys =
433                                 resultKeys.toArray(new CaptureResult.Key<?>[0]);
434                         Log.v(TAG, "onCaptureCompleted - dumping keys: " +
435                                 Arrays.toString(arrayKeys));
436                     }
437                     info.append("Total keys: " + resultKeys.size());
438                     info.append("\n");
439 
440                     if (mMainHandler != null) {
441                         mMainHandler.post (new Runnable() {
442                             @Override
443                             public void run() {
444                                 // Update UI for capture result
445                                 mCaptureResultView.setText(info);
446                             }
447                         });
448                     }
449                 }
450 
451                 @Override
452                 public void onCaptureFailed(CameraCaptureSession session, CaptureRequest request,
453                         CaptureFailure failure) {
454                     Log.e(TAG, "Capture failed");
455                 }
456     };
457 
logException(String msg, Throwable e)458     private void logException(String msg, Throwable e) {
459         Log.e(TAG, msg + Log.getStackTraceString(e));
460     }
461 
getRadioFmt()462     private RadioGroup getRadioFmt() {
463       return (RadioGroup)findViewById(R.id.radio_fmt);
464     }
465 
getOutputFormat()466     private int getOutputFormat() {
467         RadioGroup fmt = getRadioFmt();
468         switch (fmt.getCheckedRadioButtonId()) {
469             case R.id.radio_mp4:
470                 return MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4;
471 
472             case R.id.radio_webm:
473                 return MediaMuxer.OutputFormat.MUXER_OUTPUT_WEBM;
474 
475             default:
476                 throw new IllegalStateException("Checked button unrecognized.");
477         }
478     }
479 
480     private final OnSeekBarChangeListener mSensitivitySeekBarListener =
481             new OnSeekBarChangeListener() {
482 
483               @Override
484               public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
485                   Range<Integer> defaultRange = new Range<Integer>(MIN_SENSITIVITY,
486                           MAX_SENSITIVITY);
487                   CameraCharacteristics properties = mCameraOps.getCameraCharacteristics();
488                   Range<Integer> sensitivityRange = properties.get(
489                           CameraCharacteristics.SENSOR_INFO_SENSITIVITY_RANGE);
490                   if (sensitivityRange == null || sensitivityRange.getLower() > MIN_SENSITIVITY ||
491                           sensitivityRange.getUpper() < MAX_SENSITIVITY) {
492                       Log.e(TAG, "unable to get sensitivity range, use default range");
493                       sensitivityRange = defaultRange;
494                   }
495                   int min = sensitivityRange.getLower();
496                   int max = sensitivityRange.getUpper();
497                   float progressFactor = progress / (float)mSeekBarMax;
498                   int curSensitivity = (int) (min + (max - min) * progressFactor);
499                   curSensitivity = (curSensitivity / STEP_SIZE ) * STEP_SIZE;
500                   mCameraControl.getManualControls().setSensitivity(curSensitivity);
501                   // Update the sensitivity info
502                   StringBuffer info = new StringBuffer("Sensitivity(ISO):");
503                   info.append("" + curSensitivity);
504                   mSensitivityInfoView.setText(info);
505                   mCameraOps.updatePreview(mCameraControl);
506               }
507 
508               @Override
509               public void onStartTrackingTouch(SeekBar seekBar) {
510               }
511 
512               @Override
513               public void onStopTrackingTouch(SeekBar seekBar) {
514               }
515     };
516 
517     private final OnSeekBarChangeListener mExposureSeekBarListener =
518             new OnSeekBarChangeListener() {
519 
520               @Override
521               public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
522                   Range<Long> defaultRange = new Range<Long>(MIN_EXPOSURE, MAX_EXPOSURE);
523                   CameraCharacteristics properties = mCameraOps.getCameraCharacteristics();
524                   Range<Long> exposureRange = properties.get(
525                           CameraCharacteristics.SENSOR_INFO_EXPOSURE_TIME_RANGE);
526                   // Not enforce the max value check here, most of the devices don't support
527                   // larger than 30s exposure time
528                   if (exposureRange == null || exposureRange.getLower() > MIN_EXPOSURE ||
529                           exposureRange.getUpper() < 0) {
530                       exposureRange = defaultRange;
531                       Log.e(TAG, "exposure time range is invalid, use default range");
532                   }
533                   long min = exposureRange.getLower();
534                   long max = exposureRange.getUpper();
535                   float progressFactor = progress / (float)mSeekBarMax;
536                   long curExposureTime = (long) (min + (max - min) * progressFactor);
537                   mCameraControl.getManualControls().setExposure(curExposureTime);
538                   // Update the sensitivity info
539                   StringBuffer info = new StringBuffer("Exposure Time:");
540                   info.append("" + curExposureTime / 1000000.0 + "ms");
541                   mExposureInfoView.setText(info);
542                   mCameraOps.updatePreview(mCameraControl);
543               }
544 
545               @Override
546               public void onStartTrackingTouch(SeekBar seekBar) {
547               }
548 
549               @Override
550               public void onStopTrackingTouch(SeekBar seekBar) {
551               }
552     };
553 
554     private final OnSeekBarChangeListener mFrameDurationSeekBarListener =
555             new OnSeekBarChangeListener() {
556 
557               @Override
558               public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
559                   CameraCharacteristics properties = mCameraOps.getCameraCharacteristics();
560                   Long frameDurationMax = properties.get(
561                           CameraCharacteristics.SENSOR_INFO_MAX_FRAME_DURATION);
562                   if (frameDurationMax == null || frameDurationMax <= 0) {
563                       frameDurationMax = MAX_FRAME_DURATION;
564                       Log.e(TAG, "max frame duration is invalid, set to " + frameDurationMax);
565                   }
566                   // Need calculate from different resolution, hard code to 10ms for now.
567                   long min = 10000000L;
568                   long max = frameDurationMax;
569                   float progressFactor = progress / (float)mSeekBarMax;
570                   long curFrameDuration = (long) (min + (max - min) * progressFactor);
571                   mCameraControl.getManualControls().setFrameDuration(curFrameDuration);
572                   // Update the sensitivity info
573                   StringBuffer info = new StringBuffer("Frame Duration:");
574                   info.append("" + curFrameDuration / 1000000.0 + "ms");
575                   mFrameDurationInfoView.setText(info);
576                   mCameraOps.updatePreview(mCameraControl);
577               }
578 
579               @Override
580               public void onStartTrackingTouch(SeekBar seekBar) {
581               }
582 
583               @Override
584               public void onStopTrackingTouch(SeekBar seekBar) {
585               }
586     };
587 
588     private final View.OnClickListener mControlToggleListener =
589             new View.OnClickListener() {
590         @Override
591         public void onClick(View v) {
592             boolean enableManual;
593             if (mManualCtrlToggle.isChecked()) {
594                 enableManual = true;
595             } else {
596                 enableManual = false;
597             }
598             mCameraControl.getManualControls().setManualControlEnabled(enableManual);
599             enableManualControls(enableManual);
600             mCameraOps.updatePreview(mCameraControl);
601         }
602     };
603 
604     private final View.OnClickListener mRecordingToggleListener =
605             new View.OnClickListener() {
606         @Override
607         public void onClick(View v) {
608             if (mRecordingToggle.isChecked()) {
609                 try {
610                     Log.i(TAG, "start recording, useMediaCodec = " + mUseMediaCodec);
611                     RadioGroup fmt = getRadioFmt();
612                     fmt.setActivated(false);
613                     mCameraOps.startRecording(
614                             /* applicationContext */ TestingCamera2.this,
615                             /* useMediaCodec */ mUseMediaCodec,
616                             /* outputFormat */ getOutputFormat());
617                 } catch (ApiFailureException e) {
618                     logException("Failed to start recording", e);
619                 }
620             } else {
621                 try {
622                     mCameraOps.stopRecording(TestingCamera2.this);
623                     getRadioFmt().setActivated(true);
624                 } catch (ApiFailureException e) {
625                     logException("Failed to stop recording", e);
626                 }
627             }
628         }
629     };
630 
631     private final View.OnClickListener mFocusLockToggleListener =
632             new View.OnClickListener() {
633         @Override
634         public void onClick(View v) {
635             if (VERBOSE) {
636                 Log.v(TAG, "focus_lock#onClick - start");
637             }
638 
639             CameraAutoFocusControls afControls = mCameraControl.getAfControls();
640 
641             if (mFocusLockToggle.isChecked()) {
642                 Log.i(TAG, "lock focus");
643 
644                 afControls.setPendingTriggerStart();
645             } else {
646                 Log.i(TAG, "unlock focus");
647 
648                 afControls.setPendingTriggerCancel();
649             }
650 
651             mCameraOps.updatePreview(mCameraControl);
652 
653             if (VERBOSE) {
654                 Log.v(TAG, "focus_lock#onClick - end");
655             }
656         }
657     };
658 
659     private final CompoundButton.OnCheckedChangeListener mUseMediaCodecListener =
660             new CompoundButton.OnCheckedChangeListener() {
661         @Override
662         public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
663             mUseMediaCodec = isChecked;
664         }
665     };
666 
667     private final CameraOps.Listener mCameraOpsListener = new CameraOps.Listener() {
668         @Override
669         public void onCameraOpened(String cameraId, CameraCharacteristics characteristics) {
670             /*
671              * Populate dynamic per-camera settings
672              */
673 
674             // Map available AF Modes -> AF mode spinner dropdown list of strings
675             int[] availableAfModes =
676                     characteristics.get(CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES);
677 
678             String[] allAfModes = getResources().getStringArray(R.array.focus_mode_spinner_arrays);
679 
680             final List<String> afModeList = new ArrayList<>();
681             final int[] afModePositions = new int[availableAfModes.length];
682 
683             int i = 0;
684             for (int mode : availableAfModes) {
685                 afModeList.add(allAfModes[mode]);
686                 afModePositions[i++] = mode;
687             }
688 
689             ArrayAdapter<String> dataAdapter = new ArrayAdapter<>(TestingCamera2.this,
690                     android.R.layout.simple_spinner_item, afModeList);
691             dataAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
692             mFocusModeSpinner.setAdapter(dataAdapter);
693 
694             /*
695              * Change the AF mode and update preview when AF spinner's selected item changes
696              */
697             mFocusModeSpinner.setOnItemSelectedListener(new OnItemSelectedListener() {
698 
699                 @Override
700                 public void onItemSelected(AdapterView<?> parent, View view, int position,
701                         long id) {
702                     int afMode = afModePositions[position];
703 
704                     Log.i(TAG, "Change auto-focus mode to " + afModeList.get(position)
705                             + " " + afMode);
706 
707                     mCameraControl.getAfControls().setAfMode(afMode);
708 
709                     mCameraOps.updatePreview(mCameraControl);
710                 }
711 
712                 @Override
713                 public void onNothingSelected(AdapterView<?> parent) {
714                     // Do nothing
715                 }
716             });
717         }
718     };
719 }
720