/* * 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); } }