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     Assert.isNotNull(getContext());
    330 
    331     boolean isCameraAvailable = CameraManager.get().isCameraAvailable();
    332     boolean uriReadyOrProcessing = cameraUri != null || processingUri;
    333 
    334     if (cameraUri != null) {
    335       previewImageView.setImageURI(cameraUri);
    336       previewImageView.setVisibility(View.VISIBLE);
    337       previewImageView.setScaleX(cameraDirection == CameraInfo.CAMERA_FACING_FRONT ? -1 : 1);
    338     } else {
    339       previewImageView.setVisibility(View.GONE);
    340     }
    341 
    342     if (cameraDirection == CameraInfo.CAMERA_FACING_FRONT) {
    343       swapCamera.setContentDescription(getString(R.string.description_camera_switch_camera_rear));
    344     } else {
    345       swapCamera.setContentDescription(getString(R.string.description_camera_switch_camera_facing));
    346     }
    347 
    348     if (cameraUri == null && isCameraAvailable) {
    349       CameraManager.get().resetPreview();
    350       cancel.setVisibility(View.GONE);
    351     }
    352 
    353     if (!CameraManager.get().hasFrontAndBackCamera()) {
    354       swapCamera.setVisibility(View.GONE);
    355     } else {
    356       swapCamera.setVisibility(uriReadyOrProcessing ? View.GONE : View.VISIBLE);
    357     }
    358 
    359     capture.setVisibility(uriReadyOrProcessing ? View.GONE : View.VISIBLE);
    360     cancel.setVisibility(uriReadyOrProcessing ? View.VISIBLE : View.GONE);
    361 
    362     if (uriReadyOrProcessing || getListener().isLandscapeLayout()) {
    363       fullscreen.setVisibility(View.GONE);
    364       exitFullscreen.setVisibility(View.GONE);
    365     } else if (getListener().isFullscreen()) {
    366       exitFullscreen.setVisibility(View.VISIBLE);
    367       fullscreen.setVisibility(View.GONE);
    368     } else {
    369       exitFullscreen.setVisibility(View.GONE);
    370       fullscreen.setVisibility(View.VISIBLE);
    371     }
    372 
    373     swapCamera.setEnabled(isCameraAvailable);
    374     capture.setEnabled(isCameraAvailable);
    375   }
    376 
    377   @Override
    378   public void onSaveInstanceState(Bundle outState) {
    379     super.onSaveInstanceState(outState);
    380     outState.putInt(CAMERA_DIRECTION_KEY, cameraDirection);
    381     outState.putParcelable(CAMERA_URI_KEY, cameraUri);
    382   }
    383 
    384   @Override
    385   public void onRequestPermissionsResult(
    386       int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
    387     if (permissions.length > 0 && permissions[0].equals(this.permissions[0])) {
    388       PermissionsUtil.permissionRequested(getContext(), permissions[0]);
    389     }
    390     if (requestCode == CAMERA_PERMISSION
    391         && grantResults.length > 0
    392         && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
    393       Logger.get(getContext()).logImpression(DialerImpression.Type.CAMERA_PERMISSION_GRANTED);
    394       LogUtil.i("CameraComposerFragment.onRequestPermissionsResult", "Permission granted.");
    395       permissionView.setVisibility(View.GONE);
    396       PermissionsUtil.setCameraPrivacyToastShown(getContext());
    397       setupCamera();
    398     } else if (requestCode == CAMERA_PERMISSION) {
    399       Logger.get(getContext()).logImpression(DialerImpression.Type.CAMERA_PERMISSION_DENIED);
    400       LogUtil.i("CameraComposerFragment.onRequestPermissionsResult", "Permission denied.");
    401     }
    402   }
    403 
    404   public void getCameraUriWhenReady(CameraUriCallback callback) {
    405     if (processingUri) {
    406       loading.setVisibility(View.VISIBLE);
    407       uriCallback = callback;
    408     } else {
    409       callback.uriReady(cameraUri);
    410     }
    411   }
    412 
    413   /** Callback to let the caller know when the URI is ready. */
    414   public interface CameraUriCallback {
    415     void uriReady(Uri uri);
    416   }
    417 }
    418