1/*
2 * Copyright (C) 2015 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.messaging.ui.mediapicker;
18
19import android.Manifest;
20import android.content.Context;
21import android.content.pm.PackageManager;
22import android.graphics.Rect;
23import android.hardware.Camera;
24import android.net.Uri;
25import android.os.SystemClock;
26import android.view.LayoutInflater;
27import android.view.MotionEvent;
28import android.view.View;
29import android.view.ViewGroup;
30import android.view.animation.AlphaAnimation;
31import android.view.animation.Animation;
32import android.view.animation.AnimationSet;
33import android.widget.Chronometer;
34import android.widget.ImageButton;
35
36import com.android.messaging.R;
37import com.android.messaging.datamodel.data.MediaPickerMessagePartData;
38import com.android.messaging.ui.mediapicker.CameraManager.MediaCallback;
39import com.android.messaging.ui.mediapicker.camerafocus.RenderOverlay;
40import com.android.messaging.util.Assert;
41import com.android.messaging.util.LogUtil;
42import com.android.messaging.util.OsUtil;
43import com.android.messaging.util.UiUtils;
44
45/**
46 * Chooser which allows the user to take pictures or video without leaving the current app/activity
47 */
48class CameraMediaChooser extends MediaChooser implements
49        CameraManager.CameraManagerListener {
50    private CameraPreview.CameraPreviewHost mCameraPreviewHost;
51    private ImageButton mFullScreenButton;
52    private ImageButton mSwapCameraButton;
53    private ImageButton mSwapModeButton;
54    private ImageButton mCaptureButton;
55    private ImageButton mCancelVideoButton;
56    private Chronometer mVideoCounter;
57    private boolean mVideoCancelled;
58    private int mErrorToast;
59    private View mEnabledView;
60    private View mMissingPermissionView;
61
62    CameraMediaChooser(final MediaPicker mediaPicker) {
63        super(mediaPicker);
64    }
65
66    @Override
67    public int getSupportedMediaTypes() {
68        if (CameraManager.get().hasAnyCamera()) {
69            return MediaPicker.MEDIA_TYPE_IMAGE | MediaPicker.MEDIA_TYPE_VIDEO;
70        } else {
71            return MediaPicker.MEDIA_TYPE_NONE;
72        }
73    }
74
75    @Override
76    public View destroyView() {
77        CameraManager.get().closeCamera();
78        CameraManager.get().setListener(null);
79        CameraManager.get().setSubscriptionDataProvider(null);
80        return super.destroyView();
81    }
82
83    @Override
84    protected View createView(final ViewGroup container) {
85        CameraManager.get().setListener(this);
86        CameraManager.get().setSubscriptionDataProvider(this);
87        CameraManager.get().setVideoMode(false);
88        final LayoutInflater inflater = getLayoutInflater();
89        final CameraMediaChooserView view = (CameraMediaChooserView) inflater.inflate(
90                R.layout.mediapicker_camera_chooser,
91                container /* root */,
92                false /* attachToRoot */);
93        mCameraPreviewHost = (CameraPreview.CameraPreviewHost) view.findViewById(
94                R.id.camera_preview);
95        mCameraPreviewHost.getView().setOnTouchListener(new View.OnTouchListener() {
96            @Override
97            public boolean onTouch(final View view, final MotionEvent motionEvent) {
98                if (CameraManager.get().isVideoMode()) {
99                    // Prevent the swipe down in video mode because video is always captured in
100                    // full screen
101                    return true;
102                }
103
104                return false;
105            }
106        });
107
108        final View shutterVisual = view.findViewById(R.id.camera_shutter_visual);
109
110        mFullScreenButton = (ImageButton) view.findViewById(R.id.camera_fullScreen_button);
111        mFullScreenButton.setOnClickListener(new View.OnClickListener() {
112            @Override
113            public void onClick(final View view) {
114                mMediaPicker.setFullScreen(true);
115            }
116        });
117
118        mSwapCameraButton = (ImageButton) view.findViewById(R.id.camera_swapCamera_button);
119        mSwapCameraButton.setOnClickListener(new View.OnClickListener() {
120            @Override
121            public void onClick(final View view) {
122                CameraManager.get().swapCamera();
123            }
124        });
125
126        mCaptureButton = (ImageButton) view.findViewById(R.id.camera_capture_button);
127        mCaptureButton.setOnClickListener(new View.OnClickListener() {
128            @Override
129            public void onClick(final View v) {
130                final float heightPercent = Math.min(mMediaPicker.getViewPager().getHeight() /
131                        (float) mCameraPreviewHost.getView().getHeight(), 1);
132
133                if (CameraManager.get().isRecording()) {
134                    CameraManager.get().stopVideo();
135                } else {
136                    final CameraManager.MediaCallback callback = new CameraManager.MediaCallback() {
137                        @Override
138                        public void onMediaReady(
139                                final Uri uriToVideo, final String contentType,
140                                final int width, final int height) {
141                            mVideoCounter.stop();
142                            if (mVideoCancelled || uriToVideo == null) {
143                                mVideoCancelled = false;
144                            } else {
145                                final Rect startRect = new Rect();
146                                // It's possible to throw out the chooser while taking the
147                                // picture/video.  In that case, still use the attachment, just
148                                // skip the startRect
149                                if (mView != null) {
150                                    mView.getGlobalVisibleRect(startRect);
151                                }
152                                mMediaPicker.dispatchItemsSelected(
153                                        new MediaPickerMessagePartData(startRect, contentType,
154                                                uriToVideo, width, height),
155                                        true /* dismissMediaPicker */);
156                            }
157                            updateViewState();
158                        }
159
160                        @Override
161                        public void onMediaFailed(final Exception exception) {
162                            UiUtils.showToastAtBottom(R.string.camera_media_failure);
163                            updateViewState();
164                        }
165
166                        @Override
167                        public void onMediaInfo(final int what) {
168                            if (what == MediaCallback.MEDIA_NO_DATA) {
169                                UiUtils.showToastAtBottom(R.string.camera_media_failure);
170                            }
171                            updateViewState();
172                        }
173                    };
174                    if (CameraManager.get().isVideoMode()) {
175                        CameraManager.get().startVideo(callback);
176                        mVideoCounter.setBase(SystemClock.elapsedRealtime());
177                        mVideoCounter.start();
178                        updateViewState();
179                    } else {
180                        showShutterEffect(shutterVisual);
181                        CameraManager.get().takePicture(heightPercent, callback);
182                        updateViewState();
183                    }
184                }
185            }
186        });
187
188        mSwapModeButton = (ImageButton) view.findViewById(R.id.camera_swap_mode_button);
189        mSwapModeButton.setOnClickListener(new View.OnClickListener() {
190            @Override
191            public void onClick(final View view) {
192                final boolean isSwitchingToVideo = !CameraManager.get().isVideoMode();
193                if (isSwitchingToVideo && !OsUtil.hasRecordAudioPermission()) {
194                    requestRecordAudioPermission();
195                } else {
196                    onSwapMode();
197                }
198            }
199        });
200
201        mCancelVideoButton = (ImageButton) view.findViewById(R.id.camera_cancel_button);
202        mCancelVideoButton.setOnClickListener(new View.OnClickListener() {
203            @Override
204            public void onClick(final View view) {
205                mVideoCancelled = true;
206                CameraManager.get().stopVideo();
207                mMediaPicker.dismiss(true);
208            }
209        });
210
211        mVideoCounter = (Chronometer) view.findViewById(R.id.camera_video_counter);
212
213        CameraManager.get().setRenderOverlay((RenderOverlay) view.findViewById(R.id.focus_visual));
214
215        mEnabledView = view.findViewById(R.id.mediapicker_enabled);
216        mMissingPermissionView = view.findViewById(R.id.missing_permission_view);
217
218        // Must set mView before calling updateViewState because it operates on mView
219        mView = view;
220        updateViewState();
221        updateForPermissionState(CameraManager.hasCameraPermission());
222        return view;
223    }
224
225    @Override
226    public int getIconResource() {
227        return R.drawable.ic_camera_light;
228    }
229
230    @Override
231    public int getIconDescriptionResource() {
232        return R.string.mediapicker_cameraChooserDescription;
233    }
234
235    /**
236     * Updates the view when entering or leaving full-screen camera mode
237     * @param fullScreen
238     */
239    @Override
240    void onFullScreenChanged(final boolean fullScreen) {
241        super.onFullScreenChanged(fullScreen);
242        if (!fullScreen && CameraManager.get().isVideoMode()) {
243            CameraManager.get().setVideoMode(false);
244        }
245        updateViewState();
246    }
247
248    /**
249     * Initializes the control to a default state when it is opened / closed
250     * @param open True if the control is opened
251     */
252    @Override
253    void onOpenedChanged(final boolean open) {
254        super.onOpenedChanged(open);
255        updateViewState();
256    }
257
258    @Override
259    protected void setSelected(final boolean selected) {
260        super.setSelected(selected);
261        if (selected) {
262            if (CameraManager.hasCameraPermission()) {
263                // If an error occurred before the chooser was selected, show it now
264                showErrorToastIfNeeded();
265            } else {
266                requestCameraPermission();
267            }
268        }
269    }
270
271    private void requestCameraPermission() {
272        mMediaPicker.requestPermissions(new String[] { Manifest.permission.CAMERA },
273                MediaPicker.CAMERA_PERMISSION_REQUEST_CODE);
274    }
275
276    private void requestRecordAudioPermission() {
277        mMediaPicker.requestPermissions(new String[] { Manifest.permission.RECORD_AUDIO },
278                MediaPicker.RECORD_AUDIO_PERMISSION_REQUEST_CODE);
279    }
280
281    @Override
282    protected void onRequestPermissionsResult(
283            final int requestCode, final String permissions[], final int[] grantResults) {
284        if (requestCode == MediaPicker.CAMERA_PERMISSION_REQUEST_CODE) {
285            final boolean permissionGranted = grantResults[0] == PackageManager.PERMISSION_GRANTED;
286            updateForPermissionState(permissionGranted);
287            if (permissionGranted) {
288                mCameraPreviewHost.onCameraPermissionGranted();
289            }
290        } else if (requestCode == MediaPicker.RECORD_AUDIO_PERMISSION_REQUEST_CODE) {
291            Assert.isFalse(CameraManager.get().isVideoMode());
292            if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
293                // Switch to video mode
294                onSwapMode();
295            } else {
296                // Stay in still-photo mode
297            }
298        }
299    }
300
301    private void updateForPermissionState(final boolean granted) {
302        // onRequestPermissionsResult can sometimes get called before createView().
303        if (mEnabledView == null) {
304            return;
305        }
306
307        mEnabledView.setVisibility(granted ? View.VISIBLE : View.GONE);
308        mMissingPermissionView.setVisibility(granted ? View.GONE : View.VISIBLE);
309    }
310
311    @Override
312    public boolean canSwipeDown() {
313        if (CameraManager.get().isVideoMode()) {
314            return true;
315        }
316        return super.canSwipeDown();
317    }
318
319    /**
320     * Handles an error from the camera manager by showing the appropriate error message to the user
321     * @param errorCode One of the CameraManager.ERROR_* constants
322     * @param e The exception which caused the error, if any
323     */
324    @Override
325    public void onCameraError(final int errorCode, final Exception e) {
326        switch (errorCode) {
327            case CameraManager.ERROR_OPENING_CAMERA:
328            case CameraManager.ERROR_SHOWING_PREVIEW:
329                mErrorToast = R.string.camera_error_opening;
330                break;
331            case CameraManager.ERROR_INITIALIZING_VIDEO:
332                mErrorToast = R.string.camera_error_video_init_fail;
333                updateViewState();
334                break;
335            case CameraManager.ERROR_STORAGE_FAILURE:
336                mErrorToast = R.string.camera_error_storage_fail;
337                updateViewState();
338                break;
339            case CameraManager.ERROR_TAKING_PICTURE:
340                mErrorToast = R.string.camera_error_failure_taking_picture;
341                break;
342            default:
343                mErrorToast = R.string.camera_error_unknown;
344                LogUtil.w(LogUtil.BUGLE_TAG, "Unknown camera error:" + errorCode);
345                break;
346        }
347        showErrorToastIfNeeded();
348    }
349
350    private void showErrorToastIfNeeded() {
351        if (mErrorToast != 0 && mSelected) {
352            UiUtils.showToastAtBottom(mErrorToast);
353            mErrorToast = 0;
354        }
355    }
356
357    @Override
358    public void onCameraChanged() {
359        updateViewState();
360    }
361
362    private void onSwapMode() {
363        CameraManager.get().setVideoMode(!CameraManager.get().isVideoMode());
364        if (CameraManager.get().isVideoMode()) {
365            mMediaPicker.setFullScreen(true);
366
367            // For now we start recording immediately
368            mCaptureButton.performClick();
369        }
370        updateViewState();
371    }
372
373    private void showShutterEffect(final View shutterVisual) {
374        final float maxAlpha = getContext().getResources().getFraction(
375                R.fraction.camera_shutter_max_alpha, 1 /* base */, 1 /* pBase */);
376
377        // Divide by 2 so each half of the animation adds up to the full duration
378        final int animationDuration = getContext().getResources().getInteger(
379                R.integer.camera_shutter_duration) / 2;
380
381        final AnimationSet animation = new AnimationSet(false /* shareInterpolator */);
382        final Animation alphaInAnimation = new AlphaAnimation(0.0f, maxAlpha);
383        alphaInAnimation.setDuration(animationDuration);
384        animation.addAnimation(alphaInAnimation);
385
386        final Animation alphaOutAnimation = new AlphaAnimation(maxAlpha, 0.0f);
387        alphaOutAnimation.setStartOffset(animationDuration);
388        alphaOutAnimation.setDuration(animationDuration);
389        animation.addAnimation(alphaOutAnimation);
390
391        animation.setAnimationListener(new Animation.AnimationListener() {
392            @Override
393            public void onAnimationStart(final Animation animation) {
394                shutterVisual.setVisibility(View.VISIBLE);
395            }
396
397            @Override
398            public void onAnimationEnd(final Animation animation) {
399                shutterVisual.setVisibility(View.GONE);
400            }
401
402            @Override
403            public void onAnimationRepeat(final Animation animation) {
404            }
405        });
406        shutterVisual.startAnimation(animation);
407    }
408
409    /** Updates the state of the buttons and overlays based on the current state of the view */
410    private void updateViewState() {
411        if (mView == null) {
412            return;
413        }
414
415        final Context context = getContext();
416        if (context == null) {
417            // Context is null if the fragment was already removed from the activity
418            return;
419        }
420        final boolean fullScreen = mMediaPicker.isFullScreen();
421        final boolean videoMode = CameraManager.get().isVideoMode();
422        final boolean isRecording = CameraManager.get().isRecording();
423        final boolean isCameraAvailable = isCameraAvailable();
424        final Camera.CameraInfo cameraInfo = CameraManager.get().getCameraInfo();
425        final boolean frontCamera = cameraInfo != null && cameraInfo.facing ==
426                Camera.CameraInfo.CAMERA_FACING_FRONT;
427
428        mView.setSystemUiVisibility(
429                fullScreen ? View.SYSTEM_UI_FLAG_LOW_PROFILE :
430                View.SYSTEM_UI_FLAG_VISIBLE);
431
432        mFullScreenButton.setVisibility(!fullScreen ? View.VISIBLE : View.GONE);
433        mFullScreenButton.setEnabled(isCameraAvailable);
434        mSwapCameraButton.setVisibility(
435                fullScreen && !isRecording && CameraManager.get().hasFrontAndBackCamera() ?
436                        View.VISIBLE : View.GONE);
437        mSwapCameraButton.setImageResource(frontCamera ?
438                R.drawable.ic_camera_front_light :
439                R.drawable.ic_camera_rear_light);
440        mSwapCameraButton.setEnabled(isCameraAvailable);
441
442        mCancelVideoButton.setVisibility(isRecording ? View.VISIBLE : View.GONE);
443        mVideoCounter.setVisibility(isRecording ? View.VISIBLE : View.GONE);
444
445        mSwapModeButton.setImageResource(videoMode ?
446                R.drawable.ic_mp_camera_small_light :
447                R.drawable.ic_mp_video_small_light);
448        mSwapModeButton.setContentDescription(context.getString(videoMode ?
449                R.string.camera_switch_to_still_mode : R.string.camera_switch_to_video_mode));
450        mSwapModeButton.setVisibility(isRecording ? View.GONE : View.VISIBLE);
451        mSwapModeButton.setEnabled(isCameraAvailable);
452
453        if (isRecording) {
454            mCaptureButton.setImageResource(R.drawable.ic_mp_capture_stop_large_light);
455            mCaptureButton.setContentDescription(context.getString(
456                    R.string.camera_stop_recording));
457        } else if (videoMode) {
458            mCaptureButton.setImageResource(R.drawable.ic_mp_video_large_light);
459            mCaptureButton.setContentDescription(context.getString(
460                    R.string.camera_start_recording));
461        } else {
462            mCaptureButton.setImageResource(R.drawable.ic_checkmark_large_light);
463            mCaptureButton.setContentDescription(context.getString(
464                    R.string.camera_take_picture));
465        }
466        mCaptureButton.setEnabled(isCameraAvailable);
467    }
468
469    @Override
470    int getActionBarTitleResId() {
471        return 0;
472    }
473
474    /**
475     * Returns if the camera is currently ready camera is loaded and not taking a picture.
476     * otherwise we should avoid taking another picture, swapping camera or recording video.
477     */
478    private boolean isCameraAvailable() {
479        return CameraManager.get().isCameraAvailable();
480    }
481}
482