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