1 /* 2 * Copyright (C) 2012 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.camera; 18 19 import android.app.Dialog; 20 import android.content.DialogInterface; 21 import android.graphics.Bitmap; 22 import android.graphics.Matrix; 23 import android.graphics.RectF; 24 import android.graphics.SurfaceTexture; 25 import android.hardware.Camera.Face; 26 import android.os.AsyncTask; 27 import android.os.Build; 28 import android.view.GestureDetector; 29 import android.view.MotionEvent; 30 import android.view.View; 31 import android.view.ViewGroup; 32 import android.widget.FrameLayout; 33 import android.widget.ImageView; 34 35 import com.android.camera.FocusOverlayManager.FocusUI; 36 import com.android.camera.debug.DebugPropertyHelper; 37 import com.android.camera.debug.Log; 38 import com.android.camera.ui.CountDownView; 39 import com.android.camera.ui.FaceView; 40 import com.android.camera.ui.PreviewOverlay; 41 import com.android.camera.ui.PreviewStatusListener; 42 import com.android.camera.util.ApiHelper; 43 import com.android.camera.util.CameraUtil; 44 import com.android.camera.util.GservicesHelper; 45 import com.android.camera.widget.AspectRatioDialogLayout; 46 import com.android.camera.widget.AspectRatioSelector; 47 import com.android.camera.widget.LocationDialogLayout; 48 import com.android.camera2.R; 49 import com.android.ex.camera2.portability.CameraAgent; 50 import com.android.ex.camera2.portability.CameraCapabilities; 51 import com.android.ex.camera2.portability.CameraSettings; 52 53 public class PhotoUI implements PreviewStatusListener, 54 CameraAgent.CameraFaceDetectionCallback, PreviewStatusListener.PreviewAreaChangedListener { 55 56 private static final Log.Tag TAG = new Log.Tag("PhotoUI"); 57 private static final int DOWN_SAMPLE_FACTOR = 4; 58 private static final float UNSET = 0f; 59 60 private final PreviewOverlay mPreviewOverlay; 61 private final FocusUI mFocusUI; 62 private final CameraActivity mActivity; 63 private final PhotoController mController; 64 65 private final View mRootView; 66 private Dialog mDialog = null; 67 68 // TODO: Remove face view logic if UX does not bring it back within a month. 69 private final FaceView mFaceView; 70 private DecodeImageForReview mDecodeTaskForReview = null; 71 72 private float mZoomMax; 73 74 private int mPreviewWidth = 0; 75 private int mPreviewHeight = 0; 76 private float mAspectRatio = UNSET; 77 78 private ImageView mIntentReviewImageView; 79 80 private final GestureDetector.OnGestureListener mPreviewGestureListener 81 = new GestureDetector.SimpleOnGestureListener() { 82 @Override 83 public boolean onSingleTapUp(MotionEvent ev) { 84 mController.onSingleTapUp(null, (int) ev.getX(), (int) ev.getY()); 85 return true; 86 } 87 }; 88 private final DialogInterface.OnDismissListener mOnDismissListener 89 = new DialogInterface.OnDismissListener() { 90 @Override 91 public void onDismiss(DialogInterface dialog) { 92 mDialog = null; 93 } 94 }; 95 private Runnable mRunnableForNextFrame = null; 96 private final CountDownView mCountdownView; 97 98 @Override getGestureListener()99 public GestureDetector.OnGestureListener getGestureListener() { 100 return mPreviewGestureListener; 101 } 102 103 @Override getTouchListener()104 public View.OnTouchListener getTouchListener() { 105 return null; 106 } 107 108 @Override onPreviewLayoutChanged(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom)109 public void onPreviewLayoutChanged(View v, int left, int top, int right, 110 int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) { 111 int width = right - left; 112 int height = bottom - top; 113 if (mPreviewWidth != width || mPreviewHeight != height) { 114 mPreviewWidth = width; 115 mPreviewHeight = height; 116 } 117 } 118 119 @Override shouldAutoAdjustTransformMatrixOnLayout()120 public boolean shouldAutoAdjustTransformMatrixOnLayout() { 121 return true; 122 } 123 124 @Override shouldAutoAdjustBottomBar()125 public boolean shouldAutoAdjustBottomBar() { 126 return true; 127 } 128 129 @Override onPreviewFlipped()130 public void onPreviewFlipped() { 131 mController.updateCameraOrientation(); 132 } 133 134 /** 135 * Sets the runnable to run when the next frame comes in. 136 */ setRunnableForNextFrame(Runnable runnable)137 public void setRunnableForNextFrame(Runnable runnable) { 138 mRunnableForNextFrame = runnable; 139 } 140 141 /** 142 * Starts the countdown timer. 143 * 144 * @param sec seconds to countdown 145 */ startCountdown(int sec)146 public void startCountdown(int sec) { 147 mCountdownView.startCountDown(sec); 148 } 149 150 /** 151 * Sets a listener that gets notified when the countdown is finished. 152 */ setCountdownFinishedListener(CountDownView.OnCountDownStatusListener listener)153 public void setCountdownFinishedListener(CountDownView.OnCountDownStatusListener listener) { 154 mCountdownView.setCountDownStatusListener(listener); 155 } 156 157 /** 158 * Returns whether the countdown is on-going. 159 */ isCountingDown()160 public boolean isCountingDown() { 161 return mCountdownView.isCountingDown(); 162 } 163 164 /** 165 * Cancels the on-going countdown, if any. 166 */ cancelCountDown()167 public void cancelCountDown() { 168 mCountdownView.cancelCountDown(); 169 } 170 171 @Override onPreviewAreaChanged(RectF previewArea)172 public void onPreviewAreaChanged(RectF previewArea) { 173 if (mFaceView != null) { 174 mFaceView.onPreviewAreaChanged(previewArea); 175 } 176 mCountdownView.onPreviewAreaChanged(previewArea); 177 } 178 179 private class DecodeTask extends AsyncTask<Void, Void, Bitmap> { 180 private final byte [] mData; 181 private final int mOrientation; 182 private final boolean mMirror; 183 DecodeTask(byte[] data, int orientation, boolean mirror)184 public DecodeTask(byte[] data, int orientation, boolean mirror) { 185 mData = data; 186 mOrientation = orientation; 187 mMirror = mirror; 188 } 189 190 @Override doInBackground(Void... params)191 protected Bitmap doInBackground(Void... params) { 192 // Decode image in background. 193 Bitmap bitmap = CameraUtil.downSample(mData, DOWN_SAMPLE_FACTOR); 194 if (mOrientation != 0 || mMirror) { 195 Matrix m = new Matrix(); 196 if (mMirror) { 197 // Flip horizontally 198 m.setScale(-1f, 1f); 199 } 200 m.preRotate(mOrientation); 201 return Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), m, 202 false); 203 } 204 return bitmap; 205 } 206 } 207 208 private class DecodeImageForReview extends DecodeTask { DecodeImageForReview(byte[] data, int orientation, boolean mirror)209 public DecodeImageForReview(byte[] data, int orientation, boolean mirror) { 210 super(data, orientation, mirror); 211 } 212 213 @Override onPostExecute(Bitmap bitmap)214 protected void onPostExecute(Bitmap bitmap) { 215 if (isCancelled()) { 216 return; 217 } 218 219 mIntentReviewImageView.setImageBitmap(bitmap); 220 showIntentReviewImageView(); 221 222 mDecodeTaskForReview = null; 223 } 224 } 225 PhotoUI(CameraActivity activity, PhotoController controller, View parent)226 public PhotoUI(CameraActivity activity, PhotoController controller, View parent) { 227 mActivity = activity; 228 mController = controller; 229 mRootView = parent; 230 231 ViewGroup moduleRoot = (ViewGroup) mRootView.findViewById(R.id.module_layout); 232 mActivity.getLayoutInflater().inflate(R.layout.photo_module, 233 moduleRoot, true); 234 initIndicators(); 235 mFocusUI = (FocusUI) mRootView.findViewById(R.id.focus_overlay); 236 mPreviewOverlay = (PreviewOverlay) mRootView.findViewById(R.id.preview_overlay); 237 mCountdownView = (CountDownView) mRootView.findViewById(R.id.count_down_view); 238 // Show faces if we are in debug mode. 239 if (DebugPropertyHelper.showCaptureDebugUI()) { 240 mFaceView = (FaceView) mRootView.findViewById(R.id.face_view); 241 } else { 242 mFaceView = null; 243 } 244 245 if (mController.isImageCaptureIntent()) { 246 initIntentReviewImageView(); 247 } 248 } 249 initIntentReviewImageView()250 private void initIntentReviewImageView() { 251 mIntentReviewImageView = (ImageView) mRootView.findViewById(R.id.intent_review_imageview); 252 mActivity.getCameraAppUI().addPreviewAreaChangedListener( 253 new PreviewStatusListener.PreviewAreaChangedListener() { 254 @Override 255 public void onPreviewAreaChanged(RectF previewArea) { 256 FrameLayout.LayoutParams params = 257 (FrameLayout.LayoutParams) mIntentReviewImageView.getLayoutParams(); 258 params.width = (int) previewArea.width(); 259 params.height = (int) previewArea.height(); 260 params.setMargins((int) previewArea.left, (int) previewArea.top, 0, 0); 261 mIntentReviewImageView.setLayoutParams(params); 262 } 263 }); 264 } 265 266 /** 267 * Show the image review over the live preview for intent captures. 268 */ showIntentReviewImageView()269 public void showIntentReviewImageView() { 270 if (mIntentReviewImageView != null) { 271 mIntentReviewImageView.setVisibility(View.VISIBLE); 272 } 273 } 274 275 /** 276 * Hide the image review over the live preview for intent captures. 277 */ hideIntentReviewImageView()278 public void hideIntentReviewImageView() { 279 if (mIntentReviewImageView != null) { 280 mIntentReviewImageView.setVisibility(View.INVISIBLE); 281 } 282 } 283 284 getFocusUI()285 public FocusUI getFocusUI() { 286 return mFocusUI; 287 } 288 updatePreviewAspectRatio(float aspectRatio)289 public void updatePreviewAspectRatio(float aspectRatio) { 290 if (aspectRatio <= 0) { 291 Log.e(TAG, "Invalid aspect ratio: " + aspectRatio); 292 return; 293 } 294 if (aspectRatio < 1f) { 295 aspectRatio = 1f / aspectRatio; 296 } 297 298 if (mAspectRatio != aspectRatio) { 299 mAspectRatio = aspectRatio; 300 // Update transform matrix with the new aspect ratio. 301 mController.updatePreviewAspectRatio(mAspectRatio); 302 } 303 } 304 305 @Override onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height)306 public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) { 307 mController.onPreviewUIReady(); 308 } 309 310 @Override onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height)311 public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) { 312 // Ignored, Camera does all the work for us 313 } 314 315 @Override onSurfaceTextureDestroyed(SurfaceTexture surface)316 public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) { 317 mController.onPreviewUIDestroyed(); 318 return true; 319 } 320 321 @Override onSurfaceTextureUpdated(SurfaceTexture surface)322 public void onSurfaceTextureUpdated(SurfaceTexture surface) { 323 if (mRunnableForNextFrame != null) { 324 mRootView.post(mRunnableForNextFrame); 325 mRunnableForNextFrame = null; 326 } 327 } 328 getRootView()329 public View getRootView() { 330 return mRootView; 331 } 332 initIndicators()333 private void initIndicators() { 334 // TODO init toggle buttons on bottom bar here 335 } 336 onCameraOpened(CameraCapabilities capabilities, CameraSettings settings)337 public void onCameraOpened(CameraCapabilities capabilities, CameraSettings settings) { 338 initializeZoom(capabilities, settings); 339 } 340 animateCapture(final byte[] jpegData, int orientation, boolean mirror)341 public void animateCapture(final byte[] jpegData, int orientation, boolean mirror) { 342 // Decode jpeg byte array and then animate the jpeg 343 DecodeTask task = new DecodeTask(jpegData, orientation, mirror); 344 task.execute(); 345 } 346 347 // called from onResume but only the first time initializeFirstTime()348 public void initializeFirstTime() { 349 350 } 351 352 // called from onResume every other time initializeSecondTime(CameraCapabilities capabilities, CameraSettings settings)353 public void initializeSecondTime(CameraCapabilities capabilities, CameraSettings settings) { 354 initializeZoom(capabilities, settings); 355 if (mController.isImageCaptureIntent()) { 356 hidePostCaptureAlert(); 357 } 358 } 359 showLocationAndAspectRatioDialog( final PhotoModule.LocationDialogCallback locationCallback, final PhotoModule.AspectRatioDialogCallback aspectRatioDialogCallback)360 public void showLocationAndAspectRatioDialog( 361 final PhotoModule.LocationDialogCallback locationCallback, 362 final PhotoModule.AspectRatioDialogCallback aspectRatioDialogCallback) { 363 setDialog(new Dialog(mActivity, 364 android.R.style.Theme_Black_NoTitleBar_Fullscreen)); 365 final LocationDialogLayout locationDialogLayout = (LocationDialogLayout) mActivity 366 .getLayoutInflater().inflate(R.layout.location_dialog_layout, null); 367 locationDialogLayout.setLocationTaggingSelectionListener( 368 new LocationDialogLayout.LocationTaggingSelectionListener() { 369 @Override 370 public void onLocationTaggingSelected(boolean selected) { 371 // Update setting. 372 locationCallback.onLocationTaggingSelected(selected); 373 374 if (showAspectRatioDialogOnThisDevice()) { 375 // Go to next page. 376 showAspectRatioDialog(aspectRatioDialogCallback, mDialog); 377 } else { 378 // If we don't want to show the aspect ratio dialog, 379 // dismiss the dialog right after the user chose the 380 // location setting. 381 if (mDialog != null) { 382 mDialog.dismiss(); 383 } 384 } 385 } 386 }); 387 mDialog.setContentView(locationDialogLayout, new ViewGroup.LayoutParams( 388 ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); 389 mDialog.show(); 390 } 391 392 /** 393 * Dismisses previous dialog if any, sets current dialog to the given dialog, 394 * and set the on dismiss listener for the given dialog. 395 * @param dialog dialog to show 396 */ setDialog(Dialog dialog)397 private void setDialog(Dialog dialog) { 398 if (mDialog != null) { 399 mDialog.setOnDismissListener(null); 400 mDialog.dismiss(); 401 } 402 mDialog = dialog; 403 if (mDialog != null) { 404 mDialog.setOnDismissListener(mOnDismissListener); 405 } 406 } 407 408 /** 409 * @return Whether the dialog was shown. 410 */ showAspectRatioDialog(final PhotoModule.AspectRatioDialogCallback callback)411 public boolean showAspectRatioDialog(final PhotoModule.AspectRatioDialogCallback callback) { 412 if (showAspectRatioDialogOnThisDevice()) { 413 setDialog(new Dialog(mActivity, android.R.style.Theme_Black_NoTitleBar_Fullscreen)); 414 showAspectRatioDialog(callback, mDialog); 415 return true; 416 } else { 417 return false; 418 } 419 } 420 showAspectRatioDialog(final PhotoModule.AspectRatioDialogCallback callback, final Dialog aspectRatioDialog)421 private boolean showAspectRatioDialog(final PhotoModule.AspectRatioDialogCallback callback, 422 final Dialog aspectRatioDialog) { 423 if (aspectRatioDialog == null) { 424 Log.e(TAG, "Dialog for aspect ratio is null."); 425 return false; 426 } 427 final AspectRatioDialogLayout aspectRatioDialogLayout = 428 (AspectRatioDialogLayout) mActivity 429 .getLayoutInflater().inflate(R.layout.aspect_ratio_dialog_layout, null); 430 aspectRatioDialogLayout.initialize( 431 new AspectRatioDialogLayout.AspectRatioChangedListener() { 432 @Override 433 public void onAspectRatioChanged(AspectRatioSelector.AspectRatio aspectRatio) { 434 // callback to set picture size. 435 callback.onAspectRatioSelected(aspectRatio, new Runnable() { 436 @Override 437 public void run() { 438 if (mDialog != null) { 439 mDialog.dismiss(); 440 } 441 } 442 }); 443 } 444 }, callback.getCurrentAspectRatio()); 445 aspectRatioDialog.setContentView(aspectRatioDialogLayout, new ViewGroup.LayoutParams( 446 ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); 447 aspectRatioDialog.show(); 448 return true; 449 } 450 451 /** 452 * @return Whether this is a device that we should show the aspect ratio 453 * intro dialog on. 454 */ showAspectRatioDialogOnThisDevice()455 private boolean showAspectRatioDialogOnThisDevice() { 456 // We only want to show that dialog on N4/N5/N6 457 // Don't show if using API2 portability, b/17462976 458 return !GservicesHelper.useCamera2ApiThroughPortabilityLayer(mActivity) && 459 (ApiHelper.IS_NEXUS_4 || ApiHelper.IS_NEXUS_5 || ApiHelper.IS_NEXUS_6); 460 } 461 initializeZoom(CameraCapabilities capabilities, CameraSettings settings)462 public void initializeZoom(CameraCapabilities capabilities, CameraSettings settings) { 463 if ((capabilities == null) || settings == null || 464 !capabilities.supports(CameraCapabilities.Feature.ZOOM)) { 465 return; 466 } 467 mZoomMax = capabilities.getMaxZoomRatio(); 468 // Currently we use immediate zoom for fast zooming to get better UX and 469 // there is no plan to take advantage of the smooth zoom. 470 // TODO: Need to setup a path to AppUI to do this 471 mPreviewOverlay.setupZoom(mZoomMax, settings.getCurrentZoomRatio(), 472 new ZoomChangeListener()); 473 } 474 animateFlash()475 public void animateFlash() { 476 mController.startPreCaptureAnimation(); 477 } 478 onBackPressed()479 public boolean onBackPressed() { 480 // In image capture mode, back button should: 481 // 1) if there is any popup, dismiss them, 2) otherwise, get out of 482 // image capture 483 if (mController.isImageCaptureIntent()) { 484 mController.onCaptureCancelled(); 485 return true; 486 } else if (!mController.isCameraIdle()) { 487 // ignore backs while we're taking a picture 488 return true; 489 } else { 490 return false; 491 } 492 } 493 showCapturedImageForReview(byte[] jpegData, int orientation, boolean mirror)494 protected void showCapturedImageForReview(byte[] jpegData, int orientation, boolean mirror) { 495 mDecodeTaskForReview = new DecodeImageForReview(jpegData, orientation, mirror); 496 mDecodeTaskForReview.execute(); 497 498 mActivity.getCameraAppUI().transitionToIntentReviewLayout(); 499 pauseFaceDetection(); 500 } 501 hidePostCaptureAlert()502 protected void hidePostCaptureAlert() { 503 if (mDecodeTaskForReview != null) { 504 mDecodeTaskForReview.cancel(true); 505 } 506 resumeFaceDetection(); 507 } 508 setDisplayOrientation(int orientation)509 public void setDisplayOrientation(int orientation) { 510 if (mFaceView != null) { 511 mFaceView.setDisplayOrientation(orientation); 512 } 513 } 514 515 private class ZoomChangeListener implements PreviewOverlay.OnZoomChangedListener { 516 @Override onZoomValueChanged(float ratio)517 public void onZoomValueChanged(float ratio) { 518 mController.onZoomChanged(ratio); 519 } 520 521 @Override onZoomStart()522 public void onZoomStart() { 523 } 524 525 @Override onZoomEnd()526 public void onZoomEnd() { 527 } 528 } 529 setSwipingEnabled(boolean enable)530 public void setSwipingEnabled(boolean enable) { 531 mActivity.setSwipingEnabled(enable); 532 } 533 onPause()534 public void onPause() { 535 if (mFaceView != null) { 536 mFaceView.clear(); 537 } 538 if (mDialog != null) { 539 mDialog.dismiss(); 540 } 541 // recalculate aspect ratio when restarting. 542 mAspectRatio = 0.0f; 543 } 544 clearFaces()545 public void clearFaces() { 546 if (mFaceView != null) { 547 mFaceView.clear(); 548 } 549 } 550 pauseFaceDetection()551 public void pauseFaceDetection() { 552 if (mFaceView != null) { 553 mFaceView.pause(); 554 } 555 } 556 resumeFaceDetection()557 public void resumeFaceDetection() { 558 if (mFaceView != null) { 559 mFaceView.resume(); 560 } 561 } 562 onStartFaceDetection(int orientation, boolean mirror)563 public void onStartFaceDetection(int orientation, boolean mirror) { 564 if (mFaceView != null) { 565 mFaceView.clear(); 566 mFaceView.setVisibility(View.VISIBLE); 567 mFaceView.setDisplayOrientation(orientation); 568 mFaceView.setMirror(mirror); 569 mFaceView.resume(); 570 } 571 } 572 573 @Override onFaceDetection(Face[] faces, CameraAgent.CameraProxy camera)574 public void onFaceDetection(Face[] faces, CameraAgent.CameraProxy camera) { 575 if (mFaceView != null) { 576 mFaceView.setFaces(faces); 577 } 578 } 579 580 } 581