Home | History | Annotate | Download | only in camera
      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