Messaging: Replace own camera handling with pickers

We do not need to handle camera on our own - there are apps out there
that are made for that!

Change-Id: Ifeb70e46b17bff5eb8d85c8775589efac4f2c284
This commit is contained in:
Michael W
2024-12-26 15:54:37 +01:00
parent 6cc0e38bb5
commit 7f464b401c
17 changed files with 149 additions and 4198 deletions
@@ -17,55 +17,82 @@
package com.android.messaging.ui.mediapicker;
import static com.android.messaging.util.ContentType.VIDEO_UNSPECIFIED;
import android.Manifest;
import android.content.Context;
import android.app.Activity;
import android.content.pm.PackageManager;
import android.graphics.Rect;
import android.hardware.Camera;
import android.net.Uri;
import android.os.SystemClock;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.AlphaAnimation;
import android.view.animation.Animation;
import android.view.animation.AnimationSet;
import android.widget.Chronometer;
import android.widget.ImageButton;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import com.android.messaging.R;
import com.android.messaging.datamodel.MediaScratchFileProvider;
import com.android.messaging.datamodel.MessagingContentProvider;
import com.android.messaging.datamodel.data.MediaPickerMessagePartData;
import com.android.messaging.ui.mediapicker.CameraManager.MediaCallback;
import com.android.messaging.ui.mediapicker.camerafocus.RenderOverlay;
import com.android.messaging.util.Assert;
import com.android.messaging.util.LogUtil;
import com.android.messaging.util.OsUtil;
import com.android.messaging.util.UiUtils;
import java.util.ArrayList;
import java.util.List;
/**
* Chooser which allows the user to take pictures or video without leaving the current app/activity
*/
class CameraMediaChooser extends MediaChooser implements
CameraManager.CameraManagerListener {
private CameraPreview.CameraPreviewHost mCameraPreviewHost;
private ImageButton mFullScreenButton;
private ImageButton mSwapCameraButton;
private ImageButton mSwapModeButton;
private ImageButton mCaptureButton;
private ImageButton mCancelVideoButton;
private Chronometer mVideoCounter;
private boolean mVideoCancelled;
private int mErrorToast;
class CameraMediaChooser extends MediaChooser {
private View mEnabledView;
private View mMissingPermissionView;
private final ActivityResultLauncher<Uri> mPictureLauncher;
private final ActivityResultLauncher<Uri> mVideoLauncher;
private final DocumentImagePicker mDocumentImagePicker;
private Uri mOutputUri;
CameraMediaChooser(final MediaPicker mediaPicker) {
super(mediaPicker);
mDocumentImagePicker = new DocumentImagePicker(mMediaPicker, data -> {
if (mBindingRef.isBound()) {
mMediaPicker.dispatchPendingItemAdded(data);
}
});
mPictureLauncher = mediaPicker.registerForActivityResult(
new ActivityResultContracts.TakePicture(), result -> {
if (result) {
List<Uri> list = new ArrayList<>();
list.add(mOutputUri);
mDocumentImagePicker.onDocumentsPicked(list);
}
});
mVideoLauncher = mediaPicker.registerForActivityResult(
new ActivityResultContracts.CaptureVideo(), result -> {
if (result) {
final Rect startRect = new Rect();
// It's possible to throw out the chooser while taking the
// picture/video. In that case, still use the attachment, just
// skip the startRect
if (mView != null) {
mView.getGlobalVisibleRect(startRect);
}
mMediaPicker.dispatchItemsSelected(
new MediaPickerMessagePartData(startRect, VIDEO_UNSPECIFIED,
mOutputUri, MessagingContentProvider.UNSPECIFIED_SIZE,
MessagingContentProvider.UNSPECIFIED_SIZE),
true /* dismissMediaPicker */);
}
});
}
@Override
public int getSupportedMediaTypes() {
if (CameraManager.get().hasAnyCamera()) {
Activity activity = mMediaPicker.getActivity();
if (activity == null) {
return MediaPicker.MEDIA_TYPE_NONE;
}
PackageManager pm = activity.getPackageManager();
if (pm != null && pm.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY)) {
return MediaPicker.MEDIA_TYPE_IMAGE | MediaPicker.MEDIA_TYPE_VIDEO;
} else {
return MediaPicker.MEDIA_TYPE_NONE;
@@ -74,129 +101,33 @@ class CameraMediaChooser extends MediaChooser implements
@Override
public View destroyView() {
CameraManager.get().closeCamera();
CameraManager.get().setListener(null);
CameraManager.get().setSubscriptionDataProvider(null);
return super.destroyView();
}
@Override
protected View createView(final ViewGroup container) {
CameraManager.get().setListener(this);
CameraManager.get().setSubscriptionDataProvider(this);
CameraManager.get().setVideoMode(false);
final LayoutInflater inflater = getLayoutInflater();
final CameraMediaChooserView view = (CameraMediaChooserView) inflater.inflate(
final View view = inflater.inflate(
R.layout.mediapicker_camera_chooser,
container /* root */,
false /* attachToRoot */);
mCameraPreviewHost = (CameraPreview.CameraPreviewHost) view.findViewById(
R.id.camera_preview);
mCameraPreviewHost.getView().setOnTouchListener((view1, motionEvent) -> {
if (CameraManager.get().isVideoMode()) {
// Prevent the swipe down in video mode because video is always captured in
// full screen
return true;
}
return false;
});
final View shutterVisual = view.findViewById(R.id.camera_shutter_visual);
mFullScreenButton = (ImageButton) view.findViewById(R.id.camera_fullScreen_button);
mFullScreenButton.setOnClickListener(view12 -> mMediaPicker.setFullScreen(true));
mSwapCameraButton = (ImageButton) view.findViewById(R.id.camera_swapCamera_button);
mSwapCameraButton.setOnClickListener(view13 -> CameraManager.get().swapCamera());
mCaptureButton = (ImageButton) view.findViewById(R.id.camera_capture_button);
mCaptureButton.setOnClickListener(v -> {
final float heightPercent = Math.min(mMediaPicker.getViewPager().getHeight() /
(float) mCameraPreviewHost.getView().getHeight(), 1);
if (CameraManager.get().isRecording()) {
CameraManager.get().stopVideo();
} else {
final MediaCallback callback = new MediaCallback() {
@Override
public void onMediaReady(
final Uri uriToVideo, final String contentType,
final int width, final int height) {
mVideoCounter.stop();
if (mVideoCancelled || uriToVideo == null) {
mVideoCancelled = false;
} else {
final Rect startRect = new Rect();
// It's possible to throw out the chooser while taking the
// picture/video. In that case, still use the attachment, just
// skip the startRect
if (mView != null) {
mView.getGlobalVisibleRect(startRect);
}
mMediaPicker.dispatchItemsSelected(
new MediaPickerMessagePartData(startRect, contentType,
uriToVideo, width, height),
true /* dismissMediaPicker */);
}
updateViewState();
}
@Override
public void onMediaFailed(final Exception exception) {
UiUtils.showToastAtBottom(R.string.camera_media_failure);
updateViewState();
}
@Override
public void onMediaInfo(final int what) {
if (what == MediaCallback.MEDIA_NO_DATA) {
UiUtils.showToastAtBottom(R.string.camera_media_failure);
}
updateViewState();
}
};
if (CameraManager.get().isVideoMode()) {
CameraManager.get().startVideo(callback);
mVideoCounter.setBase(SystemClock.elapsedRealtime());
mVideoCounter.start();
updateViewState();
} else {
showShutterEffect(shutterVisual);
CameraManager.get().takePicture(heightPercent, callback);
updateViewState();
}
}
});
mSwapModeButton = (ImageButton) view.findViewById(R.id.camera_swap_mode_button);
mSwapModeButton.setOnClickListener(view14 -> {
final boolean isSwitchingToVideo = !CameraManager.get().isVideoMode();
if (isSwitchingToVideo && !OsUtil.hasRecordAudioPermission()) {
requestRecordAudioPermission();
} else {
onSwapMode();
}
});
mCancelVideoButton = (ImageButton) view.findViewById(R.id.camera_cancel_button);
mCancelVideoButton.setOnClickListener(view15 -> {
mVideoCancelled = true;
CameraManager.get().stopVideo();
mMediaPicker.dismiss(true);
});
mVideoCounter = (Chronometer) view.findViewById(R.id.camera_video_counter);
CameraManager.get().setRenderOverlay((RenderOverlay) view.findViewById(R.id.focus_visual));
mEnabledView = view.findViewById(R.id.mediapicker_enabled);
mMissingPermissionView = view.findViewById(R.id.missing_permission_view);
// Must set mView before calling updateViewState because it operates on mView
mView = view;
updateViewState();
updateForPermissionState(CameraManager.hasCameraPermission());
View takePictureView = view.findViewById(R.id.take_picture);
takePictureView.setOnClickListener(v -> {
mOutputUri = MediaScratchFileProvider.buildMediaScratchSpaceUri(null);
mPictureLauncher.launch(mOutputUri);
});
View takeVideoView = view.findViewById(R.id.take_video);
takeVideoView.setOnClickListener(v -> {
if (!OsUtil.hasRecordAudioPermission()) {
requestRecordAudioPermission();
} else {
mOutputUri = MediaScratchFileProvider.buildMediaScratchSpaceUri(null);
mVideoLauncher.launch(mOutputUri);
}
});
updateForPermissionState(hasCameraPermission());
return view;
}
@@ -210,36 +141,11 @@ class CameraMediaChooser extends MediaChooser implements
return R.string.mediapicker_cameraChooserDescription;
}
/**
* Updates the view when entering or leaving full-screen camera mode
*/
@Override
void onFullScreenChanged(final boolean fullScreen) {
super.onFullScreenChanged(fullScreen);
if (!fullScreen && CameraManager.get().isVideoMode()) {
CameraManager.get().setVideoMode(false);
}
updateViewState();
}
/**
* Initializes the control to a default state when it is opened / closed
* @param open True if the control is opened
*/
@Override
void onOpenedChanged(final boolean open) {
super.onOpenedChanged(open);
updateViewState();
}
@Override
protected void setSelected(final boolean selected) {
super.setSelected(selected);
if (selected) {
if (CameraManager.hasCameraPermission()) {
// If an error occurred before the chooser was selected, show it now
showErrorToastIfNeeded();
} else {
if (!hasCameraPermission()) {
requestCameraPermission();
}
}
@@ -261,17 +167,6 @@ class CameraMediaChooser extends MediaChooser implements
if (requestCode == MediaPicker.CAMERA_PERMISSION_REQUEST_CODE) {
final boolean permissionGranted = grantResults[0] == PackageManager.PERMISSION_GRANTED;
updateForPermissionState(permissionGranted);
if (permissionGranted) {
mCameraPreviewHost.onCameraPermissionGranted();
}
} else if (requestCode == MediaPicker.RECORD_AUDIO_PERMISSION_REQUEST_CODE) {
Assert.isFalse(CameraManager.get().isVideoMode());
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// Switch to video mode
onSwapMode();
} else {
// Stay in still-photo mode
}
}
}
@@ -285,174 +180,12 @@ class CameraMediaChooser extends MediaChooser implements
mMissingPermissionView.setVisibility(granted ? View.GONE : View.VISIBLE);
}
@Override
public boolean canSwipeDown() {
if (CameraManager.get().isVideoMode()) {
return true;
}
return super.canSwipeDown();
}
/**
* Handles an error from the camera manager by showing the appropriate error message to the user
* @param errorCode One of the CameraManager.ERROR_* constants
* @param e The exception which caused the error, if any
*/
@Override
public void onCameraError(final int errorCode, final Exception e) {
switch (errorCode) {
case CameraManager.ERROR_OPENING_CAMERA:
case CameraManager.ERROR_SHOWING_PREVIEW:
mErrorToast = R.string.camera_error_opening;
break;
case CameraManager.ERROR_INITIALIZING_VIDEO:
mErrorToast = R.string.camera_error_video_init_fail;
updateViewState();
break;
case CameraManager.ERROR_STORAGE_FAILURE:
mErrorToast = R.string.camera_error_storage_fail;
updateViewState();
break;
case CameraManager.ERROR_TAKING_PICTURE:
mErrorToast = R.string.camera_error_failure_taking_picture;
break;
default:
mErrorToast = R.string.camera_error_unknown;
LogUtil.w(LogUtil.BUGLE_TAG, "Unknown camera error:" + errorCode);
break;
}
showErrorToastIfNeeded();
}
private void showErrorToastIfNeeded() {
if (mErrorToast != 0 && mSelected) {
UiUtils.showToastAtBottom(mErrorToast);
mErrorToast = 0;
}
}
@Override
public void onCameraChanged() {
updateViewState();
}
private void onSwapMode() {
CameraManager.get().setVideoMode(!CameraManager.get().isVideoMode());
if (CameraManager.get().isVideoMode()) {
mMediaPicker.setFullScreen(true);
// For now we start recording immediately
mCaptureButton.performClick();
}
updateViewState();
}
private void showShutterEffect(final View shutterVisual) {
final float maxAlpha = getContext().getResources().getFraction(
R.fraction.camera_shutter_max_alpha, 1 /* base */, 1 /* pBase */);
// Divide by 2 so each half of the animation adds up to the full duration
final int animationDuration = getContext().getResources().getInteger(
R.integer.camera_shutter_duration) / 2;
final AnimationSet animation = new AnimationSet(false /* shareInterpolator */);
final Animation alphaInAnimation = new AlphaAnimation(0.0f, maxAlpha);
alphaInAnimation.setDuration(animationDuration);
animation.addAnimation(alphaInAnimation);
final Animation alphaOutAnimation = new AlphaAnimation(maxAlpha, 0.0f);
alphaOutAnimation.setStartOffset(animationDuration);
alphaOutAnimation.setDuration(animationDuration);
animation.addAnimation(alphaOutAnimation);
animation.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(final Animation animation) {
shutterVisual.setVisibility(View.VISIBLE);
}
@Override
public void onAnimationEnd(final Animation animation) {
shutterVisual.setVisibility(View.GONE);
}
@Override
public void onAnimationRepeat(final Animation animation) {
}
});
shutterVisual.startAnimation(animation);
}
/** Updates the state of the buttons and overlays based on the current state of the view */
private void updateViewState() {
if (mView == null) {
return;
}
final Context context = getContext();
if (context == null) {
// Context is null if the fragment was already removed from the activity
return;
}
final boolean fullScreen = mMediaPicker.isFullScreen();
final boolean videoMode = CameraManager.get().isVideoMode();
final boolean isRecording = CameraManager.get().isRecording();
final boolean isCameraAvailable = isCameraAvailable();
final Camera.CameraInfo cameraInfo = CameraManager.get().getCameraInfo();
final boolean frontCamera = cameraInfo != null && cameraInfo.facing ==
Camera.CameraInfo.CAMERA_FACING_FRONT;
mView.setSystemUiVisibility(
fullScreen ? View.SYSTEM_UI_FLAG_LOW_PROFILE :
View.SYSTEM_UI_FLAG_VISIBLE);
mFullScreenButton.setVisibility(!fullScreen ? View.VISIBLE : View.GONE);
mFullScreenButton.setEnabled(isCameraAvailable);
mSwapCameraButton.setVisibility(
fullScreen && !isRecording && CameraManager.get().hasFrontAndBackCamera() ?
View.VISIBLE : View.GONE);
mSwapCameraButton.setImageResource(frontCamera ?
R.drawable.ic_camera_front_light :
R.drawable.ic_camera_rear_light);
mSwapCameraButton.setEnabled(isCameraAvailable);
mCancelVideoButton.setVisibility(isRecording ? View.VISIBLE : View.GONE);
mVideoCounter.setVisibility(isRecording ? View.VISIBLE : View.GONE);
mSwapModeButton.setImageResource(videoMode ?
R.drawable.ic_camera_light :
R.drawable.ic_mp_video_small_light);
mSwapModeButton.setContentDescription(context.getString(videoMode ?
R.string.camera_switch_to_still_mode : R.string.camera_switch_to_video_mode));
mSwapModeButton.setVisibility(isRecording ? View.GONE : View.VISIBLE);
mSwapModeButton.setEnabled(isCameraAvailable);
if (isRecording) {
mCaptureButton.setImageResource(R.drawable.ic_mp_capture_stop_light);
mCaptureButton.setContentDescription(context.getString(
R.string.camera_stop_recording));
} else if (videoMode) {
mCaptureButton.setImageResource(R.drawable.ic_mp_video_small_light);
mCaptureButton.setContentDescription(context.getString(
R.string.camera_start_recording));
} else {
mCaptureButton.setImageResource(R.drawable.ic_checkmark_light);
mCaptureButton.setContentDescription(context.getString(
R.string.camera_take_picture));
}
mCaptureButton.setEnabled(isCameraAvailable);
}
@Override
int getActionBarTitleResId() {
return 0;
}
/**
* Returns if the camera is currently ready camera is loaded and not taking a picture.
* otherwise we should avoid taking another picture, swapping camera or recording video.
*/
private boolean isCameraAvailable() {
return CameraManager.get().isCameraAvailable();
private static boolean hasCameraPermission() {
return OsUtil.hasPermission(Manifest.permission.CAMERA);
}
}