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 99 public GestureDetector.OnGestureListener getGestureListener() { 100 return mPreviewGestureListener; 101 } 102 103 @Override 104 public View.OnTouchListener getTouchListener() { 105 return null; 106 } 107 108 @Override 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 120 public boolean shouldAutoAdjustTransformMatrixOnLayout() { 121 return true; 122 } 123 124 @Override 125 public boolean shouldAutoAdjustBottomBar() { 126 return true; 127 } 128 129 @Override 130 public void onPreviewFlipped() { 131 mController.updateCameraOrientation(); 132 } 133 134 /** 135 * Sets the runnable to run when the next frame comes in. 136 */ 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 */ 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 */ 153 public void setCountdownFinishedListener(CountDownView.OnCountDownStatusListener listener) { 154 mCountdownView.setCountDownStatusListener(listener); 155 } 156 157 /** 158 * Returns whether the countdown is on-going. 159 */ 160 public boolean isCountingDown() { 161 return mCountdownView.isCountingDown(); 162 } 163 164 /** 165 * Cancels the on-going countdown, if any. 166 */ 167 public void cancelCountDown() { 168 mCountdownView.cancelCountDown(); 169 } 170 171 @Override 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 184 public DecodeTask(byte[] data, int orientation, boolean mirror) { 185 mData = data; 186 mOrientation = orientation; 187 mMirror = mirror; 188 } 189 190 @Override 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 { 209 public DecodeImageForReview(byte[] data, int orientation, boolean mirror) { 210 super(data, orientation, mirror); 211 } 212 213 @Override 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 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 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 */ 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 */ 278 public void hideIntentReviewImageView() { 279 if (mIntentReviewImageView != null) { 280 mIntentReviewImageView.setVisibility(View.INVISIBLE); 281 } 282 } 283 284 285 public FocusUI getFocusUI() { 286 return mFocusUI; 287 } 288 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 306 public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) { 307 mController.onPreviewUIReady(); 308 } 309 310 @Override 311 public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) { 312 // Ignored, Camera does all the work for us 313 } 314 315 @Override 316 public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) { 317 mController.onPreviewUIDestroyed(); 318 return true; 319 } 320 321 @Override 322 public void onSurfaceTextureUpdated(SurfaceTexture surface) { 323 if (mRunnableForNextFrame != null) { 324 mRootView.post(mRunnableForNextFrame); 325 mRunnableForNextFrame = null; 326 } 327 } 328 329 public View getRootView() { 330 return mRootView; 331 } 332 333 private void initIndicators() { 334 // TODO init toggle buttons on bottom bar here 335 } 336 337 public void onCameraOpened(CameraCapabilities capabilities, CameraSettings settings) { 338 initializeZoom(capabilities, settings); 339 } 340 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 348 public void initializeFirstTime() { 349 350 } 351 352 // called from onResume every other time 353 public void initializeSecondTime(CameraCapabilities capabilities, CameraSettings settings) { 354 initializeZoom(capabilities, settings); 355 if (mController.isImageCaptureIntent()) { 356 hidePostCaptureAlert(); 357 } 358 } 359 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 */ 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 */ 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 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 */ 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 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 475 public void animateFlash() { 476 mController.startPreCaptureAnimation(); 477 } 478 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 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 502 protected void hidePostCaptureAlert() { 503 if (mDecodeTaskForReview != null) { 504 mDecodeTaskForReview.cancel(true); 505 } 506 resumeFaceDetection(); 507 } 508 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 517 public void onZoomValueChanged(float ratio) { 518 mController.onZoomChanged(ratio); 519 } 520 521 @Override 522 public void onZoomStart() { 523 } 524 525 @Override 526 public void onZoomEnd() { 527 } 528 } 529 530 public void setSwipingEnabled(boolean enable) { 531 mActivity.setSwipingEnabled(enable); 532 } 533 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 545 public void clearFaces() { 546 if (mFaceView != null) { 547 mFaceView.clear(); 548 } 549 } 550 551 public void pauseFaceDetection() { 552 if (mFaceView != null) { 553 mFaceView.pause(); 554 } 555 } 556 557 public void resumeFaceDetection() { 558 if (mFaceView != null) { 559 mFaceView.resume(); 560 } 561 } 562 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 574 public void onFaceDetection(Face[] faces, CameraAgent.CameraProxy camera) { 575 if (mFaceView != null) { 576 mFaceView.setFaces(faces); 577 } 578 } 579 580 } 581