1 /* 2 * Copyright (C) 2014 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; 18 19 import java.util.ArrayList; 20 import java.util.HashSet; 21 import java.util.LinkedList; 22 import java.util.List; 23 import java.util.Locale; 24 import java.util.Set; 25 26 import android.content.Context; 27 import android.util.AttributeSet; 28 import android.view.LayoutInflater; 29 import android.view.Surface; 30 import android.view.View; 31 import android.widget.AdapterView; 32 import android.widget.AdapterView.OnItemSelectedListener; 33 import android.widget.ArrayAdapter; 34 import android.widget.Button; 35 import android.widget.CompoundButton; 36 import android.widget.Spinner; 37 import android.widget.TextView; 38 import android.widget.ToggleButton; 39 import android.hardware.camera2.CameraAccessException; 40 import android.hardware.camera2.CameraCaptureSession; 41 import android.hardware.camera2.CameraCaptureSession.CaptureCallback; 42 import android.hardware.camera2.CameraCharacteristics; 43 import android.hardware.camera2.CameraDevice; 44 import android.hardware.camera2.CameraManager; 45 import android.hardware.camera2.CaptureRequest; 46 import android.hardware.camera2.CaptureResult; 47 import android.hardware.camera2.TotalCaptureResult; 48 49 import org.xmlpull.v1.XmlPullParser; 50 import org.xmlpull.v1.XmlPullParserException; 51 52 import com.android.testingcamera2.PaneTracker.PaneEvent; 53 54 import java.io.IOException; 55 56 /** 57 * 58 * Basic control pane block for the control list 59 * 60 */ 61 public class CameraControlPane extends ControlPane { 62 63 // XML attributes 64 65 /** Name of pane tag */ 66 private static final String PANE_NAME = "camera_pane"; 67 68 /** Attribute: ID for pane (integer) */ 69 private static final String PANE_ID = "id"; 70 /** Attribute: ID for camera to select (String) */ 71 private static final String CAMERA_ID = "camera_id"; 72 73 // End XML attributes 74 75 private static final int MAX_CACHED_RESULTS = 100; 76 77 private static int mCameraPaneIdCounter = 0; 78 79 /** 80 * These correspond to the callbacks from 81 * android.hardware.camera2.CameraDevice.StateCallback, plus UNAVAILABLE for 82 * when there's not a valid camera selected. 83 */ 84 private enum CameraState { 85 UNAVAILABLE, 86 CLOSED, 87 OPENED, 88 DISCONNECTED, 89 ERROR 90 } 91 92 /** 93 * These correspond to the callbacks from {@link CameraCaptureSession.StateCallback}, plus 94 * {@code CONFIGURING} for before a session is returned and {@code NONE} for when there 95 * is no session created. 96 */ 97 private enum SessionState { 98 NONE, 99 CONFIGURED, 100 CONFIGURE_FAILED, 101 READY, 102 ACTIVE, 103 CLOSED 104 } 105 106 private enum CameraCall { 107 NONE, 108 CONFIGURE 109 } 110 111 private final int mPaneId; 112 113 private CameraOps2 mCameraOps; 114 private InfoDisplayer mInfoDisplayer; 115 116 private Spinner mCameraSpinner; 117 private ToggleButton mOpenButton; 118 private Button mInfoButton; 119 private TextView mStatusText; 120 private Button mConfigureButton; 121 private Button mStopButton; 122 private Button mFlushButton; 123 124 /** 125 * All controls that should be enabled when there's a valid camera ID 126 * selected 127 */ 128 private final Set<View> mBaseControls = new HashSet<View>(); 129 /** 130 * All controls that should be enabled when camera is at least in the OPEN 131 * state 132 */ 133 private final Set<View> mOpenControls = new HashSet<View>(); 134 /** 135 * All controls that should be enabled when camera is at least in the IDLE 136 * state 137 */ 138 private final Set<View> mConfiguredControls = new HashSet<View>(); 139 140 private String[] mCameraIds; 141 private String mCurrentCameraId; 142 143 private CameraState mCameraState; 144 private CameraDevice mCurrentCamera; 145 private CameraCaptureSession mCurrentCaptureSession; 146 private SessionState mSessionState = SessionState.NONE; 147 private CameraCall mActiveCameraCall; 148 private LinkedList<TotalCaptureResult> mRecentResults = new LinkedList<>(); 149 150 private List<Surface> mConfiguredSurfaces; 151 private List<TargetControlPane> mConfiguredTargetPanes; 152 153 /** 154 * Constructor for tooling only 155 */ 156 public CameraControlPane(Context context, AttributeSet attrs) { 157 super(context, attrs, null, null); 158 159 mPaneId = 0; 160 setUpUI(context); 161 } 162 163 public CameraControlPane(TestingCamera21 tc, AttributeSet attrs, StatusListener listener) { 164 165 super(tc, attrs, listener, tc.getPaneTracker()); 166 167 mPaneId = mCameraPaneIdCounter++; 168 setUpUI(tc); 169 initializeCameras(tc); 170 171 if (mCameraIds != null) { 172 switchToCamera(mCameraIds[0]); 173 } 174 } 175 176 public CameraControlPane(TestingCamera21 tc, XmlPullParser configParser, StatusListener listener) 177 throws XmlPullParserException, IOException { 178 super(tc, null, listener, tc.getPaneTracker()); 179 180 configParser.require(XmlPullParser.START_TAG, XmlPullParser.NO_NAMESPACE, PANE_NAME); 181 182 int paneId = getAttributeInt(configParser, PANE_ID, -1); 183 if (paneId == -1) { 184 mPaneId = mCameraPaneIdCounter++; 185 } else { 186 mPaneId = paneId; 187 if (mPaneId >= mCameraPaneIdCounter) { 188 mCameraPaneIdCounter = mPaneId + 1; 189 } 190 } 191 192 String cameraId = getAttributeString(configParser, CAMERA_ID, null); 193 194 configParser.next(); 195 configParser.require(XmlPullParser.END_TAG, XmlPullParser.NO_NAMESPACE, PANE_NAME); 196 197 setUpUI(tc); 198 initializeCameras(tc); 199 200 boolean gotCamera = false; 201 if (mCameraIds != null && cameraId != null) { 202 for (int i = 0; i < mCameraIds.length; i++) { 203 if (cameraId.equals(mCameraIds[i])) { 204 switchToCamera(mCameraIds[i]); 205 mCameraSpinner.setSelection(i); 206 gotCamera = true; 207 } 208 } 209 } 210 211 if (!gotCamera && mCameraIds != null) { 212 switchToCamera(mCameraIds[0]); 213 } 214 } 215 216 @Override 217 public void remove() { 218 closeCurrentCamera(); 219 super.remove(); 220 } 221 222 /** 223 * Get list of target panes that are currently actively configured for this 224 * camera 225 */ 226 public List<TargetControlPane> getCurrentConfiguredTargets() { 227 return mConfiguredTargetPanes; 228 } 229 230 /** 231 * Interface to be implemented by an application service for displaying a 232 * camera's information. 233 */ 234 public interface InfoDisplayer { 235 public void showCameraInfo(String cameraId); 236 } 237 238 public CameraCharacteristics getCharacteristics() { 239 if (mCurrentCameraId != null) { 240 return mCameraOps.getCameraInfo(mCurrentCameraId); 241 } 242 return null; 243 } 244 245 public CaptureRequest.Builder getRequestBuilder(int template) { 246 CaptureRequest.Builder request = null; 247 if (mCurrentCamera != null) { 248 try { 249 request = mCurrentCamera.createCaptureRequest(template); 250 // Workaround for b/15748139 251 request.set(CaptureRequest.STATISTICS_LENS_SHADING_MAP_MODE, 252 CaptureRequest.STATISTICS_LENS_SHADING_MAP_MODE_ON); 253 } catch (CameraAccessException e) { 254 TLog.e("Unable to build request for camera %s with template %d.", e, 255 mCurrentCameraId, template); 256 } 257 } 258 return request; 259 } 260 261 /** 262 * Send single capture to camera device. 263 * 264 * @param request 265 * @return true if capture sent successfully 266 */ 267 public boolean capture(CaptureRequest request) { 268 if (mCurrentCaptureSession != null) { 269 try { 270 mCurrentCaptureSession.capture(request, mResultListener, null); 271 return true; 272 } catch (CameraAccessException e) { 273 TLog.e("Unable to capture for camera %s.", e, mCurrentCameraId); 274 } 275 } 276 return false; 277 } 278 279 public boolean repeat(CaptureRequest request) { 280 if (mCurrentCaptureSession != null) { 281 try { 282 mCurrentCaptureSession.setRepeatingRequest(request, mResultListener, null); 283 return true; 284 } catch (CameraAccessException e) { 285 TLog.e("Unable to set repeating request for camera %s.", e, mCurrentCameraId); 286 } 287 } 288 return false; 289 } 290 291 public TotalCaptureResult getResultAt(long timestamp) { 292 for (TotalCaptureResult result : mRecentResults) { 293 long resultTimestamp = result.get(CaptureResult.SENSOR_TIMESTAMP); 294 if (resultTimestamp == timestamp) return result; 295 if (resultTimestamp > timestamp) return null; 296 } 297 return null; 298 } 299 300 private CaptureCallback mResultListener = new CaptureCallback() { 301 public void onCaptureCompleted( 302 CameraCaptureSession session, 303 CaptureRequest request, 304 TotalCaptureResult result) { 305 mRecentResults.add(result); 306 if (mRecentResults.size() > MAX_CACHED_RESULTS) { 307 mRecentResults.remove(); 308 } 309 } 310 }; 311 312 private void setUpUI(Context context) { 313 String paneName = 314 String.format(Locale.US, "%s %c", 315 context.getResources().getString(R.string.camera_pane_title), 316 (char) ('A' + mPaneId)); 317 this.setName(paneName); 318 319 LayoutInflater inflater = 320 (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 321 322 inflater.inflate(R.layout.camera_pane, this); 323 324 mCameraSpinner = (Spinner) findViewById(R.id.camera_pane_camera_spinner); 325 mCameraSpinner.setOnItemSelectedListener(mCameraSpinnerListener); 326 327 mOpenButton = (ToggleButton) findViewById(R.id.camera_pane_open_button); 328 mOpenButton.setOnCheckedChangeListener(mOpenButtonListener); 329 mBaseControls.add(mOpenButton); 330 331 mInfoButton = (Button) findViewById(R.id.camera_pane_info_button); 332 mInfoButton.setOnClickListener(mInfoButtonListener); 333 mBaseControls.add(mInfoButton); 334 335 mStatusText = (TextView) findViewById(R.id.camera_pane_status_text); 336 337 mConfigureButton = (Button) findViewById(R.id.camera_pane_configure_button); 338 mConfigureButton.setOnClickListener(mConfigureButtonListener); 339 mOpenControls.add(mConfigureButton); 340 341 mStopButton = (Button) findViewById(R.id.camera_pane_stop_button); 342 mStopButton.setOnClickListener(mStopButtonListener); 343 mConfiguredControls.add(mStopButton); 344 mFlushButton = (Button) findViewById(R.id.camera_pane_flush_button); 345 mFlushButton.setOnClickListener(mFlushButtonListener); 346 mConfiguredControls.add(mFlushButton); 347 } 348 349 private void initializeCameras(TestingCamera21 tc) { 350 mCameraOps = tc.getCameraOps(); 351 mInfoDisplayer = tc; 352 353 updateCameraList(); 354 } 355 356 private void updateCameraList() { 357 mCameraIds = null; 358 try { 359 mCameraIds = mCameraOps.getCamerasAndListen(mCameraAvailabilityCallback); 360 String[] cameraSpinnerItems = new String[mCameraIds.length]; 361 for (int i = 0; i < mCameraIds.length; i++) { 362 cameraSpinnerItems[i] = String.format("Camera %s", mCameraIds[i]); 363 } 364 mCameraSpinner.setAdapter(new ArrayAdapter<String>(getContext(), R.layout.spinner_item, 365 cameraSpinnerItems)); 366 367 } catch (CameraAccessException e) { 368 TLog.e("Exception trying to get list of cameras: " + e); 369 } 370 } 371 372 private final CompoundButton.OnCheckedChangeListener mOpenButtonListener = 373 new CompoundButton.OnCheckedChangeListener() { 374 @Override 375 public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { 376 if (isChecked) { 377 // Open camera 378 mCurrentCamera = null; 379 boolean success = mCameraOps.openCamera(mCurrentCameraId, mCameraListener); 380 buttonView.setChecked(success); 381 } else { 382 // Close camera 383 closeCurrentCamera(); 384 } 385 } 386 }; 387 388 private final OnClickListener mInfoButtonListener = new OnClickListener() { 389 @Override 390 public void onClick(View v) { 391 mInfoDisplayer.showCameraInfo(mCurrentCameraId); 392 } 393 }; 394 395 private final OnClickListener mStopButtonListener = new OnClickListener() { 396 @Override 397 public void onClick(View v) { 398 if (mCurrentCaptureSession != null) { 399 try { 400 mCurrentCaptureSession.stopRepeating(); 401 } catch (CameraAccessException e) { 402 TLog.e("Unable to stop repeating request for camera %s.", e, mCurrentCameraId); 403 } 404 } 405 } 406 }; 407 408 private final OnClickListener mFlushButtonListener = new OnClickListener() { 409 @Override 410 public void onClick(View v) { 411 if (mCurrentCaptureSession != null) { 412 try { 413 mCurrentCaptureSession.abortCaptures(); 414 } catch (CameraAccessException e) { 415 TLog.e("Unable to flush camera %s.", e, mCurrentCameraId); 416 } 417 } 418 } 419 }; 420 421 private final OnClickListener mConfigureButtonListener = new OnClickListener() { 422 @Override 423 public void onClick(View v) { 424 List<Surface> targetSurfaces = new ArrayList<Surface>(); 425 List<TargetControlPane> targetPanes = new ArrayList<TargetControlPane>(); 426 for (TargetControlPane targetPane : mPaneTracker.getPanes(TargetControlPane.class)) { 427 Surface target = targetPane.getTargetSurfaceForCameraPane(getPaneName()); 428 if (target != null) { 429 targetSurfaces.add(target); 430 targetPanes.add(targetPane); 431 } 432 } 433 try { 434 TLog.i("Configuring camera %s with %d surfaces", mCurrentCamera.getId(), 435 targetSurfaces.size()); 436 mActiveCameraCall = CameraCall.CONFIGURE; 437 if (targetSurfaces.size() > 0) { 438 mCurrentCamera.createCaptureSession(targetSurfaces, mSessionListener, /*handler*/null); 439 } else if (mCurrentCaptureSession != null) { 440 mCurrentCaptureSession.close(); 441 mCurrentCaptureSession = null; 442 } 443 mConfiguredSurfaces = targetSurfaces; 444 mConfiguredTargetPanes = targetPanes; 445 } catch (CameraAccessException e) { 446 mActiveCameraCall = CameraCall.NONE; 447 TLog.e("Unable to configure camera %s.", e, mCurrentCamera.getId()); 448 } catch (IllegalArgumentException e) { 449 mActiveCameraCall = CameraCall.NONE; 450 TLog.e("Unable to configure camera %s.", e, mCurrentCamera.getId()); 451 } catch (IllegalStateException e) { 452 mActiveCameraCall = CameraCall.NONE; 453 TLog.e("Unable to configure camera %s.", e, mCurrentCamera.getId()); 454 } 455 } 456 }; 457 458 private final CameraCaptureSession.StateCallback mSessionListener = 459 new CameraCaptureSession.StateCallback() { 460 461 @Override 462 public void onConfigured(CameraCaptureSession session) { 463 mCurrentCaptureSession = session; 464 TLog.i("Configuration completed for camera %s.", mCurrentCamera.getId()); 465 466 setSessionState(SessionState.CONFIGURED); 467 } 468 469 @Override 470 public void onConfigureFailed(CameraCaptureSession session) { 471 mActiveCameraCall = CameraCall.NONE; 472 TLog.e("Configuration failed for camera %s.", mCurrentCamera.getId()); 473 474 setSessionState(SessionState.CONFIGURE_FAILED); 475 } 476 477 @Override 478 public void onReady(CameraCaptureSession session) { 479 setSessionState(SessionState.READY); 480 } 481 482 /** 483 * This method is called when the session starts actively processing capture requests. 484 * 485 * <p>If capture requests are submitted prior to {@link #onConfigured} being called, 486 * then the session will start processing those requests immediately after the callback, 487 * and this method will be immediately called after {@link #onConfigured}. 488 * 489 * <p>If the session runs out of capture requests to process and calls {@link #onReady}, 490 * then this callback will be invoked again once new requests are submitted for capture.</p> 491 */ 492 @Override 493 public void onActive(CameraCaptureSession session) { 494 setSessionState(SessionState.ACTIVE); 495 } 496 497 /** 498 * This method is called when the session is closed. 499 * 500 * <p>A session is closed when a new session is created by the parent camera device, 501 * or when the parent camera device is closed (either by the user closing the device, 502 * or due to a camera device disconnection or fatal error).</p> 503 * 504 * <p>Once a session is closed, all methods on it will throw an IllegalStateException, and 505 * any repeating requests or bursts are stopped (as if {@link #stopRepeating()} was called). 506 * However, any in-progress capture requests submitted to the session will be completed 507 * as normal.</p> 508 */ 509 @Override 510 public void onClosed(CameraCaptureSession session) { 511 // Ignore closes if the session has been replaced 512 if (mCurrentCaptureSession != null && session != mCurrentCaptureSession) { 513 return; 514 } 515 setSessionState(SessionState.CLOSED); 516 } 517 }; 518 519 private final CameraDevice.StateCallback mCameraListener = new CameraDevice.StateCallback() { 520 @Override 521 public void onClosed(CameraDevice camera) { 522 // Don't change state on close, tracked by callers of close() 523 } 524 525 @Override 526 public void onDisconnected(CameraDevice camera) { 527 setCameraState(CameraState.DISCONNECTED); 528 } 529 530 @Override 531 public void onError(CameraDevice camera, int error) { 532 setCameraState(CameraState.ERROR); 533 } 534 535 @Override 536 public void onOpened(CameraDevice camera) { 537 mCurrentCamera = camera; 538 setCameraState(CameraState.OPENED); 539 } 540 }; 541 542 private void switchToCamera(String newCameraId) { 543 closeCurrentCamera(); 544 545 mCurrentCameraId = newCameraId; 546 547 if (mCurrentCameraId == null) { 548 setCameraState(CameraState.UNAVAILABLE); 549 } else { 550 setCameraState(CameraState.CLOSED); 551 } 552 553 mPaneTracker.notifyOtherPanes(this, PaneTracker.PaneEvent.NEW_CAMERA_SELECTED); 554 } 555 556 private void closeCurrentCamera() { 557 if (mCurrentCamera != null) { 558 mCurrentCamera.close(); 559 mCurrentCamera = null; 560 setCameraState(CameraState.CLOSED); 561 mOpenButton.setChecked(false); 562 } 563 } 564 565 private void setSessionState(SessionState newState) { 566 mSessionState = newState; 567 mStatusText.setText("S." + mSessionState.toString()); 568 569 switch (mSessionState) { 570 case CONFIGURE_FAILED: 571 mActiveCameraCall = CameraCall.NONE; 572 // fall-through 573 case CLOSED: 574 enableBaseControls(true); 575 enableOpenControls(true); 576 enableConfiguredControls(false); 577 mConfiguredTargetPanes = null; 578 break; 579 case NONE: 580 enableBaseControls(true); 581 enableOpenControls(true); 582 enableConfiguredControls(false); 583 mConfiguredTargetPanes = null; 584 break; 585 case CONFIGURED: 586 if (mActiveCameraCall != CameraCall.CONFIGURE) { 587 throw new AssertionError(); 588 } 589 mPaneTracker.notifyOtherPanes(this, PaneEvent.CAMERA_CONFIGURED); 590 mActiveCameraCall = CameraCall.NONE; 591 // fall-through 592 case READY: 593 case ACTIVE: 594 enableBaseControls(true); 595 enableOpenControls(true); 596 enableConfiguredControls(true); 597 break; 598 default: 599 throw new AssertionError("Unhandled case " + mSessionState); 600 } 601 } 602 603 private void setCameraState(CameraState newState) { 604 mCameraState = newState; 605 mStatusText.setText("C." + mCameraState.toString()); 606 switch (mCameraState) { 607 case UNAVAILABLE: 608 enableBaseControls(false); 609 enableOpenControls(false); 610 enableConfiguredControls(false); 611 mConfiguredTargetPanes = null; 612 break; 613 case CLOSED: 614 case DISCONNECTED: 615 case ERROR: 616 enableBaseControls(true); 617 enableOpenControls(false); 618 enableConfiguredControls(false); 619 mConfiguredTargetPanes = null; 620 break; 621 case OPENED: 622 enableBaseControls(true); 623 enableOpenControls(true); 624 enableConfiguredControls(false); 625 mConfiguredTargetPanes = null; 626 break; 627 } 628 } 629 630 private void enableBaseControls(boolean enabled) { 631 for (View v : mBaseControls) { 632 v.setEnabled(enabled); 633 } 634 if (!enabled) { 635 mOpenButton.setChecked(false); 636 } 637 } 638 639 private void enableOpenControls(boolean enabled) { 640 for (View v : mOpenControls) { 641 v.setEnabled(enabled); 642 } 643 } 644 645 private void enableConfiguredControls(boolean enabled) { 646 for (View v : mConfiguredControls) { 647 v.setEnabled(enabled); 648 } 649 } 650 651 private final CameraManager.AvailabilityCallback mCameraAvailabilityCallback = 652 new CameraManager.AvailabilityCallback() { 653 @Override 654 public void onCameraAvailable(String cameraId) { 655 // TODO: Update camera list in an intelligent fashion 656 // (can't just call updateCameraList or the selected camera may change) 657 } 658 659 @Override 660 public void onCameraUnavailable(String cameraId) { 661 // TODO: Update camera list in an intelligent fashion 662 // (can't just call updateCameraList or the selected camera may change) 663 } 664 }; 665 666 private final OnItemSelectedListener mCameraSpinnerListener = new OnItemSelectedListener() { 667 @Override 668 public void onItemSelected(AdapterView<?> parent, View view, int pos, long id) { 669 String newCameraId = mCameraIds[pos]; 670 if (newCameraId != mCurrentCameraId) { 671 switchToCamera(newCameraId); 672 } 673 } 674 675 @Override 676 public void onNothingSelected(AdapterView<?> parent) { 677 switchToCamera(null); 678 } 679 }; 680 } 681