diff options
Diffstat (limited to 'java/com/android/dialer/callcomposer/CameraComposerFragment.java')
-rw-r--r-- | java/com/android/dialer/callcomposer/CameraComposerFragment.java | 378 |
1 files changed, 378 insertions, 0 deletions
diff --git a/java/com/android/dialer/callcomposer/CameraComposerFragment.java b/java/com/android/dialer/callcomposer/CameraComposerFragment.java new file mode 100644 index 000000000..f2d0a94a7 --- /dev/null +++ b/java/com/android/dialer/callcomposer/CameraComposerFragment.java @@ -0,0 +1,378 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.dialer.callcomposer; + +import android.Manifest; +import android.Manifest.permission; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.graphics.drawable.Animatable; +import android.hardware.Camera.CameraInfo; +import android.net.Uri; +import android.os.Bundle; +import android.provider.Settings; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.content.ContextCompat; +import android.view.LayoutInflater; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.view.animation.AlphaAnimation; +import android.view.animation.Animation; +import android.view.animation.AnimationSet; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.ProgressBar; +import android.widget.TextView; +import android.widget.Toast; +import com.android.dialer.callcomposer.camera.CameraManager; +import com.android.dialer.callcomposer.camera.CameraManager.CameraManagerListener; +import com.android.dialer.callcomposer.camera.CameraManager.MediaCallback; +import com.android.dialer.callcomposer.camera.CameraPreview.CameraPreviewHost; +import com.android.dialer.callcomposer.camera.camerafocus.RenderOverlay; +import com.android.dialer.callcomposer.cameraui.CameraMediaChooserView; +import com.android.dialer.common.Assert; +import com.android.dialer.common.LogUtil; +import com.android.dialer.logging.Logger; +import com.android.dialer.logging.nano.DialerImpression; +import com.android.dialer.util.PermissionsUtil; + +/** Fragment used to compose call with image from the user's camera. */ +public class CameraComposerFragment extends CallComposerFragment + implements CameraManagerListener, OnClickListener, CameraManager.MediaCallback { + + private View permissionView; + private ImageButton exitFullscreen; + private ImageButton fullscreen; + private ImageButton swapCamera; + private ImageButton capture; + private ImageButton cancel; + private CameraMediaChooserView cameraView; + private RenderOverlay focus; + private View shutter; + private View allowPermission; + private CameraPreviewHost preview; + private ProgressBar loading; + + private Uri cameraUri; + private boolean processingUri; + private String[] permissions = new String[] {Manifest.permission.CAMERA}; + private CameraUriCallback uriCallback; + + public static CameraComposerFragment newInstance() { + return new CameraComposerFragment(); + } + + @Nullable + @Override + public View onCreateView( + LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle bundle) { + View root = inflater.inflate(R.layout.fragment_camera_composer, container, false); + permissionView = root.findViewById(R.id.permission_view); + loading = (ProgressBar) root.findViewById(R.id.loading); + cameraView = (CameraMediaChooserView) root.findViewById(R.id.camera_view); + shutter = cameraView.findViewById(R.id.camera_shutter_visual); + exitFullscreen = (ImageButton) cameraView.findViewById(R.id.camera_exit_fullscreen); + fullscreen = (ImageButton) cameraView.findViewById(R.id.camera_fullscreen); + swapCamera = (ImageButton) cameraView.findViewById(R.id.swap_camera_button); + capture = (ImageButton) cameraView.findViewById(R.id.camera_capture_button); + cancel = (ImageButton) cameraView.findViewById(R.id.camera_cancel_button); + focus = (RenderOverlay) cameraView.findViewById(R.id.focus_visual); + preview = (CameraPreviewHost) cameraView.findViewById(R.id.camera_preview); + + exitFullscreen.setOnClickListener(this); + fullscreen.setOnClickListener(this); + swapCamera.setOnClickListener(this); + capture.setOnClickListener(this); + cancel.setOnClickListener(this); + + if (!PermissionsUtil.hasPermission(getContext(), permission.CAMERA)) { + LogUtil.i("CameraComposerFragment.onCreateView", "Permission view shown."); + Logger.get(getContext()).logImpression(DialerImpression.Type.CAMERA_PERMISSION_DISPLAYED); + ImageView permissionImage = (ImageView) permissionView.findViewById(R.id.permission_icon); + TextView permissionText = (TextView) permissionView.findViewById(R.id.permission_text); + allowPermission = permissionView.findViewById(R.id.allow); + + allowPermission.setOnClickListener(this); + permissionText.setText(R.string.camera_permission_text); + permissionImage.setImageResource(R.drawable.quantum_ic_camera_alt_white_48); + permissionImage.setColorFilter( + ContextCompat.getColor(getContext(), R.color.dialer_theme_color)); + permissionView.setVisibility(View.VISIBLE); + } else { + setupCamera(); + } + + setTopView(cameraView); + return root; + } + + private void setupCamera() { + CameraManager.get().setListener(this); + preview.setShown(); + CameraManager.get().setRenderOverlay(focus); + CameraManager.get().selectCamera(CameraInfo.CAMERA_FACING_BACK); + setCameraUri(null); + } + + @Override + public void onCameraError(int errorCode, Exception exception) { + LogUtil.e("CameraComposerFragment.onCameraError", "errorCode: ", errorCode, exception); + } + + @Override + public void onCameraChanged() { + updateViewState(); + } + + @Override + public boolean shouldHide() { + return !processingUri && cameraUri == null; + } + + @Override + public void onClick(View view) { + if (view == capture) { + float heightPercent = 1; + if (!getListener().isFullscreen()) { + heightPercent = Math.min((float) cameraView.getHeight() / preview.getView().getHeight(), 1); + } + + showShutterEffect(shutter); + processingUri = true; + setCameraUri(null); + focus.getPieRenderer().clear(); + CameraManager.get().takePicture(heightPercent, this); + } else if (view == swapCamera) { + ((Animatable) swapCamera.getDrawable()).start(); + CameraManager.get().swapCamera(); + } else if (view == cancel) { + processingUri = false; + setCameraUri(null); + } else if (view == exitFullscreen) { + getListener().showFullscreen(false); + fullscreen.setVisibility(View.VISIBLE); + exitFullscreen.setVisibility(View.GONE); + } else if (view == fullscreen) { + getListener().showFullscreen(true); + fullscreen.setVisibility(View.GONE); + exitFullscreen.setVisibility(View.VISIBLE); + } else if (view == allowPermission) { + // Checks to see if the user has permanently denied this permission. If this is the first + // time seeing this permission or they only pressed deny previously, they will see the + // permission request. If they permanently denied the permission, they will be sent to Dialer + // settings in order enable the permission. + if (PermissionsUtil.isFirstRequest(getContext(), permissions[0]) + || shouldShowRequestPermissionRationale(permissions[0])) { + Logger.get(getContext()).logImpression(DialerImpression.Type.CAMERA_PERMISSION_REQUESTED); + LogUtil.i("CameraComposerFragment.onClick", "Camera permission requested."); + requestPermissions(permissions, CAMERA_PERMISSION); + } else { + Logger.get(getContext()).logImpression(DialerImpression.Type.CAMERA_PERMISSION_SETTINGS); + LogUtil.i("CameraComposerFragment.onClick", "Settings opened to enable permission."); + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.setData(Uri.parse("package:" + getContext().getPackageName())); + startActivity(intent); + } + } + } + + /** + * Called by {@link com.android.dialer.callcomposer.camera.ImagePersistTask} when the image is + * finished being cropped and stored on the device. + */ + @Override + public void onMediaReady(Uri uri, String contentType, int width, int height) { + if (processingUri) { + processingUri = false; + setCameraUri(uri); + // If the user needed the URI before it was ready, uriCallback will be set and we should + // send the URI to them ASAP. + if (uriCallback != null) { + uriCallback.uriReady(uri); + uriCallback = null; + } + } else { + updateViewState(); + } + } + + /** + * Called by {@link com.android.dialer.callcomposer.camera.ImagePersistTask} when the image failed + * to crop or be stored on the device. + */ + @Override + public void onMediaFailed(Exception exception) { + LogUtil.e("CallComposerFragment.onMediaFailed", null, exception); + Toast.makeText(getContext(), R.string.camera_media_failure, Toast.LENGTH_LONG).show(); + setCameraUri(null); + processingUri = false; + if (uriCallback != null) { + loading.setVisibility(View.GONE); + uriCallback = null; + } + } + + /** + * Usually called by {@link CameraManager} if the user does something to interrupt the picture + * while it's being taken (like switching the camera). + */ + @Override + public void onMediaInfo(int what) { + if (what == MediaCallback.MEDIA_NO_DATA) { + Toast.makeText(getContext(), R.string.camera_media_failure, Toast.LENGTH_LONG).show(); + } + setCameraUri(null); + processingUri = false; + } + + @Override + public void onDestroy() { + super.onDestroy(); + CameraManager.get().setListener(null); + } + + private void showShutterEffect(final View shutterVisual) { + float maxAlpha = .7f; + int animationDurationMillis = 100; + + AnimationSet animation = new AnimationSet(false /* shareInterpolator */); + Animation alphaInAnimation = new AlphaAnimation(0.0f, maxAlpha); + alphaInAnimation.setDuration(animationDurationMillis); + animation.addAnimation(alphaInAnimation); + + Animation alphaOutAnimation = new AlphaAnimation(maxAlpha, 0.0f); + alphaOutAnimation.setStartOffset(animationDurationMillis); + alphaOutAnimation.setDuration(animationDurationMillis); + animation.addAnimation(alphaOutAnimation); + + animation.setAnimationListener( + new Animation.AnimationListener() { + @Override + public void onAnimationStart(Animation animation) { + shutterVisual.setVisibility(View.VISIBLE); + } + + @Override + public void onAnimationEnd(Animation animation) { + shutterVisual.setVisibility(View.GONE); + } + + @Override + public void onAnimationRepeat(Animation animation) {} + }); + shutterVisual.startAnimation(animation); + } + + @NonNull + public String getMimeType() { + return "image/jpeg"; + } + + private void setCameraUri(Uri uri) { + cameraUri = uri; + // It's possible that if the user takes a picture and press back very quickly, the activity will + // no longer be alive and when the image cropping process completes, so we need to check that + // activity is still alive before trying to invoke it. + if (getListener() != null) { + updateViewState(); + getListener().composeCall(this); + } + } + + @Override + public void onResume() { + super.onResume(); + if (PermissionsUtil.hasCameraPermissions(getContext())) { + permissionView.setVisibility(View.GONE); + setupCamera(); + } + } + + /** Updates the state of the buttons and overlays based on the current state of the view */ + private void updateViewState() { + Assert.isNotNull(cameraView); + Assert.isNotNull(getContext()); + + boolean isCameraAvailable = CameraManager.get().isCameraAvailable(); + boolean uriReadyOrProcessing = cameraUri != null || processingUri; + + if (cameraUri == null && isCameraAvailable) { + CameraManager.get().resetPreview(); + cancel.setVisibility(View.GONE); + } + + if (!CameraManager.get().hasFrontAndBackCamera()) { + swapCamera.setVisibility(View.GONE); + } else { + swapCamera.setVisibility(uriReadyOrProcessing ? View.GONE : View.VISIBLE); + } + + capture.setVisibility(uriReadyOrProcessing ? View.GONE : View.VISIBLE); + cancel.setVisibility(uriReadyOrProcessing ? View.VISIBLE : View.GONE); + + if (uriReadyOrProcessing) { + fullscreen.setVisibility(View.GONE); + exitFullscreen.setVisibility(View.GONE); + } else if (getListener().isFullscreen()) { + exitFullscreen.setVisibility(View.VISIBLE); + fullscreen.setVisibility(View.GONE); + } else { + exitFullscreen.setVisibility(View.GONE); + fullscreen.setVisibility(View.VISIBLE); + } + + swapCamera.setEnabled(isCameraAvailable); + capture.setEnabled(isCameraAvailable); + } + + @Override + public void onRequestPermissionsResult( + int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + if (permissions.length > 0 && permissions[0].equals(this.permissions[0])) { + PermissionsUtil.permissionRequested(getContext(), permissions[0]); + } + if (requestCode == CAMERA_PERMISSION + && grantResults.length > 0 + && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + Logger.get(getContext()).logImpression(DialerImpression.Type.CAMERA_PERMISSION_GRANTED); + LogUtil.i("CameraComposerFragment.onRequestPermissionsResult", "Permission granted."); + permissionView.setVisibility(View.GONE); + setupCamera(); + } else if (requestCode == CAMERA_PERMISSION) { + Logger.get(getContext()).logImpression(DialerImpression.Type.CAMERA_PERMISSION_DENIED); + LogUtil.i("CameraComposerFragment.onRequestPermissionsResult", "Permission denied."); + } + } + + public void getCameraUriWhenReady(CameraUriCallback callback) { + if (processingUri) { + loading.setVisibility(View.VISIBLE); + uriCallback = callback; + } else { + callback.uriReady(cameraUri); + } + } + + /** Callback to let the caller know when the URI is ready. */ + public interface CameraUriCallback { + void uriReady(Uri uri); + } +} |