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
17package com.android.dialer.callcomposer;
18
19import android.Manifest;
20import android.Manifest.permission;
21import android.content.Intent;
22import android.content.pm.PackageManager;
23import android.graphics.drawable.Animatable;
24import android.hardware.Camera.CameraInfo;
25import android.net.Uri;
26import android.os.Bundle;
27import android.provider.Settings;
28import android.support.annotation.NonNull;
29import android.support.annotation.Nullable;
30import android.support.v4.content.ContextCompat;
31import android.view.LayoutInflater;
32import android.view.View;
33import android.view.View.OnClickListener;
34import android.view.ViewGroup;
35import android.view.animation.AlphaAnimation;
36import android.view.animation.Animation;
37import android.view.animation.AnimationSet;
38import android.widget.ImageButton;
39import android.widget.ImageView;
40import android.widget.ProgressBar;
41import android.widget.TextView;
42import android.widget.Toast;
43import com.android.dialer.callcomposer.camera.CameraManager;
44import com.android.dialer.callcomposer.camera.CameraManager.CameraManagerListener;
45import com.android.dialer.callcomposer.camera.CameraManager.MediaCallback;
46import com.android.dialer.callcomposer.camera.CameraPreview.CameraPreviewHost;
47import com.android.dialer.callcomposer.camera.camerafocus.RenderOverlay;
48import com.android.dialer.callcomposer.cameraui.CameraMediaChooserView;
49import com.android.dialer.common.Assert;
50import com.android.dialer.common.LogUtil;
51import com.android.dialer.logging.DialerImpression;
52import com.android.dialer.logging.Logger;
53import com.android.dialer.util.PermissionsUtil;
54
55/** Fragment used to compose call with image from the user's camera. */
56public 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