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