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