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