| /* |
| * 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.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.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.DialerImpression; |
| import com.android.dialer.logging.Logger; |
| import com.android.dialer.theme.base.ThemeComponent; |
| 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 static final String CAMERA_DIRECTION_KEY = "camera_direction"; |
| private static final String CAMERA_URI_KEY = "camera_key"; |
| |
| 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 ImageView previewImageView; |
| |
| private Uri cameraUri; |
| private boolean processingUri; |
| private String[] permissions = new String[] {Manifest.permission.CAMERA}; |
| private CameraUriCallback uriCallback; |
| private int cameraDirection = CameraInfo.CAMERA_FACING_BACK; |
| |
| 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 = root.findViewById(R.id.loading); |
| cameraView = root.findViewById(R.id.camera_view); |
| shutter = cameraView.findViewById(R.id.camera_shutter_visual); |
| exitFullscreen = cameraView.findViewById(R.id.camera_exit_fullscreen); |
| fullscreen = cameraView.findViewById(R.id.camera_fullscreen); |
| swapCamera = cameraView.findViewById(R.id.swap_camera_button); |
| capture = cameraView.findViewById(R.id.camera_capture_button); |
| cancel = cameraView.findViewById(R.id.camera_cancel_button); |
| focus = cameraView.findViewById(R.id.focus_visual); |
| preview = cameraView.findViewById(R.id.camera_preview); |
| previewImageView = root.findViewById(R.id.preview_image_view); |
| |
| exitFullscreen.setOnClickListener(this); |
| fullscreen.setOnClickListener(this); |
| swapCamera.setOnClickListener(this); |
| capture.setOnClickListener(this); |
| cancel.setOnClickListener(this); |
| |
| |
| if (!PermissionsUtil.hasCameraPermissions(getContext())) { |
| LogUtil.i("CameraComposerFragment.onCreateView", "Permission view shown."); |
| Logger.get(getContext()).logImpression(DialerImpression.Type.CAMERA_PERMISSION_DISPLAYED); |
| ImageView permissionImage = permissionView.findViewById(R.id.permission_icon); |
| TextView permissionText = 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(ThemeComponent.get(getContext()).theme().getColorPrimary()); |
| permissionView.setVisibility(View.VISIBLE); |
| } else { |
| if (bundle != null) { |
| cameraDirection = bundle.getInt(CAMERA_DIRECTION_KEY); |
| cameraUri = bundle.getParcelable(CAMERA_URI_KEY); |
| } |
| setupCamera(); |
| } |
| return root; |
| } |
| |
| private void setupCamera() { |
| if (!PermissionsUtil.hasCameraPrivacyToastShown(getContext())) { |
| PermissionsUtil.showCameraPermissionToast(getContext()); |
| } |
| CameraManager.get().setListener(this); |
| preview.setShown(); |
| CameraManager.get().setRenderOverlay(focus); |
| CameraManager.get().selectCamera(cameraDirection); |
| setCameraUri(cameraUri); |
| } |
| |
| @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 clearComposer() { |
| processingUri = false; |
| setCameraUri(null); |
| } |
| |
| @Override |
| public void onClick(View view) { |
| if (view == capture) { |
| float heightPercent = 1; |
| if (!getListener().isFullscreen() && !getListener().isLandscapeLayout()) { |
| 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(); |
| cameraDirection = CameraManager.get().getCameraInfo().facing; |
| } else if (view == cancel) { |
| clearComposer(); |
| } 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); |
| if (isDetached() || getContext() == null) { |
| LogUtil.i( |
| "CameraComposerFragment.updateViewState", "Fragment detached, cannot update view state"); |
| return; |
| } |
| |
| boolean isCameraAvailable = CameraManager.get().isCameraAvailable(); |
| boolean uriReadyOrProcessing = cameraUri != null || processingUri; |
| |
| if (cameraUri != null) { |
| previewImageView.setImageURI(cameraUri); |
| previewImageView.setVisibility(View.VISIBLE); |
| previewImageView.setScaleX(cameraDirection == CameraInfo.CAMERA_FACING_FRONT ? -1 : 1); |
| } else { |
| previewImageView.setVisibility(View.GONE); |
| } |
| |
| if (cameraDirection == CameraInfo.CAMERA_FACING_FRONT) { |
| swapCamera.setContentDescription(getString(R.string.description_camera_switch_camera_rear)); |
| } else { |
| swapCamera.setContentDescription(getString(R.string.description_camera_switch_camera_facing)); |
| } |
| |
| 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 || getListener().isLandscapeLayout()) { |
| 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 onSaveInstanceState(Bundle outState) { |
| super.onSaveInstanceState(outState); |
| outState.putInt(CAMERA_DIRECTION_KEY, cameraDirection); |
| outState.putParcelable(CAMERA_URI_KEY, cameraUri); |
| } |
| |
| @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); |
| PermissionsUtil.setCameraPrivacyToastShown(getContext()); |
| 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); |
| } |
| } |