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.os.Build;
28import android.view.GestureDetector;
29import android.view.MotionEvent;
30import android.view.View;
31import android.view.ViewGroup;
32import android.widget.FrameLayout;
33import android.widget.ImageView;
34
35import com.android.camera.FocusOverlayManager.FocusUI;
36import com.android.camera.debug.DebugPropertyHelper;
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.ApiHelper;
43import com.android.camera.util.CameraUtil;
44import com.android.camera.util.GservicesHelper;
45import com.android.camera.widget.AspectRatioDialogLayout;
46import com.android.camera.widget.AspectRatioSelector;
47import com.android.camera.widget.LocationDialogLayout;
48import com.android.camera2.R;
49import com.android.ex.camera2.portability.CameraAgent;
50import com.android.ex.camera2.portability.CameraCapabilities;
51import com.android.ex.camera2.portability.CameraSettings;
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;
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