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