1 /* 2 * Copyright 2023 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.DeviceAsWebcam; 18 19 import static android.accessibilityservice.AccessibilityServiceInfo.FEEDBACK_ALL_MASK; 20 21 import android.accessibilityservice.AccessibilityServiceInfo; 22 import android.animation.ObjectAnimator; 23 import android.annotation.SuppressLint; 24 import android.app.Activity; 25 import android.content.ComponentName; 26 import android.content.Intent; 27 import android.content.ServiceConnection; 28 import android.graphics.Bitmap; 29 import android.graphics.Canvas; 30 import android.graphics.Insets; 31 import android.graphics.Paint; 32 import android.graphics.SurfaceTexture; 33 import android.graphics.drawable.BitmapDrawable; 34 import android.graphics.drawable.Drawable; 35 import android.graphics.drawable.ShapeDrawable; 36 import android.graphics.drawable.shapes.OvalShape; 37 import android.hardware.camera2.CameraCharacteristics; 38 import android.hardware.camera2.CameraMetadata; 39 import android.os.Bundle; 40 import android.os.ConditionVariable; 41 import android.os.IBinder; 42 import android.util.Log; 43 import android.util.Range; 44 import android.util.Size; 45 import android.view.GestureDetector; 46 import android.view.Gravity; 47 import android.view.HapticFeedbackConstants; 48 import android.view.KeyEvent; 49 import android.view.MotionEvent; 50 import android.view.TextureView; 51 import android.view.View; 52 import android.view.ViewGroup; 53 import android.view.Window; 54 import android.view.WindowInsets; 55 import android.view.WindowInsetsController; 56 import android.view.accessibility.AccessibilityManager; 57 import android.view.animation.AccelerateDecelerateInterpolator; 58 import android.widget.FrameLayout; 59 import android.widget.ImageButton; 60 61 import androidx.cardview.widget.CardView; 62 63 import com.android.DeviceAsWebcam.view.SelectorListItemData; 64 import com.android.DeviceAsWebcam.view.SwitchCameraSelectorView; 65 import com.android.DeviceAsWebcam.view.ZoomController; 66 67 import java.util.ArrayList; 68 import java.util.List; 69 import java.util.concurrent.Executor; 70 import java.util.concurrent.Executors; 71 import java.util.function.Consumer; 72 73 public class DeviceAsWebcamPreview extends Activity { 74 private static final String TAG = DeviceAsWebcamPreview.class.getSimpleName(); 75 private static final boolean VERBOSE = Log.isLoggable(TAG, Log.VERBOSE); 76 private static final int ROTATION_ANIMATION_DURATION_MS = 300; 77 78 private final Executor mThreadExecutor = Executors.newFixedThreadPool(2); 79 private final ConditionVariable mServiceReady = new ConditionVariable(); 80 81 private boolean mTextureViewSetup = false; 82 private Size mPreviewSize; 83 private DeviceAsWebcamFgService mLocalFgService; 84 private AccessibilityManager mAccessibilityManager; 85 86 private FrameLayout mTextureViewContainer; 87 private CardView mTextureViewCard; 88 private TextureView mTextureView; 89 private View mFocusIndicator; 90 private ZoomController mZoomController = null; 91 private ImageButton mToggleCameraButton; 92 private SwitchCameraSelectorView mSwitchCameraSelectorView; 93 private List<SelectorListItemData> mSelectorListItemDataList; 94 // A listener to monitor the preview size change events. This might be invoked when toggling 95 // camera or the webcam stream is started after the preview stream. 96 Consumer<Size> mPreviewSizeChangeListener = size -> runOnUiThread(() -> { 97 mPreviewSize = size; 98 setTextureViewScale(); 99 } 100 ); 101 102 // Listener for when Accessibility service are enabled or disabled. 103 AccessibilityManager.AccessibilityServicesStateChangeListener mAccessibilityListener = 104 accessibilityManager -> { 105 List<AccessibilityServiceInfo> services = 106 accessibilityManager.getEnabledAccessibilityServiceList(FEEDBACK_ALL_MASK); 107 boolean areServicesEnabled = !services.isEmpty(); 108 runOnUiThread(() -> 109 mZoomController.onAccessibilityServicesEnabled(areServicesEnabled)); 110 }; 111 112 113 /** 114 * {@link View.OnLayoutChangeListener} to add to 115 * {@link DeviceAsWebcamPreview#mTextureViewContainer} for when we need to know 116 * when changes to the view are committed. 117 * <p> 118 * NOTE: This removes itself as a listener after one call to prevent spurious callbacks 119 * once the texture view has been resized. 120 */ 121 View.OnLayoutChangeListener mTextureViewContainerLayoutListener = 122 new View.OnLayoutChangeListener() { 123 @Override 124 public void onLayoutChange(View view, int left, int top, int right, int bottom, 125 int oldLeft, int oldTop, int oldRight, int oldBottom) { 126 // Remove self to prevent further calls to onLayoutChange. 127 view.removeOnLayoutChangeListener(this); 128 // Update the texture view to fit the new bounds. 129 runOnUiThread(() -> { 130 if (mPreviewSize != null) { 131 setTextureViewScale(); 132 } 133 }); 134 } 135 }; 136 137 /** 138 * {@link TextureView.SurfaceTextureListener} handles several lifecycle events on a 139 * {@link TextureView}. 140 */ 141 private final TextureView.SurfaceTextureListener mSurfaceTextureListener = 142 new TextureView.SurfaceTextureListener() { 143 @Override 144 public void onSurfaceTextureAvailable(SurfaceTexture texture, int width, 145 int height) { 146 runOnUiThread(() -> { 147 if (VERBOSE) { 148 Log.v(TAG, "onSurfaceTextureAvailable " + width + " x " + height); 149 } 150 mServiceReady.block(); 151 152 if (!mTextureViewSetup) { 153 setupTextureViewLayout(); 154 } 155 156 if (mLocalFgService == null) { 157 return; 158 } 159 mLocalFgService.setOnDestroyedCallback(() -> onServiceDestroyed()); 160 161 if (mPreviewSize == null) { 162 return; 163 } 164 mLocalFgService.setPreviewSurfaceTexture(texture, mPreviewSize, 165 mPreviewSizeChangeListener); 166 List<CameraId> availableCameraIds = 167 mLocalFgService.getAvailableCameraIds(); 168 if (availableCameraIds != null && availableCameraIds.size() > 1) { 169 setupSwitchCameraSelector(); 170 mToggleCameraButton.setVisibility(View.VISIBLE); 171 if (canToggleCamera()) { 172 mToggleCameraButton.setOnClickListener(v -> toggleCamera()); 173 } else { 174 mToggleCameraButton.setOnClickListener(v -> { 175 mSwitchCameraSelectorView.show(); 176 }); 177 } 178 mToggleCameraButton.setOnLongClickListener(v -> { 179 mSwitchCameraSelectorView.show(); 180 return true; 181 }); 182 } else { 183 mToggleCameraButton.setVisibility(View.GONE); 184 } 185 rotateUiByRotationDegrees(mLocalFgService.getCurrentRotation()); 186 mLocalFgService.setRotationUpdateListener( 187 rotation -> rotateUiByRotationDegrees(rotation)); 188 }); 189 } 190 191 @Override 192 public void onSurfaceTextureSizeChanged(SurfaceTexture texture, int width, 193 int height) { 194 if (VERBOSE) { 195 Log.v(TAG, "onSurfaceTextureSizeChanged " + width + " x " + height); 196 } 197 } 198 199 @Override 200 public boolean onSurfaceTextureDestroyed(SurfaceTexture texture) { 201 runOnUiThread(() -> { 202 if (mLocalFgService != null) { 203 mLocalFgService.removePreviewSurfaceTexture(); 204 } 205 }); 206 return true; 207 } 208 209 @Override 210 public void onSurfaceTextureUpdated(SurfaceTexture texture) { 211 } 212 }; 213 214 private ServiceConnection mConnection = new ServiceConnection() { 215 @Override 216 public void onServiceConnected(ComponentName className, IBinder service) { 217 mLocalFgService = ((DeviceAsWebcamFgService.LocalBinder) service).getService(); 218 if (VERBOSE) { 219 Log.v(TAG, "Got Fg service"); 220 } 221 mServiceReady.open(); 222 } 223 224 @Override 225 public void onServiceDisconnected(ComponentName className) { 226 // Serialize updating mLocalFgService on UI Thread as all consumers of mLocalFgService 227 // run on the UI Thread. 228 runOnUiThread(() -> { 229 mLocalFgService = null; 230 finish(); 231 }); 232 } 233 }; 234 235 private MotionEventToZoomRatioConverter mMotionEventToZoomRatioConverter = null; 236 private final MotionEventToZoomRatioConverter.ZoomRatioUpdatedListener mZoomRatioListener = 237 new MotionEventToZoomRatioConverter.ZoomRatioUpdatedListener() { 238 @Override 239 public void onZoomRatioUpdated(float updatedZoomRatio) { 240 if (mLocalFgService == null) { 241 return; 242 } 243 244 mLocalFgService.setZoomRatio(updatedZoomRatio); 245 mZoomController.setZoomRatio(updatedZoomRatio, 246 ZoomController.ZOOM_UI_SEEK_BAR_MODE); 247 } 248 }; 249 250 private GestureDetector.SimpleOnGestureListener mTapToFocusListener = 251 new GestureDetector.SimpleOnGestureListener() { 252 @Override 253 public boolean onSingleTapUp(MotionEvent motionEvent) { 254 return tapToFocus(motionEvent); 255 } 256 }; 257 setTextureViewScale()258 private void setTextureViewScale() { 259 FrameLayout.LayoutParams frameLayout = new FrameLayout.LayoutParams(mPreviewSize.getWidth(), 260 mPreviewSize.getHeight(), Gravity.CENTER); 261 mTextureView.setLayoutParams(frameLayout); 262 263 int pWidth = mTextureViewContainer.getWidth(); 264 int pHeight = mTextureViewContainer.getHeight(); 265 float scaleYToUnstretched = (float) mPreviewSize.getWidth() / mPreviewSize.getHeight(); 266 float scaleXToUnstretched = (float) mPreviewSize.getHeight() / mPreviewSize.getWidth(); 267 float additionalScaleForX = (float) pWidth / mPreviewSize.getHeight(); 268 float additionalScaleForY = (float) pHeight / mPreviewSize.getWidth(); 269 270 // To fit the preview, either letterbox or pillar box. 271 float additionalScaleChosen = Math.min(additionalScaleForX, additionalScaleForY); 272 273 float texScaleX = scaleXToUnstretched * additionalScaleChosen; 274 float texScaleY = scaleYToUnstretched * additionalScaleChosen; 275 276 mTextureView.setScaleX(texScaleX); 277 mTextureView.setScaleY(texScaleY); 278 279 // Resize the card view to match TextureView's final size exactly. This is to clip 280 // textureView corners. 281 ViewGroup.LayoutParams cardLayoutParams = mTextureViewCard.getLayoutParams(); 282 // Reduce size by two pixels to remove any rounding errors from casting to int. 283 cardLayoutParams.height = ((int) (mPreviewSize.getHeight() * texScaleY)) - 2; 284 cardLayoutParams.width = ((int) (mPreviewSize.getWidth() * texScaleX)) - 2; 285 mTextureViewCard.setLayoutParams(cardLayoutParams); 286 } 287 288 @SuppressLint("ClickableViewAccessibility") setupZoomUiControl()289 private void setupZoomUiControl() { 290 if (mLocalFgService == null || mLocalFgService.getCameraInfo() == null) { 291 return; 292 } 293 294 Range<Float> zoomRatioRange = mLocalFgService.getCameraInfo().getZoomRatioRange(); 295 296 if (zoomRatioRange == null) { 297 return; 298 } 299 300 // Retrieves current zoom ratio setting from CameraController so that the zoom ratio set by 301 // the previous closed activity can be correctly restored 302 float currentZoomRatio = mLocalFgService.getZoomRatio(); 303 304 mMotionEventToZoomRatioConverter = new MotionEventToZoomRatioConverter( 305 getApplicationContext(), zoomRatioRange, currentZoomRatio, 306 mZoomRatioListener); 307 308 GestureDetector tapToFocusGestureDetector = new GestureDetector(getApplicationContext(), 309 mTapToFocusListener); 310 311 // Restores the focus indicator if tap-to-focus points exist 312 float[] tapToFocusPoints = mLocalFgService.getTapToFocusPoints(); 313 if (tapToFocusPoints != null) { 314 showFocusIndicator(tapToFocusPoints); 315 } 316 317 mTextureView.setOnTouchListener( 318 (view, event) -> { 319 mMotionEventToZoomRatioConverter.onTouchEvent(event); 320 tapToFocusGestureDetector.onTouchEvent(event); 321 return true; 322 }); 323 324 mZoomController.init(getLayoutInflater(), zoomRatioRange); 325 mZoomController.setZoomRatio(currentZoomRatio, ZoomController.ZOOM_UI_TOGGLE_MODE); 326 mZoomController.setOnZoomRatioUpdatedListener( 327 value -> { 328 if (mLocalFgService != null) { 329 mLocalFgService.setZoomRatio(value); 330 } 331 mMotionEventToZoomRatioConverter.setZoomRatio(value); 332 }); 333 if (mAccessibilityManager != null) { 334 mAccessibilityListener.onAccessibilityServicesStateChanged(mAccessibilityManager); 335 } 336 } 337 setupZoomRatioSeekBar()338 private void setupZoomRatioSeekBar() { 339 if (mLocalFgService == null) { 340 return; 341 } 342 343 mZoomController.setSupportedZoomRatioRange( 344 mLocalFgService.getCameraInfo().getZoomRatioRange()); 345 } 346 setupSwitchCameraSelector()347 private void setupSwitchCameraSelector() { 348 if (mLocalFgService == null || mLocalFgService.getCameraInfo() == null) { 349 return; 350 } 351 setToggleCameraContentDescription(); 352 mSelectorListItemDataList = createSelectorItemDataList(); 353 354 mSwitchCameraSelectorView.init(getLayoutInflater(), mSelectorListItemDataList); 355 mSwitchCameraSelectorView.setRotation(mLocalFgService.getCurrentRotation()); 356 mSwitchCameraSelectorView.setOnCameraSelectedListener(cameraId -> switchCamera(cameraId)); 357 mSwitchCameraSelectorView.updateSelectedItem( 358 mLocalFgService.getCameraInfo().getCameraId()); 359 360 // Dynamically enable/disable the toggle button and zoom controller so that the behaviors 361 // under accessibility mode will be correct. 362 mSwitchCameraSelectorView.setOnVisibilityChangedListener(visibility -> { 363 mToggleCameraButton.setEnabled(visibility != View.VISIBLE); 364 mZoomController.setEnabled(visibility != View.VISIBLE); 365 }); 366 } 367 rotateUiByRotationDegrees(int rotation)368 private void rotateUiByRotationDegrees(int rotation) { 369 if (mLocalFgService == null) { 370 // Don't do anything if no foreground service is connected 371 return; 372 } 373 int finalRotation = calculateUiRotation(rotation); 374 runOnUiThread(() -> { 375 ObjectAnimator anim = ObjectAnimator.ofFloat(mToggleCameraButton, 376 /*propertyName=*/"rotation", finalRotation) 377 .setDuration(ROTATION_ANIMATION_DURATION_MS); 378 anim.setInterpolator(new AccelerateDecelerateInterpolator()); 379 anim.start(); 380 mToggleCameraButton.performHapticFeedback( 381 HapticFeedbackConstants.GESTURE_THRESHOLD_ACTIVATE); 382 383 mZoomController.setTextDisplayRotation(finalRotation, ROTATION_ANIMATION_DURATION_MS); 384 mSwitchCameraSelectorView.setRotation(finalRotation); 385 }); 386 } 387 calculateUiRotation(int rotation)388 private int calculateUiRotation(int rotation) { 389 // Rotates the UI control container according to the device sensor rotation degrees and the 390 // camera sensor orientation. 391 int sensorOrientation = mLocalFgService.getCameraInfo().getSensorOrientation(); 392 if (mLocalFgService.getCameraInfo().getLensFacing() 393 == CameraCharacteristics.LENS_FACING_BACK) { 394 rotation = (rotation + sensorOrientation) % 360; 395 } else { 396 rotation = (360 + rotation - sensorOrientation) % 360; 397 } 398 399 // Rotation angle of the view must be [-179, 180] to ensure we always rotate the 400 // view through the natural orientation (0) 401 return rotation <= 180 ? rotation : rotation - 360; 402 } 403 setupTextureViewLayout()404 private void setupTextureViewLayout() { 405 mPreviewSize = mLocalFgService.getSuitablePreviewSize(); 406 if (mPreviewSize != null) { 407 setTextureViewScale(); 408 setupZoomUiControl(); 409 } 410 } 411 onServiceDestroyed()412 private void onServiceDestroyed() { 413 ConditionVariable cv = new ConditionVariable(); 414 cv.close(); 415 runOnUiThread(() -> { 416 try { 417 mLocalFgService = null; 418 finish(); 419 } finally { 420 cv.open(); 421 } 422 }); 423 cv.block(); 424 } 425 426 @Override onCreate(Bundle savedInstanceState)427 public void onCreate(Bundle savedInstanceState) { 428 super.onCreate(savedInstanceState); 429 setContentView(R.layout.preview_layout); 430 mTextureViewContainer = findViewById(R.id.texture_view_container); 431 mTextureViewCard = findViewById(R.id.texture_view_card); 432 mTextureView = findViewById(R.id.texture_view); 433 mFocusIndicator = findViewById(R.id.focus_indicator); 434 mFocusIndicator.setBackground(createFocusIndicatorDrawable()); 435 mToggleCameraButton = findViewById(R.id.toggle_camera_button); 436 mZoomController = findViewById(R.id.zoom_ui_controller); 437 mSwitchCameraSelectorView = findViewById(R.id.switch_camera_selector_view); 438 439 mAccessibilityManager = getSystemService(AccessibilityManager.class); 440 if (mAccessibilityManager != null) { 441 mAccessibilityManager.addAccessibilityServicesStateChangeListener( 442 mAccessibilityListener); 443 } 444 445 // Update view to allow for status bar. This let's us keep a consistent background color 446 // behind the statusbar. 447 mTextureViewContainer.setOnApplyWindowInsetsListener((view, inset) -> { 448 Insets cutoutInset = inset.getInsets(WindowInsets.Type.displayCutout()); 449 int minMargin = (int) getResources().getDimension(R.dimen.preview_margin_top_min); 450 451 ViewGroup.MarginLayoutParams layoutParams = 452 (ViewGroup.MarginLayoutParams) mTextureViewContainer.getLayoutParams(); 453 // Set the top margin to accommodate the cutout. However, if the cutout is 454 // very small, add a small margin to prevent the preview from going too close to 455 // the edge of the device. 456 int newMargin = Math.max(minMargin, cutoutInset.top); 457 if (newMargin != layoutParams.topMargin) { 458 layoutParams.topMargin = Math.max(minMargin, cutoutInset.top); 459 mTextureViewContainer.setLayoutParams(layoutParams); 460 // subscribe to layout changes of the texture view container so we can 461 // resize the texture view once the container has been drawn with the new 462 // margins 463 mTextureViewContainer 464 .addOnLayoutChangeListener(mTextureViewContainerLayoutListener); 465 } 466 return WindowInsets.CONSUMED; 467 }); 468 469 bindService(new Intent(this, DeviceAsWebcamFgService.class), 0, mThreadExecutor, 470 mConnection); 471 } 472 createFocusIndicatorDrawable()473 private Drawable createFocusIndicatorDrawable() { 474 int indicatorSize = getResources().getDimensionPixelSize(R.dimen.focus_indicator_size); 475 Bitmap bitmap = Bitmap.createBitmap(indicatorSize, indicatorSize, Bitmap.Config.ARGB_8888); 476 Canvas canvas = new Canvas(bitmap); 477 478 OvalShape ovalShape = new OvalShape(); 479 ShapeDrawable shapeDrawable = new ShapeDrawable(ovalShape); 480 Paint paint = shapeDrawable.getPaint(); 481 paint.setAntiAlias(true); 482 paint.setStyle(Paint.Style.STROKE); 483 484 int strokeWidth = getResources().getDimensionPixelSize( 485 R.dimen.focus_indicator_stroke_width); 486 paint.setStrokeWidth(strokeWidth); 487 paint.setColor(getResources().getColor(android.R.color.white, null)); 488 int halfIndicatorSize = indicatorSize / 2; 489 canvas.drawCircle(halfIndicatorSize, halfIndicatorSize, halfIndicatorSize - strokeWidth, 490 paint); 491 paint.setStyle(Paint.Style.FILL); 492 paint.setColor(getResources().getColor(R.color.focus_indicator_background_color, null)); 493 canvas.drawCircle(halfIndicatorSize, halfIndicatorSize, halfIndicatorSize - strokeWidth, 494 paint); 495 496 return new BitmapDrawable(getResources(), bitmap); 497 } 498 hideSystemUiAndActionBar()499 private void hideSystemUiAndActionBar() { 500 // Hides status bar 501 Window window = getWindow(); 502 window.setStatusBarColor(android.R.color.system_neutral1_800); 503 window.setDecorFitsSystemWindows(false); 504 WindowInsetsController controller = window.getInsetsController(); 505 if (controller != null) { 506 controller.hide(WindowInsets.Type.systemBars()); 507 controller.setSystemBarsBehavior( 508 WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE); 509 } 510 // Hides the action bar 511 getActionBar().hide(); 512 } 513 514 @Override onResume()515 public void onResume() { 516 super.onResume(); 517 hideSystemUiAndActionBar(); 518 // When the screen is turned off and turned back on, the SurfaceTexture is already 519 // available, and "onSurfaceTextureAvailable" will not be called. In that case, we can open 520 // a camera and start preview from here (otherwise, we wait until the surface is ready in 521 // the SurfaceTextureListener). 522 if (mTextureView.isAvailable()) { 523 mServiceReady.block(); 524 if (!mTextureViewSetup) { 525 setupTextureViewLayout(); 526 mTextureViewSetup = true; 527 } 528 if (mLocalFgService != null && mPreviewSize != null) { 529 mLocalFgService.setPreviewSurfaceTexture(mTextureView.getSurfaceTexture(), 530 mPreviewSize, mPreviewSizeChangeListener); 531 rotateUiByRotationDegrees(mLocalFgService.getCurrentRotation()); 532 mLocalFgService.setRotationUpdateListener(rotation -> 533 runOnUiThread(() -> rotateUiByRotationDegrees(rotation))); 534 mZoomController.setZoomRatio(mLocalFgService.getZoomRatio(), 535 ZoomController.ZOOM_UI_TOGGLE_MODE); 536 } 537 } else { 538 mTextureView.setSurfaceTextureListener(mSurfaceTextureListener); 539 } 540 } 541 542 @Override onPause()543 public void onPause() { 544 if (mLocalFgService != null) { 545 mLocalFgService.removePreviewSurfaceTexture(); 546 mLocalFgService.setRotationUpdateListener(null); 547 } 548 super.onPause(); 549 } 550 @Override onKeyDown(int keyCode, KeyEvent event)551 public boolean onKeyDown(int keyCode, KeyEvent event) { 552 if (mLocalFgService == null || (keyCode != KeyEvent.KEYCODE_VOLUME_DOWN 553 && keyCode != KeyEvent.KEYCODE_VOLUME_UP)) { 554 return super.onKeyDown(keyCode, event); 555 } 556 557 float zoomRatio = mLocalFgService.getZoomRatio(); 558 559 // Uses volume key events to adjust zoom ratio 560 if ((keyCode == KeyEvent.KEYCODE_VOLUME_DOWN)){ 561 zoomRatio -= 0.1f; 562 } else { 563 zoomRatio += 0.1f; 564 } 565 566 // Clamps the zoom ratio in the supported range 567 Range<Float> zoomRatioRange = mLocalFgService.getCameraInfo().getZoomRatioRange(); 568 zoomRatio = Math.min(Math.max(zoomRatio, zoomRatioRange.getLower()), 569 zoomRatioRange.getUpper()); 570 571 // Updates the new value to all related controls 572 mLocalFgService.setZoomRatio(zoomRatio); 573 mZoomController.setZoomRatio(zoomRatio, ZoomController.ZOOM_UI_SEEK_BAR_MODE); 574 mMotionEventToZoomRatioConverter.setZoomRatio(zoomRatio); 575 576 return true; 577 } 578 579 @Override onDestroy()580 public void onDestroy() { 581 if (mAccessibilityManager != null) { 582 mAccessibilityManager.removeAccessibilityServicesStateChangeListener( 583 mAccessibilityListener); 584 } 585 if (mLocalFgService != null) { 586 mLocalFgService.setOnDestroyedCallback(null); 587 } 588 unbindService(mConnection); 589 super.onDestroy(); 590 } 591 592 /** 593 * Returns {@code true} when the device has both available back and front cameras. Otherwise, 594 * returns {@code false}. 595 */ canToggleCamera()596 private boolean canToggleCamera() { 597 if (mLocalFgService == null) { 598 return false; 599 } 600 601 List<CameraId> availableCameraIds = mLocalFgService.getAvailableCameraIds(); 602 boolean hasBackCamera = false; 603 boolean hasFrontCamera = false; 604 605 for (CameraId cameraId : availableCameraIds) { 606 CameraInfo cameraInfo = mLocalFgService.getOrCreateCameraInfo(cameraId); 607 if (cameraInfo.getLensFacing() == CameraCharacteristics.LENS_FACING_BACK) { 608 hasBackCamera = true; 609 } else if (cameraInfo.getLensFacing() == CameraCharacteristics.LENS_FACING_FRONT) { 610 hasFrontCamera = true; 611 } 612 } 613 614 return hasBackCamera && hasFrontCamera; 615 } 616 setToggleCameraContentDescription()617 private void setToggleCameraContentDescription() { 618 if (mLocalFgService == null) { 619 return; 620 } 621 int lensFacing = mLocalFgService.getCameraInfo().getLensFacing(); 622 CharSequence descr = getText(R.string.toggle_camera_button_description_front); 623 if (lensFacing == CameraMetadata.LENS_FACING_FRONT) { 624 descr = getText(R.string.toggle_camera_button_description_back); 625 } 626 mToggleCameraButton.setContentDescription(descr); 627 } 628 toggleCamera()629 private void toggleCamera() { 630 if (mLocalFgService == null) { 631 return; 632 } 633 634 mLocalFgService.toggleCamera(); 635 setToggleCameraContentDescription(); 636 mFocusIndicator.setVisibility(View.GONE); 637 mMotionEventToZoomRatioConverter.reset(mLocalFgService.getZoomRatio(), 638 mLocalFgService.getCameraInfo().getZoomRatioRange()); 639 setupZoomRatioSeekBar(); 640 mZoomController.setZoomRatio(mLocalFgService.getZoomRatio(), 641 ZoomController.ZOOM_UI_TOGGLE_MODE); 642 mSwitchCameraSelectorView.updateSelectedItem( 643 mLocalFgService.getCameraInfo().getCameraId()); 644 } 645 switchCamera(CameraId cameraId)646 private void switchCamera(CameraId cameraId) { 647 if (mLocalFgService == null) { 648 return; 649 } 650 651 mLocalFgService.switchCamera(cameraId); 652 setToggleCameraContentDescription(); 653 mMotionEventToZoomRatioConverter.reset(mLocalFgService.getZoomRatio(), 654 mLocalFgService.getCameraInfo().getZoomRatioRange()); 655 setupZoomRatioSeekBar(); 656 mZoomController.setZoomRatio(mLocalFgService.getZoomRatio(), 657 ZoomController.ZOOM_UI_TOGGLE_MODE); 658 } 659 tapToFocus(MotionEvent motionEvent)660 private boolean tapToFocus(MotionEvent motionEvent) { 661 if (mLocalFgService == null || mLocalFgService.getCameraInfo() == null) { 662 return false; 663 } 664 665 float[] normalizedPoint = calculateNormalizedPoint(motionEvent); 666 667 if (isTapToResetAutoFocus(normalizedPoint)) { 668 mFocusIndicator.setVisibility(View.GONE); 669 mLocalFgService.resetToAutoFocus(); 670 } else { 671 showFocusIndicator(normalizedPoint); 672 mLocalFgService.tapToFocus(normalizedPoint); 673 } 674 675 return true; 676 } 677 678 /** 679 * Returns whether the new points overlap with the original tap-to-focus points or not. 680 */ isTapToResetAutoFocus(float[] newNormalizedPoints)681 private boolean isTapToResetAutoFocus(float[] newNormalizedPoints) { 682 float[] oldNormalizedPoints = mLocalFgService.getTapToFocusPoints(); 683 684 if (oldNormalizedPoints == null) { 685 return false; 686 } 687 688 // Calculates the distance between the new and old points 689 float distanceX = Math.abs(newNormalizedPoints[1] - oldNormalizedPoints[1]) 690 * mTextureViewCard.getWidth(); 691 float distanceY = Math.abs(newNormalizedPoints[0] - oldNormalizedPoints[0]) 692 * mTextureViewCard.getHeight(); 693 double distance = Math.sqrt(distanceX*distanceX + distanceY*distanceY); 694 695 int indicatorRadius = getResources().getDimensionPixelSize(R.dimen.focus_indicator_size) 696 / 2; 697 698 // Checks whether the distance is less than the circle radius of focus indicator 699 return indicatorRadius >= distance; 700 } 701 702 /** 703 * Calculates the normalized point which will be the point between [0, 0] to [1, 1] mapping to 704 * the preview size. 705 */ calculateNormalizedPoint(MotionEvent motionEvent)706 private float[] calculateNormalizedPoint(MotionEvent motionEvent) { 707 return new float[]{motionEvent.getX() / mPreviewSize.getWidth(), 708 motionEvent.getY() / mPreviewSize.getHeight()}; 709 } 710 711 /** 712 * Show the focus indicator and hide it automatically after a proper duration. 713 */ showFocusIndicator(float[] normalizedPoint)714 private void showFocusIndicator(float[] normalizedPoint) { 715 int indicatorSize = getResources().getDimensionPixelSize(R.dimen.focus_indicator_size); 716 float translationX = 717 normalizedPoint[0] * mTextureViewCard.getWidth() - indicatorSize / 2f; 718 float translationY = normalizedPoint[1] * mTextureViewCard.getHeight() 719 - indicatorSize / 2f; 720 mFocusIndicator.setTranslationX(translationX); 721 mFocusIndicator.setTranslationY(translationY); 722 mFocusIndicator.setVisibility(View.VISIBLE); 723 } 724 createSelectorItemDataList()725 private List<SelectorListItemData> createSelectorItemDataList() { 726 List<SelectorListItemData> selectorItemDataList = new ArrayList<>(); 727 addSelectorItemDataByLensFacing(selectorItemDataList, 728 CameraCharacteristics.LENS_FACING_BACK); 729 addSelectorItemDataByLensFacing(selectorItemDataList, 730 CameraCharacteristics.LENS_FACING_FRONT); 731 732 return selectorItemDataList; 733 } 734 addSelectorItemDataByLensFacing(List<SelectorListItemData> selectorItemDataList, int targetLensFacing)735 private void addSelectorItemDataByLensFacing(List<SelectorListItemData> selectorItemDataList, 736 int targetLensFacing) { 737 if (mLocalFgService == null) { 738 return; 739 } 740 741 boolean lensFacingHeaderAdded = false; 742 743 for (CameraId cameraId : mLocalFgService.getAvailableCameraIds()) { 744 CameraInfo cameraInfo = mLocalFgService.getOrCreateCameraInfo(cameraId); 745 746 if (cameraInfo.getLensFacing() == targetLensFacing) { 747 if (!lensFacingHeaderAdded) { 748 selectorItemDataList.add( 749 SelectorListItemData.createHeaderItemData(targetLensFacing)); 750 lensFacingHeaderAdded = true; 751 } 752 753 selectorItemDataList.add(SelectorListItemData.createCameraItemData( 754 cameraInfo)); 755 } 756 757 } 758 } 759 } 760