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