• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
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(
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 
setupCamera()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
onCameraError(int errorCode, Exception exception)145   public void onCameraError(int errorCode, Exception exception) {
146     LogUtil.e("CameraComposerFragment.onCameraError", "errorCode: ", errorCode, exception);
147   }
148 
149   @Override
onCameraChanged()150   public void onCameraChanged() {
151     updateViewState();
152   }
153 
154   @Override
shouldHide()155   public boolean shouldHide() {
156     return !processingUri && cameraUri == null;
157   }
158 
159   @Override
clearComposer()160   public void clearComposer() {
161     processingUri = false;
162     setCameraUri(null);
163   }
164 
165   @Override
onClick(View view)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
onMediaReady(Uri uri, String contentType, int width, int height)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
onMediaFailed(Exception exception)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
onMediaInfo(int what)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
onDestroy()264   public void onDestroy() {
265     super.onDestroy();
266     CameraManager.get().setListener(null);
267   }
268 
showShutterEffect(final View shutterVisual)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
getMimeType()302   public String getMimeType() {
303     return "image/jpeg";
304   }
305 
setCameraUri(Uri uri)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
onResume()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 */
updateViewState()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
onSaveInstanceState(Bundle outState)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
onRequestPermissionsResult( int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults)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 
getCameraUriWhenReady(CameraUriCallback callback)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 {
uriReady(Uri uri)415     void uriReady(Uri uri);
416   }
417 }
418