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