1 /* 2 * Copyright (C) 2016 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 17 package com.android.dialer.callcomposer; 18 19 import android.Manifest; 20 import android.content.Intent; 21 import android.content.pm.PackageManager; 22 import android.graphics.drawable.Animatable; 23 import android.hardware.Camera.CameraInfo; 24 import android.net.Uri; 25 import android.os.Bundle; 26 import android.provider.Settings; 27 import android.support.annotation.NonNull; 28 import android.support.annotation.Nullable; 29 import android.support.v4.content.ContextCompat; 30 import android.view.LayoutInflater; 31 import android.view.View; 32 import android.view.View.OnClickListener; 33 import android.view.ViewGroup; 34 import android.view.animation.AlphaAnimation; 35 import android.view.animation.Animation; 36 import android.view.animation.AnimationSet; 37 import android.widget.ImageButton; 38 import android.widget.ImageView; 39 import android.widget.ProgressBar; 40 import android.widget.TextView; 41 import android.widget.Toast; 42 import com.android.dialer.callcomposer.camera.CameraManager; 43 import com.android.dialer.callcomposer.camera.CameraManager.CameraManagerListener; 44 import com.android.dialer.callcomposer.camera.CameraManager.MediaCallback; 45 import com.android.dialer.callcomposer.camera.CameraPreview.CameraPreviewHost; 46 import com.android.dialer.callcomposer.camera.camerafocus.RenderOverlay; 47 import com.android.dialer.callcomposer.cameraui.CameraMediaChooserView; 48 import com.android.dialer.common.Assert; 49 import com.android.dialer.common.LogUtil; 50 import com.android.dialer.logging.DialerImpression; 51 import com.android.dialer.logging.Logger; 52 import com.android.dialer.util.PermissionsUtil; 53 54 /** Fragment used to compose call with image from the user's camera. */ 55 public class CameraComposerFragment extends CallComposerFragment 56 implements CameraManagerListener, OnClickListener, CameraManager.MediaCallback { 57 58 private static final String CAMERA_DIRECTION_KEY = "camera_direction"; 59 private static final String CAMERA_URI_KEY = "camera_key"; 60 61 private View permissionView; 62 private ImageButton exitFullscreen; 63 private ImageButton fullscreen; 64 private ImageButton swapCamera; 65 private ImageButton capture; 66 private ImageButton cancel; 67 private CameraMediaChooserView cameraView; 68 private RenderOverlay focus; 69 private View shutter; 70 private View allowPermission; 71 private CameraPreviewHost preview; 72 private ProgressBar loading; 73 private ImageView previewImageView; 74 75 private Uri cameraUri; 76 private boolean processingUri; 77 private String[] permissions = new String[] {Manifest.permission.CAMERA}; 78 private CameraUriCallback uriCallback; 79 private int cameraDirection = CameraInfo.CAMERA_FACING_BACK; 80 newInstance()81 public static CameraComposerFragment newInstance() { 82 return new CameraComposerFragment(); 83 } 84 85 @Nullable 86 @Override onCreateView( LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle bundle)87 public View onCreateView( 88 LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle bundle) { 89 View root = inflater.inflate(R.layout.fragment_camera_composer, container, false); 90 permissionView = root.findViewById(R.id.permission_view); 91 loading = root.findViewById(R.id.loading); 92 cameraView = root.findViewById(R.id.camera_view); 93 shutter = cameraView.findViewById(R.id.camera_shutter_visual); 94 exitFullscreen = cameraView.findViewById(R.id.camera_exit_fullscreen); 95 fullscreen = cameraView.findViewById(R.id.camera_fullscreen); 96 swapCamera = cameraView.findViewById(R.id.swap_camera_button); 97 capture = cameraView.findViewById(R.id.camera_capture_button); 98 cancel = cameraView.findViewById(R.id.camera_cancel_button); 99 focus = cameraView.findViewById(R.id.focus_visual); 100 preview = cameraView.findViewById(R.id.camera_preview); 101 previewImageView = root.findViewById(R.id.preview_image_view); 102 103 exitFullscreen.setOnClickListener(this); 104 fullscreen.setOnClickListener(this); 105 swapCamera.setOnClickListener(this); 106 capture.setOnClickListener(this); 107 cancel.setOnClickListener(this); 108 109 110 if (!PermissionsUtil.hasCameraPermissions(getContext())) { 111 LogUtil.i("CameraComposerFragment.onCreateView", "Permission view shown."); 112 Logger.get(getContext()).logImpression(DialerImpression.Type.CAMERA_PERMISSION_DISPLAYED); 113 ImageView permissionImage = permissionView.findViewById(R.id.permission_icon); 114 TextView permissionText = permissionView.findViewById(R.id.permission_text); 115 allowPermission = permissionView.findViewById(R.id.allow); 116 117 allowPermission.setOnClickListener(this); 118 permissionText.setText(R.string.camera_permission_text); 119 permissionImage.setImageResource(R.drawable.quantum_ic_camera_alt_white_48); 120 permissionImage.setColorFilter( 121 ContextCompat.getColor(getContext(), R.color.dialer_theme_color)); 122 permissionView.setVisibility(View.VISIBLE); 123 } else { 124 if (bundle != null) { 125 cameraDirection = bundle.getInt(CAMERA_DIRECTION_KEY); 126 cameraUri = bundle.getParcelable(CAMERA_URI_KEY); 127 } 128 setupCamera(); 129 } 130 return root; 131 } 132 setupCamera()133 private void setupCamera() { 134 if (!PermissionsUtil.hasCameraPrivacyToastShown(getContext())) { 135 PermissionsUtil.showCameraPermissionToast(getContext()); 136 } 137 CameraManager.get().setListener(this); 138 preview.setShown(); 139 CameraManager.get().setRenderOverlay(focus); 140 CameraManager.get().selectCamera(cameraDirection); 141 setCameraUri(cameraUri); 142 } 143 144 @Override onCameraError(int errorCode, Exception exception)145 public void onCameraError(int errorCode, Exception exception) { 146 LogUtil.e("CameraComposerFragment.onCameraError", "errorCode: ", errorCode, exception); 147 } 148 149 @Override onCameraChanged()150 public void onCameraChanged() { 151 updateViewState(); 152 } 153 154 @Override shouldHide()155 public boolean shouldHide() { 156 return !processingUri && cameraUri == null; 157 } 158 159 @Override clearComposer()160 public void clearComposer() { 161 processingUri = false; 162 setCameraUri(null); 163 } 164 165 @Override onClick(View view)166 public void onClick(View view) { 167 if (view == capture) { 168 float heightPercent = 1; 169 if (!getListener().isFullscreen() && !getListener().isLandscapeLayout()) { 170 heightPercent = Math.min((float) cameraView.getHeight() / preview.getView().getHeight(), 1); 171 } 172 173 showShutterEffect(shutter); 174 processingUri = true; 175 setCameraUri(null); 176 focus.getPieRenderer().clear(); 177 CameraManager.get().takePicture(heightPercent, this); 178 } else if (view == swapCamera) { 179 ((Animatable) swapCamera.getDrawable()).start(); 180 CameraManager.get().swapCamera(); 181 cameraDirection = CameraManager.get().getCameraInfo().facing; 182 } else if (view == cancel) { 183 clearComposer(); 184 } else if (view == exitFullscreen) { 185 getListener().showFullscreen(false); 186 fullscreen.setVisibility(View.VISIBLE); 187 exitFullscreen.setVisibility(View.GONE); 188 } else if (view == fullscreen) { 189 getListener().showFullscreen(true); 190 fullscreen.setVisibility(View.GONE); 191 exitFullscreen.setVisibility(View.VISIBLE); 192 } else if (view == allowPermission) { 193 // Checks to see if the user has permanently denied this permission. If this is the first 194 // time seeing this permission or they only pressed deny previously, they will see the 195 // permission request. If they permanently denied the permission, they will be sent to Dialer 196 // settings in order enable the permission. 197 if (PermissionsUtil.isFirstRequest(getContext(), permissions[0]) 198 || shouldShowRequestPermissionRationale(permissions[0])) { 199 Logger.get(getContext()).logImpression(DialerImpression.Type.CAMERA_PERMISSION_REQUESTED); 200 LogUtil.i("CameraComposerFragment.onClick", "Camera permission requested."); 201 requestPermissions(permissions, CAMERA_PERMISSION); 202 } else { 203 Logger.get(getContext()).logImpression(DialerImpression.Type.CAMERA_PERMISSION_SETTINGS); 204 LogUtil.i("CameraComposerFragment.onClick", "Settings opened to enable permission."); 205 Intent intent = new Intent(Intent.ACTION_VIEW); 206 intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); 207 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 208 intent.setData(Uri.parse("package:" + getContext().getPackageName())); 209 startActivity(intent); 210 } 211 } 212 } 213 214 /** 215 * Called by {@link com.android.dialer.callcomposer.camera.ImagePersistTask} when the image is 216 * finished being cropped and stored on the device. 217 */ 218 @Override onMediaReady(Uri uri, String contentType, int width, int height)219 public void onMediaReady(Uri uri, String contentType, int width, int height) { 220 if (processingUri) { 221 processingUri = false; 222 setCameraUri(uri); 223 // If the user needed the URI before it was ready, uriCallback will be set and we should 224 // send the URI to them ASAP. 225 if (uriCallback != null) { 226 uriCallback.uriReady(uri); 227 uriCallback = null; 228 } 229 } else { 230 updateViewState(); 231 } 232 } 233 234 /** 235 * Called by {@link com.android.dialer.callcomposer.camera.ImagePersistTask} when the image failed 236 * to crop or be stored on the device. 237 */ 238 @Override onMediaFailed(Exception exception)239 public void onMediaFailed(Exception exception) { 240 LogUtil.e("CallComposerFragment.onMediaFailed", null, exception); 241 Toast.makeText(getContext(), R.string.camera_media_failure, Toast.LENGTH_LONG).show(); 242 setCameraUri(null); 243 processingUri = false; 244 if (uriCallback != null) { 245 loading.setVisibility(View.GONE); 246 uriCallback = null; 247 } 248 } 249 250 /** 251 * Usually called by {@link CameraManager} if the user does something to interrupt the picture 252 * while it's being taken (like switching the camera). 253 */ 254 @Override onMediaInfo(int what)255 public void onMediaInfo(int what) { 256 if (what == MediaCallback.MEDIA_NO_DATA) { 257 Toast.makeText(getContext(), R.string.camera_media_failure, Toast.LENGTH_LONG).show(); 258 } 259 setCameraUri(null); 260 processingUri = false; 261 } 262 263 @Override onDestroy()264 public void onDestroy() { 265 super.onDestroy(); 266 CameraManager.get().setListener(null); 267 } 268 showShutterEffect(final View shutterVisual)269 private void showShutterEffect(final View shutterVisual) { 270 float maxAlpha = .7f; 271 int animationDurationMillis = 100; 272 273 AnimationSet animation = new AnimationSet(false /* shareInterpolator */); 274 Animation alphaInAnimation = new AlphaAnimation(0.0f, maxAlpha); 275 alphaInAnimation.setDuration(animationDurationMillis); 276 animation.addAnimation(alphaInAnimation); 277 278 Animation alphaOutAnimation = new AlphaAnimation(maxAlpha, 0.0f); 279 alphaOutAnimation.setStartOffset(animationDurationMillis); 280 alphaOutAnimation.setDuration(animationDurationMillis); 281 animation.addAnimation(alphaOutAnimation); 282 283 animation.setAnimationListener( 284 new Animation.AnimationListener() { 285 @Override 286 public void onAnimationStart(Animation animation) { 287 shutterVisual.setVisibility(View.VISIBLE); 288 } 289 290 @Override 291 public void onAnimationEnd(Animation animation) { 292 shutterVisual.setVisibility(View.GONE); 293 } 294 295 @Override 296 public void onAnimationRepeat(Animation animation) {} 297 }); 298 shutterVisual.startAnimation(animation); 299 } 300 301 @NonNull getMimeType()302 public String getMimeType() { 303 return "image/jpeg"; 304 } 305 setCameraUri(Uri uri)306 private void setCameraUri(Uri uri) { 307 cameraUri = uri; 308 // It's possible that if the user takes a picture and press back very quickly, the activity will 309 // no longer be alive and when the image cropping process completes, so we need to check that 310 // activity is still alive before trying to invoke it. 311 if (getListener() != null) { 312 updateViewState(); 313 getListener().composeCall(this); 314 } 315 } 316 317 @Override onResume()318 public void onResume() { 319 super.onResume(); 320 if (PermissionsUtil.hasCameraPermissions(getContext())) { 321 permissionView.setVisibility(View.GONE); 322 setupCamera(); 323 } 324 } 325 326 /** Updates the state of the buttons and overlays based on the current state of the view */ updateViewState()327 private void updateViewState() { 328 Assert.isNotNull(cameraView); 329 Assert.isNotNull(getContext()); 330 331 boolean isCameraAvailable = CameraManager.get().isCameraAvailable(); 332 boolean uriReadyOrProcessing = cameraUri != null || processingUri; 333 334 if (cameraUri != null) { 335 previewImageView.setImageURI(cameraUri); 336 previewImageView.setVisibility(View.VISIBLE); 337 previewImageView.setScaleX(cameraDirection == CameraInfo.CAMERA_FACING_FRONT ? -1 : 1); 338 } else { 339 previewImageView.setVisibility(View.GONE); 340 } 341 342 if (cameraDirection == CameraInfo.CAMERA_FACING_FRONT) { 343 swapCamera.setContentDescription(getString(R.string.description_camera_switch_camera_rear)); 344 } else { 345 swapCamera.setContentDescription(getString(R.string.description_camera_switch_camera_facing)); 346 } 347 348 if (cameraUri == null && isCameraAvailable) { 349 CameraManager.get().resetPreview(); 350 cancel.setVisibility(View.GONE); 351 } 352 353 if (!CameraManager.get().hasFrontAndBackCamera()) { 354 swapCamera.setVisibility(View.GONE); 355 } else { 356 swapCamera.setVisibility(uriReadyOrProcessing ? View.GONE : View.VISIBLE); 357 } 358 359 capture.setVisibility(uriReadyOrProcessing ? View.GONE : View.VISIBLE); 360 cancel.setVisibility(uriReadyOrProcessing ? View.VISIBLE : View.GONE); 361 362 if (uriReadyOrProcessing || getListener().isLandscapeLayout()) { 363 fullscreen.setVisibility(View.GONE); 364 exitFullscreen.setVisibility(View.GONE); 365 } else if (getListener().isFullscreen()) { 366 exitFullscreen.setVisibility(View.VISIBLE); 367 fullscreen.setVisibility(View.GONE); 368 } else { 369 exitFullscreen.setVisibility(View.GONE); 370 fullscreen.setVisibility(View.VISIBLE); 371 } 372 373 swapCamera.setEnabled(isCameraAvailable); 374 capture.setEnabled(isCameraAvailable); 375 } 376 377 @Override onSaveInstanceState(Bundle outState)378 public void onSaveInstanceState(Bundle outState) { 379 super.onSaveInstanceState(outState); 380 outState.putInt(CAMERA_DIRECTION_KEY, cameraDirection); 381 outState.putParcelable(CAMERA_URI_KEY, cameraUri); 382 } 383 384 @Override onRequestPermissionsResult( int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults)385 public void onRequestPermissionsResult( 386 int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { 387 if (permissions.length > 0 && permissions[0].equals(this.permissions[0])) { 388 PermissionsUtil.permissionRequested(getContext(), permissions[0]); 389 } 390 if (requestCode == CAMERA_PERMISSION 391 && grantResults.length > 0 392 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { 393 Logger.get(getContext()).logImpression(DialerImpression.Type.CAMERA_PERMISSION_GRANTED); 394 LogUtil.i("CameraComposerFragment.onRequestPermissionsResult", "Permission granted."); 395 permissionView.setVisibility(View.GONE); 396 PermissionsUtil.setCameraPrivacyToastShown(getContext()); 397 setupCamera(); 398 } else if (requestCode == CAMERA_PERMISSION) { 399 Logger.get(getContext()).logImpression(DialerImpression.Type.CAMERA_PERMISSION_DENIED); 400 LogUtil.i("CameraComposerFragment.onRequestPermissionsResult", "Permission denied."); 401 } 402 } 403 getCameraUriWhenReady(CameraUriCallback callback)404 public void getCameraUriWhenReady(CameraUriCallback callback) { 405 if (processingUri) { 406 loading.setVisibility(View.VISIBLE); 407 uriCallback = callback; 408 } else { 409 callback.uriReady(cameraUri); 410 } 411 } 412 413 /** Callback to let the caller know when the URI is ready. */ 414 public interface CameraUriCallback { uriReady(Uri uri)415 void uriReady(Uri uri); 416 } 417 } 418