• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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