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