Improve camera capture with CameraX.
parent
4593014d00
commit
73b8f11b5a
|
@ -3,7 +3,7 @@
|
|||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="org.thoughtcrime.securesms">
|
||||
|
||||
<uses-sdk tools:overrideLibrary="com.amulyakhare.textdrawable,com.astuetz.pagerslidingtabstrip,pl.tajchert.waitingdots,com.h6ah4i.android.multiselectlistpreferencecompat,android.support.v13,com.davemorrissey.labs.subscaleview,com.tomergoldst.tooltips,com.klinker.android.send_message,com.takisoft.colorpicker,android.support.v14.preference"/>
|
||||
<uses-sdk tools:overrideLibrary="androidx.camera.core,androidx.camera.camera2"/>
|
||||
|
||||
<permission android:name="org.thoughtcrime.securesms.ACCESS_SECRETS"
|
||||
android:label="Access to TextSecure Secrets"
|
||||
|
|
46
build.gradle
46
build.gradle
|
@ -54,7 +54,7 @@ repositories {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'androidx.appcompat:appcompat:1.0.2'
|
||||
implementation 'androidx.appcompat:appcompat:1.1.0-beta01'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.0.0'
|
||||
implementation 'com.google.android.material:material:1.0.0'
|
||||
implementation 'androidx.legacy:legacy-support-v13:1.0.0'
|
||||
|
@ -67,6 +67,8 @@ dependencies {
|
|||
implementation 'androidx.multidex:multidex:2.0.1'
|
||||
implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0'
|
||||
implementation 'androidx.lifecycle:lifecycle-common-java8:2.0.0'
|
||||
implementation "androidx.camera:camera-core:1.0.0-alpha02"
|
||||
implementation "androidx.camera:camera-camera2:1.0.0-alpha02"
|
||||
|
||||
implementation('com.google.firebase:firebase-messaging:17.3.4') {
|
||||
exclude group: 'com.google.firebase', module: 'firebase-core'
|
||||
|
@ -173,12 +175,14 @@ dependencyVerification {
|
|||
'com.tomergoldst.android:tooltips:4c56697dd1ad64b8066535c61f961a6d901e7ae5d97ae27084ba40ad620349b6',
|
||||
'com.takisoft.fix:colorpicker:f5d0dbabe406a1800498ca9c1faf34db36e021d8488bf10360f29961fe3ab0d1',
|
||||
'com.codewaves.stickyheadergrid:stickyheadergrid:5b4aa6a52a957cfd55f60f4220c11c0c371385a3cb9786cae03c260dcdef5794',
|
||||
'androidx.appcompat:appcompat:a3080cdd5e5c56cb72f9d428b1657d4380011ec211cfedf76e084b95f6bf0d03',
|
||||
'androidx.appcompat:appcompat:49ad229add44f822fcb3c8405c3fddbd72660da6a839ce29e13158f5980514fd',
|
||||
'com.melnykov:floatingactionbutton:15d58d4fac0f7a288d0e5301bbaf501a146f5b3f5921277811bf99bd3b397263',
|
||||
'androidx.recyclerview:recyclerview:06956fb1ac014027ca9d2b40469a4b42aa61b4957bb11848e1ff352701ab4548',
|
||||
'androidx.legacy:legacy-support-v13:65f5fcb57644d381d471a00fdf50f90b808be6b48a8ae57fb4ea39b7da8cca86',
|
||||
'androidx.cardview:cardview:1193c04c22a3d6b5946dae9f4e8c59d6adde6a71b6bd5d87fb99d82dda1afec7',
|
||||
'androidx.gridlayout:gridlayout:a7e5dc6f39dbc3dc6ac6d57b02a9c6fd792e80f0e45ddb3bb08e8f03d23c8755',
|
||||
'androidx.camera:camera-camera2:9dc33e45da983ebd29a888401ac700323ff573821eee3fa4d993dfa3d316ee2e',
|
||||
'androidx.camera:camera-core:bf32bfcb5d103d865c6af1221a1d82e994c917b53c0bc080f1e9750bdc21cbb9',
|
||||
'androidx.exifinterface:exifinterface:ee48be10aab8f54efff4c14b77d11e10b9eeee4379d5ef6bf297a2923c55cc11',
|
||||
'androidx.constraintlayout:constraintlayout:5ff864def9d41cd04e08348d69591143bae3ceff4284cf8608bceb98c36ac830',
|
||||
'androidx.multidex:multidex:42dd32ff9f97f85771b82a20003a8d70f68ab7b4ba328964312ce0732693db09',
|
||||
|
@ -223,41 +227,47 @@ dependencyVerification {
|
|||
'com.google.android.gms:play-services-stats:5b2d8281adbfd6e74d2295c94bab9ea80fc9a84dfbb397995673f5af4d4c6368',
|
||||
'com.google.android.gms:play-services-basement:e08bfd1e87c4e50ef76161d7ac76b873aeb975367eeb3afa4abe62ea1887c7c6',
|
||||
'androidx.legacy:legacy-support-v4:78fec1485f0f388a4749022dd51416857127cd2544ae1c3fd0b16589055480b0',
|
||||
'androidx.fragment:fragment:65dd32d71fe65a32e77989a6cfb1ad09307038927f82a740c7611162d0b518f8',
|
||||
'androidx.vectordrawable:vectordrawable-animated:26c3a0cf0a9a9a7d235a0b00f2f37e431d52d9952751e3eb7c90b4b52c236cf1',
|
||||
'androidx.fragment:fragment:9656d81c472b5142bbc3471ef7259fbc93905dc38e823c63a99e48819881b6e7',
|
||||
'androidx.appcompat:appcompat-resources:53c0a33d07c4bab48d4c8169bf30053aa14965af4a775b56092a9fc7079802b1',
|
||||
'androidx.legacy:legacy-support-core-ui:0d1260c6e7e6a337f875df71b516931e703f716e90889817cd3a20fa5ac3d947',
|
||||
'androidx.drawerlayout:drawerlayout:9402442cdc5a43cf62fb14f8cf98c63342d4d9d9b805c8033c6cf7e802749ac1',
|
||||
'androidx.legacy:legacy-support-core-utils:a7edcf01d5b52b3034073027bc4775b78a4764bb6202bb91d61c829add8dd1c7',
|
||||
'androidx.vectordrawable:vectordrawable:4ca358957b9510e52fc388e01c9d33c2d655d406bfe6e71984e9afea9f715ed2',
|
||||
'androidx.transition:transition:a00a0f763f401abcecda9b0eafcb738929c5801b111a9a414b81a193d0f4008d',
|
||||
'androidx.media:media:b23b527b2bac870c4a7451e6982d7132e413e88d7f27dbeb1fc7640a720cd9ee',
|
||||
'androidx.loader:loader:11f735cb3b55c458d470bed9e25254375b518b4b1bad6926783a7026db0f5025',
|
||||
'androidx.viewpager:viewpager:147af4e14a1984010d8f155e5e19d781f03c1d70dfed02a8e0d18428b8fc8682',
|
||||
'androidx.loader:loader:11f735cb3b55c458d470bed9e25254375b518b4b1bad6926783a7026db0f5025',
|
||||
'androidx.activity:activity:0d6bafb56a72da893f3990ca5d819214d047f5f6b5c5f822ed97971c05eeb85a',
|
||||
'androidx.vectordrawable:vectordrawable-animated:f1613c47f1e6d2cd02ec9a42925f1a964fa63d1d028d34d884364cc3b9ffcb8f',
|
||||
'androidx.vectordrawable:vectordrawable:b632152304edb506bf7eacb329ef41e43b80164bf5be4c7bb132a249a65cbc26',
|
||||
'androidx.coordinatorlayout:coordinatorlayout:e508c695489493374d942bf7b4ee02abf7571d25aac4c622e57d6cd5cd29eb73',
|
||||
'androidx.drawerlayout:drawerlayout:9402442cdc5a43cf62fb14f8cf98c63342d4d9d9b805c8033c6cf7e802749ac1',
|
||||
'androidx.slidingpanelayout:slidingpanelayout:76bffb7cefbf780794d8817002dad1562f3e27c0a9f746d62401c8edb30aeede',
|
||||
'androidx.customview:customview:20e5b8f6526a34595a604f56718da81167c0b40a7a94a57daa355663f2594df2',
|
||||
'androidx.swiperefreshlayout:swiperefreshlayout:9761b3a809c9b093fd06a3c4bbc645756dec0e95b5c9da419bc9f2a3f3026e8d',
|
||||
'androidx.asynclayoutinflater:asynclayoutinflater:f7eab60c57addd94bb06275832fe7600611beaaae1a1ec597c231956faf96c8b',
|
||||
'androidx.core:core:b1a90522c22cad8c5fb7a4f912493dbcde463c6a37b4148dfb9423763460f998',
|
||||
'androidx.versionedparcelable:versionedparcelable:f6438a93ed8016ccddca0e140a70be0b0110e0424edaa1472f84f98fed2e1ce3',
|
||||
'androidx.collection:collection:9c8d117b5c2bc120a1cdfeb857e05b495b16c36013570372a708f7827e3ac9f9',
|
||||
'androidx.core:core:45c7a50ad1f366e62db496d8cef7730d5ee1681215007d1a19e6b6d800a12842',
|
||||
'androidx.cursoradapter:cursoradapter:a81c8fe78815fa47df5b749deb52727ad11f9397da58b16017f4eb2c11e28564',
|
||||
'androidx.lifecycle:lifecycle-process:d8ff6fd844559743050c9ae010a6df230f2a3dbdf3e14498316f30bd8df836b5',
|
||||
'androidx.lifecycle:lifecycle-service:cb2b15bb0cf14134e953ed8ead96f94265018643f519367d51fd837f7311e9f8',
|
||||
'androidx.lifecycle:lifecycle-runtime:e4afc9e636183f6f3e0edf1cf46121a492ffd2c673075bb07f55c7a99dd43cfb',
|
||||
'androidx.lifecycle:lifecycle-livedata:c82609ced8c498f0a701a30fb6771bb7480860daee84d82e0a81ee86edf7ba39',
|
||||
'androidx.lifecycle:lifecycle-livedata-core:fde334ec7e22744c0f5bfe7caf1a84c9d717327044400577bdf9bd921ec4f7bc',
|
||||
'androidx.arch.core:core-runtime:87e65fc767c712b437649c7cee2431ebb4bed6daef82e501d4125b3ed3f65f8e',
|
||||
'androidx.arch.core:core-common:4b80b337779b526e64b0ee0ca9e0df43b808344d145f8e9b1c42a134dac57ad8',
|
||||
'androidx.lifecycle:lifecycle-common:7bad7a188804adea6fa1f35d5ef99b705f20bd93ecadde484760ff86b535fefc',
|
||||
'androidx.lifecycle:lifecycle-viewmodel:d6460aea1b6bad80ab14cf88297e9e43bfde8d87c3e5c28f2c508233ffbcc062',
|
||||
'androidx.concurrent:concurrent-listenablefuture-callback:14dce0acbffd705cfe9fb378960f851a9d8fc3f293d1157c310c9624a561d0a8',
|
||||
'androidx.concurrent:concurrent-listenablefuture:f9ef396ca4a43b9685d28bec117b278aa9171de0e446e5138e931074e3462feb',
|
||||
'com.github.bumptech.glide:gifdecoder:7ee9402ae1c48fac9232b67e81f881c217b907b3252e49ce57bdb97937ebb270',
|
||||
'androidx.versionedparcelable:versionedparcelable:948c751f6352d4c0f93f15fa1bf506c59083bc7754264dd9a325a6da0e2eec05',
|
||||
'androidx.collection:collection:632a0e5407461de774409352940e292a291037724207a787820c77daf7d33b72',
|
||||
'androidx.interpolator:interpolator:33193135a64fe21fa2c35eec6688f1a76e512606c0fc83dc1b689e37add7732a',
|
||||
'androidx.documentfile:documentfile:865a061ef2fad16522f8433536b8d47208c46ff7c7745197dfa1eeb481869487',
|
||||
'androidx.localbroadcastmanager:localbroadcastmanager:e71c328ceef5c4a7d76f2d86df1b65d65fe2acf868b1a4efd84a3f34336186d8',
|
||||
'androidx.print:print:1d5c7f3135a1bba661fc373fd72e11eb0a4adbb3396787826dd8e4190d5d9edd',
|
||||
'androidx.interpolator:interpolator:33193135a64fe21fa2c35eec6688f1a76e512606c0fc83dc1b689e37add7732a',
|
||||
'androidx.annotation:annotation:0baae9755f7caf52aa80cd04324b91ba93af55d4d1d17dcc9a7b53d99ef7c016',
|
||||
'androidx.lifecycle:lifecycle-process:d8ff6fd844559743050c9ae010a6df230f2a3dbdf3e14498316f30bd8df836b5',
|
||||
'androidx.lifecycle:lifecycle-service:cb2b15bb0cf14134e953ed8ead96f94265018643f519367d51fd837f7311e9f8',
|
||||
'androidx.lifecycle:lifecycle-runtime:7e6d414d03bb184f3015dacc6233eeaded45fa23f0cf4c1f6d3395d6495fa41c',
|
||||
'androidx.lifecycle:lifecycle-viewmodel:9f2efb59328027fa9f0c413d4d5910aab68d149b139ca8ce432135105b74833a',
|
||||
'androidx.savedstate:savedstate:115ac7313095b2d159565d2bc851a7722e43fc00347fc828214ff8917799b5f0',
|
||||
'androidx.lifecycle:lifecycle-common:76db6be533bd730fb361c2feb12a2c26d9952824746847da82601ef81f082643',
|
||||
'androidx.arch.core:core-common:fe1237bf029d063e7f29fe39aeaf73ef74c8b0a3658486fc29d3c54326653889',
|
||||
'androidx.annotation:annotation:d38d63edb30f1467818d50aaf05f8a692dea8b31392a049bfa991b159ad5b692',
|
||||
'androidx.constraintlayout:constraintlayout-solver:965c177e64fbd81bd1d27b402b66ef9d7bc7b5cb5f718044bf7a453abc542045',
|
||||
'com.google.auto.value:auto-value-annotations:0e951fee8c31f60270bc46553a8586001b7b93dbb12aec06373aa99a150392c0',
|
||||
'org.signal:signal-metadata-android:02323bc29317fa9d3b62fab0b507c94ba2e9bcc4a78d588888ffd313853757b3',
|
||||
'org.whispersystems:signal-service-java:6a1218cd6cebe6afbb613a00110a5c72708b3af5a7896d495ac4ed50ba58f07e',
|
||||
'com.github.bumptech.glide:disklrucache:4696a81340eb6beee21ab93f703ed6e7ae49fb4ce3bc2fbc546e5bacd21b96b9',
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<org.thoughtcrime.securesms.mediasend.camerax.CameraXView
|
||||
android:id="@+id/camerax_camera"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/camerax_controls_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
</FrameLayout>
|
|
@ -43,7 +43,11 @@ import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
|||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
|
||||
public class Camera1Fragment extends Fragment implements TextureView.SurfaceTextureListener,
|
||||
/**
|
||||
* Camera capture implemented with the legacy camera API's. Should only be used if sdk < 21.
|
||||
*/
|
||||
public class Camera1Fragment extends Fragment implements CameraFragment,
|
||||
TextureView.SurfaceTextureListener,
|
||||
Camera1Controller.EventListener
|
||||
{
|
||||
|
||||
|
@ -317,12 +321,6 @@ public class Camera1Fragment extends Fragment implements TextureView.SurfaceText
|
|||
}
|
||||
};
|
||||
|
||||
public interface Controller {
|
||||
void onCameraError();
|
||||
void onImageCaptured(@NonNull byte[] data, int width, int height);
|
||||
int getDisplayRotation();
|
||||
}
|
||||
|
||||
private enum Stage {
|
||||
SURFACE_AVAILABLE, CAMERA_PROPERTIES_AVAILABLE
|
||||
}
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
package org.thoughtcrime.securesms.mediasend;
|
||||
|
||||
import android.os.Build;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
public interface CameraFragment {
|
||||
|
||||
static Fragment newInstance() {
|
||||
if (Build.VERSION.SDK_INT >= 21) {
|
||||
return CameraXFragment.newInstance();
|
||||
} else {
|
||||
return Camera1Fragment.newInstance();
|
||||
}
|
||||
}
|
||||
|
||||
interface Controller {
|
||||
void onCameraError();
|
||||
void onImageCaptured(@NonNull byte[] data, int width, int height);
|
||||
int getDisplayRotation();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,199 @@
|
|||
package org.thoughtcrime.securesms.mediasend;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.res.Configuration;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.WindowManager;
|
||||
import android.view.animation.Animation;
|
||||
import android.view.animation.AnimationUtils;
|
||||
import android.view.animation.DecelerateInterpolator;
|
||||
import android.view.animation.RotateAnimation;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.camera.core.CameraX;
|
||||
import androidx.camera.core.ImageCapture;
|
||||
import androidx.camera.core.ImageProxy;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.mediasend.camerax.CameraXUtil;
|
||||
import org.thoughtcrime.securesms.mediasend.camerax.CameraXView;
|
||||
import org.thoughtcrime.securesms.util.Stopwatch;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Camera captured implemented using the CameraX SDK, which uses Camera2 under the hood. Should be
|
||||
* preferred whenever possible.
|
||||
*/
|
||||
@RequiresApi(21)
|
||||
public class CameraXFragment extends Fragment implements CameraFragment {
|
||||
|
||||
private static final String TAG = Log.tag(CameraXFragment.class);
|
||||
|
||||
private CameraXView camera;
|
||||
private ViewGroup controlsContainer;
|
||||
private Controller controller;
|
||||
|
||||
public static CameraXFragment newInstance() {
|
||||
return new CameraXFragment();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(@NonNull Context context) {
|
||||
super.onAttach(context);
|
||||
|
||||
if (!(getActivity() instanceof Controller)) {
|
||||
throw new IllegalStateException("Parent activity must implement controller interface.");
|
||||
}
|
||||
|
||||
this.controller = (Controller) getActivity();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public @Nullable View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.camerax_fragment, container, false);
|
||||
}
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
this.camera = view.findViewById(R.id.camerax_camera);
|
||||
this.controlsContainer = view.findViewById(R.id.camerax_controls_container);
|
||||
|
||||
camera.bindToLifecycle(this);
|
||||
camera.setCameraLensFacing(CameraXUtil.toLensFacing(TextSecurePreferences.getDirectCaptureCameraId(requireContext())));
|
||||
|
||||
onOrientationChanged(getResources().getConfiguration().orientation);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
|
||||
requireActivity().getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
|
||||
requireActivity().getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
CameraX.unbindAll();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConfigurationChanged(@NonNull Configuration newConfig) {
|
||||
super.onConfigurationChanged(newConfig);
|
||||
onOrientationChanged(newConfig.orientation);
|
||||
}
|
||||
|
||||
private void onOrientationChanged(int orientation) {
|
||||
int layout = orientation == Configuration.ORIENTATION_PORTRAIT ? R.layout.camera_controls_portrait
|
||||
: R.layout.camera_controls_landscape;
|
||||
|
||||
controlsContainer.removeAllViews();
|
||||
controlsContainer.addView(LayoutInflater.from(getContext()).inflate(layout, controlsContainer, false));
|
||||
initControls();
|
||||
}
|
||||
|
||||
@SuppressLint({"ClickableViewAccessibility", "MissingPermission"})
|
||||
private void initControls() {
|
||||
View flipButton = requireView().findViewById(R.id.camera_flip_button);
|
||||
View captureButton = requireView().findViewById(R.id.camera_capture_button);
|
||||
|
||||
captureButton.setOnTouchListener((v, event) -> {
|
||||
switch (event.getAction()) {
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
Animation shrinkAnimation = AnimationUtils.loadAnimation(getContext(), R.anim.camera_capture_button_shrink);
|
||||
shrinkAnimation.setFillAfter(true);
|
||||
shrinkAnimation.setFillEnabled(true);
|
||||
captureButton.startAnimation(shrinkAnimation);
|
||||
onCaptureClicked();
|
||||
break;
|
||||
case MotionEvent.ACTION_UP:
|
||||
case MotionEvent.ACTION_CANCEL:
|
||||
case MotionEvent.ACTION_OUTSIDE:
|
||||
Animation growAnimation = AnimationUtils.loadAnimation(getContext(), R.anim.camera_capture_button_grow);
|
||||
growAnimation.setFillAfter(true);
|
||||
growAnimation.setFillEnabled(true);
|
||||
captureButton.startAnimation(growAnimation);
|
||||
captureButton.setEnabled(false);
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
if (camera.hasCameraWithLensFacing(CameraX.LensFacing.FRONT) && camera.hasCameraWithLensFacing(CameraX.LensFacing.BACK)) {
|
||||
flipButton.setVisibility(View.VISIBLE);
|
||||
flipButton.setOnClickListener(v -> {
|
||||
camera.toggleCamera();
|
||||
TextSecurePreferences.setDirectCaptureCameraId(getContext(), CameraXUtil.toCameraDirectionInt(camera.getCameraLensFacing()));
|
||||
|
||||
Animation animation = new RotateAnimation(0, -180, RotateAnimation.RELATIVE_TO_SELF, 0.5f, RotateAnimation.RELATIVE_TO_SELF, 0.5f);
|
||||
animation.setDuration(200);
|
||||
animation.setInterpolator(new DecelerateInterpolator());
|
||||
flipButton.startAnimation(animation);
|
||||
});
|
||||
} else {
|
||||
flipButton.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
private void onCaptureClicked() {
|
||||
Stopwatch stopwatch = new Stopwatch("Capture");
|
||||
|
||||
camera.takePicture(new ImageCapture.OnImageCapturedListener() {
|
||||
@Override
|
||||
public void onCaptureSuccess(ImageProxy image, int rotationDegrees) {
|
||||
SimpleTask.run(CameraXFragment.this.getLifecycle(), () -> {
|
||||
stopwatch.split("captured");
|
||||
try {
|
||||
byte[] bytes = CameraXUtil.toJpegBytes(image, rotationDegrees, camera.getCameraLensFacing() == CameraX.LensFacing.FRONT);
|
||||
return new CaptureResult(bytes, image.getWidth(), image.getHeight());
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
} finally {
|
||||
image.close();
|
||||
}
|
||||
}, result -> {
|
||||
stopwatch.split("transformed");
|
||||
stopwatch.stop(TAG);
|
||||
|
||||
if (result != null) {
|
||||
controller.onImageCaptured(result.data, result.width, result.height);
|
||||
} else {
|
||||
controller.onCameraError();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ImageCapture.UseCaseError useCaseError, String message, @Nullable Throwable cause) {
|
||||
controller.onCameraError();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static final class CaptureResult {
|
||||
public final byte[] data;
|
||||
public final int width;
|
||||
public final int height;
|
||||
|
||||
private CaptureResult(byte[] data, int width, int height) {
|
||||
this.data = data;
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,6 +1,8 @@
|
|||
package org.thoughtcrime.securesms.mediasend;
|
||||
|
||||
import android.Manifest;
|
||||
|
||||
import androidx.fragment.app.FragmentTransaction;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
|
@ -50,7 +52,7 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
|
|||
MediaPickerItemFragment.Controller,
|
||||
MediaSendFragment.Controller,
|
||||
ImageEditorFragment.Controller,
|
||||
Camera1Fragment.Controller
|
||||
CameraFragment.Controller
|
||||
{
|
||||
private static final String TAG = MediaSendActivity.class.getSimpleName();
|
||||
|
||||
|
@ -144,7 +146,7 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
|
|||
boolean isCamera = getIntent().getBooleanExtra(KEY_IS_CAMERA, false);
|
||||
|
||||
if (isCamera) {
|
||||
Fragment fragment = Camera1Fragment.newInstance();
|
||||
Fragment fragment = CameraFragment.newInstance();
|
||||
getSupportFragmentManager().beginTransaction()
|
||||
.replace(R.id.mediasend_fragment_container, fragment, TAG_CAMERA)
|
||||
.commit();
|
||||
|
@ -216,7 +218,7 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
|
|||
@Override
|
||||
public void onMediaSelected(@NonNull Media media) {
|
||||
viewModel.onSingleMediaSelected(this, media);
|
||||
navigateToMediaSend(recipient, transport, dynamicLanguage.getCurrentLocale());
|
||||
navigateToMediaSend(recipient, transport, dynamicLanguage.getCurrentLocale(), false);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -305,7 +307,7 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
|
|||
Log.i(TAG, "Camera capture stored: " + media.getUri().toString());
|
||||
|
||||
viewModel.onImageCaptured(media);
|
||||
navigateToMediaSend(recipient, transport, dynamicLanguage.getCurrentLocale());
|
||||
navigateToMediaSend(recipient, transport, dynamicLanguage.getCurrentLocale(), true);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -323,7 +325,7 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
|
|||
animateButtonVisibility(countButton, countButton.getVisibility(), buttonState.isVisible() ? View.VISIBLE : View.GONE);
|
||||
|
||||
if (buttonState.getCount() > 0) {
|
||||
countButton.setOnClickListener(v -> navigateToMediaSend(recipient, transport, locale));
|
||||
countButton.setOnClickListener(v -> navigateToMediaSend(recipient, transport, locale, false));
|
||||
if (buttonState.isVisible()) {
|
||||
animateButtonTextChange(countButton);
|
||||
}
|
||||
|
@ -356,7 +358,7 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
|
|||
});
|
||||
}
|
||||
|
||||
private void navigateToMediaSend(@NonNull Recipient recipient, @NonNull TransportOption transport, @NonNull Locale locale) {
|
||||
private void navigateToMediaSend(@NonNull Recipient recipient, @NonNull TransportOption transport, @NonNull Locale locale, boolean fade) {
|
||||
MediaSendFragment fragment = MediaSendFragment.newInstance(recipient, transport, locale);
|
||||
String backstackTag = null;
|
||||
|
||||
|
@ -365,11 +367,17 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
|
|||
backstackTag = TAG_SEND;
|
||||
}
|
||||
|
||||
getSupportFragmentManager().beginTransaction()
|
||||
.setCustomAnimations(R.anim.slide_from_right, R.anim.slide_to_left, R.anim.slide_from_left, R.anim.slide_to_right)
|
||||
.replace(R.id.mediasend_fragment_container, fragment, TAG_SEND)
|
||||
.addToBackStack(backstackTag)
|
||||
.commit();
|
||||
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
|
||||
|
||||
if (fade) {
|
||||
transaction.setCustomAnimations(R.anim.fade_in, R.anim.fade_out, R.anim.fade_out, R.anim.fade_in);
|
||||
} else {
|
||||
transaction.setCustomAnimations(R.anim.slide_from_right, R.anim.slide_to_left, R.anim.slide_from_left, R.anim.slide_to_right);
|
||||
}
|
||||
|
||||
transaction.replace(R.id.mediasend_fragment_container, fragment, TAG_SEND)
|
||||
.addToBackStack(backstackTag)
|
||||
.commit();
|
||||
}
|
||||
|
||||
private void navigateToCamera() {
|
||||
|
@ -379,7 +387,7 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
|
|||
.withRationaleDialog(getString(R.string.ConversationActivity_to_capture_photos_and_video_allow_signal_access_to_the_camera), R.drawable.ic_photo_camera_white_48dp)
|
||||
.withPermanentDenialDialog(getString(R.string.ConversationActivity_signal_needs_the_camera_permission_to_take_photos_or_video))
|
||||
.onAllGranted(() -> {
|
||||
Camera1Fragment fragment = getOrCreateCameraFragment();
|
||||
Fragment fragment = getOrCreateCameraFragment();
|
||||
getSupportFragmentManager().beginTransaction()
|
||||
.setCustomAnimations(R.anim.slide_from_right, R.anim.slide_to_left, R.anim.slide_from_left, R.anim.slide_to_right)
|
||||
.replace(R.id.mediasend_fragment_container, fragment, TAG_CAMERA)
|
||||
|
@ -390,11 +398,11 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
|
|||
.execute();
|
||||
}
|
||||
|
||||
private Camera1Fragment getOrCreateCameraFragment() {
|
||||
Camera1Fragment fragment = (Camera1Fragment) getSupportFragmentManager().findFragmentByTag(TAG_CAMERA);
|
||||
private Fragment getOrCreateCameraFragment() {
|
||||
Fragment fragment = getSupportFragmentManager().findFragmentByTag(TAG_CAMERA);
|
||||
|
||||
return fragment != null ? fragment
|
||||
: Camera1Fragment.newInstance();
|
||||
: CameraFragment.newInstance();
|
||||
}
|
||||
|
||||
private void animateButtonVisibility(@NonNull View button, int oldVisibility, int newVisibility) {
|
||||
|
|
|
@ -0,0 +1,797 @@
|
|||
package org.thoughtcrime.securesms.mediasend.camerax;
|
||||
|
||||
/*
|
||||
* Copyright (C) 2019 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import android.Manifest.permission;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.graphics.Matrix;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.SurfaceTexture;
|
||||
import android.hardware.camera2.CameraAccessException;
|
||||
import android.hardware.camera2.CameraCharacteristics;
|
||||
import android.hardware.camera2.CameraManager;
|
||||
import android.os.Looper;
|
||||
import android.util.Log;
|
||||
import android.util.Rational;
|
||||
import android.util.Size;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.annotation.RequiresPermission;
|
||||
import androidx.annotation.UiThread;
|
||||
import androidx.camera.core.CameraInfo;
|
||||
import androidx.camera.core.CameraInfoUnavailableException;
|
||||
import androidx.camera.core.CameraOrientationUtil;
|
||||
import androidx.camera.core.CameraX;
|
||||
import androidx.camera.core.CameraX.LensFacing;
|
||||
import androidx.camera.core.FlashMode;
|
||||
import androidx.camera.core.ImageCapture;
|
||||
import androidx.camera.core.ImageCapture.OnImageCapturedListener;
|
||||
import androidx.camera.core.ImageCapture.OnImageSavedListener;
|
||||
import androidx.camera.core.ImageCaptureConfig;
|
||||
import androidx.camera.core.Preview;
|
||||
import androidx.camera.core.PreviewConfig;
|
||||
import androidx.camera.core.VideoCapture;
|
||||
import androidx.camera.core.VideoCapture.OnVideoSavedListener;
|
||||
import androidx.camera.core.VideoCaptureConfig;
|
||||
import androidx.lifecycle.Lifecycle;
|
||||
import androidx.lifecycle.LifecycleObserver;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
import androidx.lifecycle.OnLifecycleEvent;
|
||||
|
||||
import org.thoughtcrime.securesms.mediasend.camerax.CameraXView.CaptureMode;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
/** CameraX use case operation built on @{link androidx.camera.core}. */
|
||||
@RequiresApi(21)
|
||||
@SuppressLint("RestrictedApi")
|
||||
final class CameraXModule {
|
||||
public static final String TAG = "CameraXModule";
|
||||
|
||||
private static final int MAX_VIEW_DIMENSION = 2000;
|
||||
private static final float UNITY_ZOOM_SCALE = 1f;
|
||||
private static final float ZOOM_NOT_SUPPORTED = UNITY_ZOOM_SCALE;
|
||||
private static final Rational ASPECT_RATIO_16_9 = new Rational(16, 9);
|
||||
private static final Rational ASPECT_RATIO_4_3 = new Rational(4, 3);
|
||||
private static final Rational ASPECT_RATIO_9_16 = new Rational(9, 16);
|
||||
private static final Rational ASPECT_RATIO_3_4 = new Rational(3, 4);
|
||||
|
||||
private final CameraManager mCameraManager;
|
||||
private final PreviewConfig.Builder mPreviewConfigBuilder;
|
||||
private final VideoCaptureConfig.Builder mVideoCaptureConfigBuilder;
|
||||
private final ImageCaptureConfig.Builder mImageCaptureConfigBuilder;
|
||||
private final CameraXView mCameraXView;
|
||||
final AtomicBoolean mVideoIsRecording = new AtomicBoolean(false);
|
||||
private CaptureMode mCaptureMode = CaptureMode.IMAGE;
|
||||
private long mMaxVideoDuration = CameraXView.INDEFINITE_VIDEO_DURATION;
|
||||
private long mMaxVideoSize = CameraXView.INDEFINITE_VIDEO_SIZE;
|
||||
private FlashMode mFlash = FlashMode.OFF;
|
||||
@Nullable
|
||||
private ImageCapture mImageCapture;
|
||||
@Nullable
|
||||
private VideoCapture mVideoCapture;
|
||||
@Nullable
|
||||
Preview mPreview;
|
||||
@Nullable
|
||||
LifecycleOwner mCurrentLifecycle;
|
||||
private final LifecycleObserver mCurrentLifecycleObserver =
|
||||
new LifecycleObserver() {
|
||||
@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
|
||||
public void onDestroy(LifecycleOwner owner) {
|
||||
if (owner == mCurrentLifecycle) {
|
||||
clearCurrentLifecycle();
|
||||
mPreview.removePreviewOutputListener();
|
||||
}
|
||||
}
|
||||
};
|
||||
@Nullable
|
||||
private LifecycleOwner mNewLifecycle;
|
||||
private float mZoomLevel = UNITY_ZOOM_SCALE;
|
||||
@Nullable
|
||||
private Rect mCropRegion;
|
||||
@Nullable
|
||||
private CameraX.LensFacing mCameraLensFacing = LensFacing.BACK;
|
||||
|
||||
CameraXModule(CameraXView view) {
|
||||
this.mCameraXView = view;
|
||||
|
||||
mCameraManager = (CameraManager) view.getContext().getSystemService(Context.CAMERA_SERVICE);
|
||||
|
||||
mPreviewConfigBuilder = new PreviewConfig.Builder().setTargetName("Preview");
|
||||
|
||||
mImageCaptureConfigBuilder =
|
||||
new ImageCaptureConfig.Builder().setTargetName("ImageCapture");
|
||||
|
||||
mVideoCaptureConfigBuilder =
|
||||
new VideoCaptureConfig.Builder().setTargetName("VideoCapture");
|
||||
}
|
||||
|
||||
/**
|
||||
* Rescales view rectangle with dimensions in [-1000, 1000] to a corresponding rectangle in the
|
||||
* sensor coordinate frame.
|
||||
*/
|
||||
private static Rect rescaleViewRectToSensorRect(Rect view, Rect sensor) {
|
||||
// Scale width and height.
|
||||
int newWidth = Math.round(view.width() * sensor.width() / (float) MAX_VIEW_DIMENSION);
|
||||
int newHeight = Math.round(view.height() * sensor.height() / (float) MAX_VIEW_DIMENSION);
|
||||
|
||||
// Scale top/left corner.
|
||||
int halfViewDimension = MAX_VIEW_DIMENSION / 2;
|
||||
int leftOffset =
|
||||
Math.round(
|
||||
(view.left + halfViewDimension)
|
||||
* sensor.width()
|
||||
/ (float) MAX_VIEW_DIMENSION)
|
||||
+ sensor.left;
|
||||
int topOffset =
|
||||
Math.round(
|
||||
(view.top + halfViewDimension)
|
||||
* sensor.height()
|
||||
/ (float) MAX_VIEW_DIMENSION)
|
||||
+ sensor.top;
|
||||
|
||||
// Now, produce the scaled rect.
|
||||
Rect scaled = new Rect();
|
||||
scaled.left = leftOffset;
|
||||
scaled.top = topOffset;
|
||||
scaled.right = scaled.left + newWidth;
|
||||
scaled.bottom = scaled.top + newHeight;
|
||||
return scaled;
|
||||
}
|
||||
|
||||
@RequiresPermission(permission.CAMERA)
|
||||
public void bindToLifecycle(LifecycleOwner lifecycleOwner) {
|
||||
mNewLifecycle = lifecycleOwner;
|
||||
|
||||
if (getMeasuredWidth() > 0 && getMeasuredHeight() > 0) {
|
||||
bindToLifecycleAfterViewMeasured();
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresPermission(permission.CAMERA)
|
||||
void bindToLifecycleAfterViewMeasured() {
|
||||
if (mNewLifecycle == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
clearCurrentLifecycle();
|
||||
mCurrentLifecycle = mNewLifecycle;
|
||||
mNewLifecycle = null;
|
||||
if (mCurrentLifecycle.getLifecycle().getCurrentState() == Lifecycle.State.DESTROYED) {
|
||||
mCurrentLifecycle = null;
|
||||
throw new IllegalArgumentException("Cannot bind to lifecycle in a destroyed state.");
|
||||
}
|
||||
|
||||
final int cameraOrientation;
|
||||
try {
|
||||
String cameraId;
|
||||
Set<LensFacing> available = getAvailableCameraLensFacing();
|
||||
|
||||
if (available.isEmpty()) {
|
||||
Log.w(TAG, "Unable to bindToLifeCycle since no cameras available");
|
||||
mCameraLensFacing = null;
|
||||
}
|
||||
|
||||
// Ensure the current camera exists, or default to another camera
|
||||
if (mCameraLensFacing != null && !available.contains(mCameraLensFacing)) {
|
||||
Log.w(TAG, "Camera does not exist with direction " + mCameraLensFacing);
|
||||
|
||||
// Default to the first available camera direction
|
||||
mCameraLensFacing = available.iterator().next();
|
||||
|
||||
Log.w(TAG, "Defaulting to primary camera with direction " + mCameraLensFacing);
|
||||
}
|
||||
|
||||
// Do not attempt to create use cases for a null cameraLensFacing. This could occur if
|
||||
// the
|
||||
// user explicitly sets the LensFacing to null, or if we determined there
|
||||
// were no available cameras, which should be logged in the logic above.
|
||||
if (mCameraLensFacing == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
cameraId = CameraX.getCameraWithLensFacing(mCameraLensFacing);
|
||||
if (cameraId == null) {
|
||||
return;
|
||||
}
|
||||
CameraInfo cameraInfo = CameraX.getCameraInfo(cameraId);
|
||||
cameraOrientation = cameraInfo.getSensorRotationDegrees();
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException("Unable to bind to lifecycle.", e);
|
||||
}
|
||||
|
||||
// Set the preferred aspect ratio as 4:3 if it is IMAGE only mode. Set the preferred aspect
|
||||
// ratio as 16:9 if it is VIDEO or MIXED mode. Then, it will be WYSIWYG when the view finder
|
||||
// is
|
||||
// in CENTER_INSIDE mode.
|
||||
|
||||
boolean isDisplayPortrait = getDisplayRotationDegrees() == 0
|
||||
|| getDisplayRotationDegrees() == 180;
|
||||
|
||||
if (getCaptureMode() == CaptureMode.IMAGE) {
|
||||
mImageCaptureConfigBuilder.setTargetAspectRatio(
|
||||
isDisplayPortrait ? ASPECT_RATIO_3_4 : ASPECT_RATIO_4_3);
|
||||
mPreviewConfigBuilder.setTargetAspectRatio(
|
||||
isDisplayPortrait ? ASPECT_RATIO_3_4 : ASPECT_RATIO_4_3);
|
||||
} else {
|
||||
mImageCaptureConfigBuilder.setTargetAspectRatio(
|
||||
isDisplayPortrait ? ASPECT_RATIO_9_16 : ASPECT_RATIO_16_9);
|
||||
mPreviewConfigBuilder.setTargetAspectRatio(
|
||||
isDisplayPortrait ? ASPECT_RATIO_9_16 : ASPECT_RATIO_16_9);
|
||||
}
|
||||
|
||||
mImageCaptureConfigBuilder.setTargetRotation(getDisplaySurfaceRotation());
|
||||
mImageCaptureConfigBuilder.setLensFacing(mCameraLensFacing);
|
||||
mImageCaptureConfigBuilder.setCaptureMode(CameraXUtil.getOptimalCaptureMode());
|
||||
mImageCaptureConfigBuilder.setTargetResolution(new Size(1920, 1080));
|
||||
mImageCapture = new ImageCapture(mImageCaptureConfigBuilder.build());
|
||||
|
||||
mVideoCaptureConfigBuilder.setTargetRotation(getDisplaySurfaceRotation());
|
||||
mVideoCaptureConfigBuilder.setLensFacing(mCameraLensFacing);
|
||||
mVideoCapture = new VideoCapture(mVideoCaptureConfigBuilder.build());
|
||||
mPreviewConfigBuilder.setLensFacing(mCameraLensFacing);
|
||||
|
||||
int relativeCameraOrientation = getRelativeCameraOrientation(false);
|
||||
|
||||
if (relativeCameraOrientation == 90 || relativeCameraOrientation == 270) {
|
||||
mPreviewConfigBuilder.setTargetResolution(
|
||||
new Size(getMeasuredHeight(), getMeasuredWidth()));
|
||||
} else {
|
||||
mPreviewConfigBuilder.setTargetResolution(
|
||||
new Size(getMeasuredWidth(), getMeasuredHeight()));
|
||||
}
|
||||
|
||||
mPreview = new Preview(mPreviewConfigBuilder.build());
|
||||
mPreview.setOnPreviewOutputUpdateListener(
|
||||
new Preview.OnPreviewOutputUpdateListener() {
|
||||
@Override
|
||||
public void onUpdated(Preview.PreviewOutput output) {
|
||||
boolean needReverse = cameraOrientation != 0 && cameraOrientation != 180;
|
||||
int textureWidth =
|
||||
needReverse
|
||||
? output.getTextureSize().getHeight()
|
||||
: output.getTextureSize().getWidth();
|
||||
int textureHeight =
|
||||
needReverse
|
||||
? output.getTextureSize().getWidth()
|
||||
: output.getTextureSize().getHeight();
|
||||
CameraXModule.this.onPreviewSourceDimensUpdated(textureWidth,
|
||||
textureHeight);
|
||||
CameraXModule.this.setSurfaceTexture(output.getSurfaceTexture());
|
||||
}
|
||||
});
|
||||
|
||||
if (getCaptureMode() == CaptureMode.IMAGE) {
|
||||
CameraX.bindToLifecycle(mCurrentLifecycle, mImageCapture, mPreview);
|
||||
} else if (getCaptureMode() == CaptureMode.VIDEO) {
|
||||
CameraX.bindToLifecycle(mCurrentLifecycle, mVideoCapture, mPreview);
|
||||
} else {
|
||||
CameraX.bindToLifecycle(mCurrentLifecycle, mImageCapture, mVideoCapture, mPreview);
|
||||
}
|
||||
setZoomLevel(mZoomLevel);
|
||||
mCurrentLifecycle.getLifecycle().addObserver(mCurrentLifecycleObserver);
|
||||
// Enable flash setting in ImageCapture after use cases are created and binded.
|
||||
setFlash(getFlash());
|
||||
}
|
||||
|
||||
public void open() {
|
||||
throw new UnsupportedOperationException(
|
||||
"Explicit open/close of camera not yet supported. Use bindtoLifecycle() instead.");
|
||||
}
|
||||
|
||||
public void close() {
|
||||
throw new UnsupportedOperationException(
|
||||
"Explicit open/close of camera not yet supported. Use bindtoLifecycle() instead.");
|
||||
}
|
||||
|
||||
public void stopPreview() {
|
||||
if (mPreview != null) {
|
||||
mPreview.clear();
|
||||
}
|
||||
}
|
||||
|
||||
public void takePicture(OnImageCapturedListener listener) {
|
||||
if (mImageCapture == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (getCaptureMode() == CaptureMode.VIDEO) {
|
||||
throw new IllegalStateException("Can not take picture under VIDEO capture mode.");
|
||||
}
|
||||
|
||||
if (listener == null) {
|
||||
throw new IllegalArgumentException("OnImageCapturedListener should not be empty");
|
||||
}
|
||||
|
||||
mImageCapture.takePicture(listener);
|
||||
}
|
||||
|
||||
public void takePicture(File saveLocation, OnImageSavedListener listener) {
|
||||
if (mImageCapture == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (getCaptureMode() == CaptureMode.VIDEO) {
|
||||
throw new IllegalStateException("Can not take picture under VIDEO capture mode.");
|
||||
}
|
||||
|
||||
if (listener == null) {
|
||||
throw new IllegalArgumentException("OnImageSavedListener should not be empty");
|
||||
}
|
||||
|
||||
ImageCapture.Metadata metadata = new ImageCapture.Metadata();
|
||||
metadata.isReversedHorizontal = mCameraLensFacing == LensFacing.FRONT;
|
||||
mImageCapture.takePicture(saveLocation, listener, metadata);
|
||||
}
|
||||
|
||||
public void startRecording(File file, final OnVideoSavedListener listener) {
|
||||
if (mVideoCapture == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (getCaptureMode() == CaptureMode.IMAGE) {
|
||||
throw new IllegalStateException("Can not record video under IMAGE capture mode.");
|
||||
}
|
||||
|
||||
if (listener == null) {
|
||||
throw new IllegalArgumentException("OnVideoSavedListener should not be empty");
|
||||
}
|
||||
|
||||
mVideoIsRecording.set(true);
|
||||
mVideoCapture.startRecording(
|
||||
file,
|
||||
new VideoCapture.OnVideoSavedListener() {
|
||||
@Override
|
||||
public void onVideoSaved(File savedFile) {
|
||||
mVideoIsRecording.set(false);
|
||||
listener.onVideoSaved(savedFile);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(
|
||||
VideoCapture.UseCaseError useCaseError,
|
||||
String message,
|
||||
@Nullable Throwable cause) {
|
||||
mVideoIsRecording.set(false);
|
||||
Log.e(TAG, message, cause);
|
||||
listener.onError(useCaseError, message, cause);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void stopRecording() {
|
||||
if (mVideoCapture == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
mVideoCapture.stopRecording();
|
||||
}
|
||||
|
||||
public boolean isRecording() {
|
||||
return mVideoIsRecording.get();
|
||||
}
|
||||
|
||||
// TODO(b/124269166): Rethink how we can handle permissions here.
|
||||
@SuppressLint("MissingPermission")
|
||||
public void setCameraLensFacing(@Nullable LensFacing lensFacing) {
|
||||
// Setting same lens facing is a no-op, so check for that first
|
||||
if (mCameraLensFacing != lensFacing) {
|
||||
// If we're not bound to a lifecycle, just update the camera that will be opened when we
|
||||
// attach to a lifecycle.
|
||||
mCameraLensFacing = lensFacing;
|
||||
|
||||
if (mCurrentLifecycle != null) {
|
||||
// Re-bind to lifecycle with new camera
|
||||
bindToLifecycle(mCurrentLifecycle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresPermission(permission.CAMERA)
|
||||
public boolean hasCameraWithLensFacing(LensFacing lensFacing) {
|
||||
String cameraId;
|
||||
try {
|
||||
cameraId = CameraX.getCameraWithLensFacing(lensFacing);
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException("Unable to query lens facing.", e);
|
||||
}
|
||||
|
||||
return cameraId != null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public LensFacing getLensFacing() {
|
||||
return mCameraLensFacing;
|
||||
}
|
||||
|
||||
public void toggleCamera() {
|
||||
// TODO(b/124269166): Rethink how we can handle permissions here.
|
||||
@SuppressLint("MissingPermission")
|
||||
Set<LensFacing> availableCameraLensFacing = getAvailableCameraLensFacing();
|
||||
|
||||
if (availableCameraLensFacing.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (mCameraLensFacing == null) {
|
||||
setCameraLensFacing(availableCameraLensFacing.iterator().next());
|
||||
return;
|
||||
}
|
||||
|
||||
if (mCameraLensFacing == LensFacing.BACK
|
||||
&& availableCameraLensFacing.contains(LensFacing.FRONT)) {
|
||||
setCameraLensFacing(LensFacing.FRONT);
|
||||
return;
|
||||
}
|
||||
|
||||
if (mCameraLensFacing == LensFacing.FRONT
|
||||
&& availableCameraLensFacing.contains(LensFacing.BACK)) {
|
||||
setCameraLensFacing(LensFacing.BACK);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
public void focus(Rect focus, Rect metering) {
|
||||
if (mPreview == null) {
|
||||
// Nothing to focus on since we don't yet have a preview
|
||||
return;
|
||||
}
|
||||
|
||||
Rect rescaledFocus;
|
||||
Rect rescaledMetering;
|
||||
try {
|
||||
Rect sensorRegion;
|
||||
if (mCropRegion != null) {
|
||||
sensorRegion = mCropRegion;
|
||||
} else {
|
||||
sensorRegion = getSensorSize(getActiveCamera());
|
||||
}
|
||||
rescaledFocus = rescaleViewRectToSensorRect(focus, sensorRegion);
|
||||
rescaledMetering = rescaleViewRectToSensorRect(metering, sensorRegion);
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Failed to rescale the focus and metering rectangles.", e);
|
||||
return;
|
||||
}
|
||||
|
||||
mPreview.focus(rescaledFocus, rescaledMetering);
|
||||
}
|
||||
|
||||
public float getZoomLevel() {
|
||||
return mZoomLevel;
|
||||
}
|
||||
|
||||
public void setZoomLevel(float zoomLevel) {
|
||||
// Set the zoom level in case it is set before binding to a lifecycle
|
||||
this.mZoomLevel = zoomLevel;
|
||||
|
||||
if (mPreview == null) {
|
||||
// Nothing to zoom on yet since we don't have a preview. Defer calculating crop
|
||||
// region.
|
||||
return;
|
||||
}
|
||||
|
||||
Rect sensorSize;
|
||||
try {
|
||||
sensorSize = getSensorSize(getActiveCamera());
|
||||
if (sensorSize == null) {
|
||||
Log.e(TAG, "Failed to get the sensor size.");
|
||||
return;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Failed to get the sensor size.", e);
|
||||
return;
|
||||
}
|
||||
|
||||
float minZoom = getMinZoomLevel();
|
||||
float maxZoom = getMaxZoomLevel();
|
||||
|
||||
if (this.mZoomLevel < minZoom) {
|
||||
Log.e(TAG, "Requested zoom level is less than minimum zoom level.");
|
||||
}
|
||||
if (this.mZoomLevel > maxZoom) {
|
||||
Log.e(TAG, "Requested zoom level is greater than maximum zoom level.");
|
||||
}
|
||||
this.mZoomLevel = Math.max(minZoom, Math.min(maxZoom, this.mZoomLevel));
|
||||
|
||||
float zoomScaleFactor =
|
||||
(maxZoom == minZoom) ? minZoom : (this.mZoomLevel - minZoom) / (maxZoom - minZoom);
|
||||
int minWidth = Math.round(sensorSize.width() / maxZoom);
|
||||
int minHeight = Math.round(sensorSize.height() / maxZoom);
|
||||
int diffWidth = sensorSize.width() - minWidth;
|
||||
int diffHeight = sensorSize.height() - minHeight;
|
||||
float cropWidth = diffWidth * zoomScaleFactor;
|
||||
float cropHeight = diffHeight * zoomScaleFactor;
|
||||
|
||||
Rect cropRegion =
|
||||
new Rect(
|
||||
/*left=*/ (int) Math.ceil(cropWidth / 2 - 0.5f),
|
||||
/*top=*/ (int) Math.ceil(cropHeight / 2 - 0.5f),
|
||||
/*right=*/ (int) Math.floor(sensorSize.width() - cropWidth / 2 + 0.5f),
|
||||
/*bottom=*/ (int) Math.floor(sensorSize.height() - cropHeight / 2 + 0.5f));
|
||||
|
||||
if (cropRegion.width() < 50 || cropRegion.height() < 50) {
|
||||
Log.e(TAG, "Crop region is too small to compute 3A stats, so ignoring further zoom.");
|
||||
return;
|
||||
}
|
||||
this.mCropRegion = cropRegion;
|
||||
|
||||
mPreview.zoom(cropRegion);
|
||||
}
|
||||
|
||||
public float getMinZoomLevel() {
|
||||
return UNITY_ZOOM_SCALE;
|
||||
}
|
||||
|
||||
public float getMaxZoomLevel() {
|
||||
try {
|
||||
CameraCharacteristics characteristics =
|
||||
mCameraManager.getCameraCharacteristics(getActiveCamera());
|
||||
Float maxZoom =
|
||||
characteristics.get(CameraCharacteristics.SCALER_AVAILABLE_MAX_DIGITAL_ZOOM);
|
||||
if (maxZoom == null) {
|
||||
return ZOOM_NOT_SUPPORTED;
|
||||
}
|
||||
if (maxZoom == ZOOM_NOT_SUPPORTED) {
|
||||
return ZOOM_NOT_SUPPORTED;
|
||||
}
|
||||
return maxZoom;
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Failed to get SCALER_AVAILABLE_MAX_DIGITAL_ZOOM.", e);
|
||||
}
|
||||
return ZOOM_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
public boolean isZoomSupported() {
|
||||
return getMaxZoomLevel() != ZOOM_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
// TODO(b/124269166): Rethink how we can handle permissions here.
|
||||
@SuppressLint("MissingPermission")
|
||||
private void rebindToLifecycle() {
|
||||
if (mCurrentLifecycle != null) {
|
||||
bindToLifecycle(mCurrentLifecycle);
|
||||
}
|
||||
}
|
||||
|
||||
int getRelativeCameraOrientation(boolean compensateForMirroring) {
|
||||
int rotationDegrees;
|
||||
try {
|
||||
String cameraId = CameraX.getCameraWithLensFacing(getLensFacing());
|
||||
CameraInfo cameraInfo = CameraX.getCameraInfo(cameraId);
|
||||
rotationDegrees = cameraInfo.getSensorRotationDegrees(getDisplaySurfaceRotation());
|
||||
if (compensateForMirroring) {
|
||||
rotationDegrees = (360 - rotationDegrees) % 360;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Failed to query camera", e);
|
||||
rotationDegrees = 0;
|
||||
}
|
||||
|
||||
return rotationDegrees;
|
||||
}
|
||||
|
||||
public void invalidateView() {
|
||||
transformPreview();
|
||||
updateViewInfo();
|
||||
}
|
||||
|
||||
void clearCurrentLifecycle() {
|
||||
if (mCurrentLifecycle != null) {
|
||||
// Remove previous use cases
|
||||
CameraX.unbind(mImageCapture, mVideoCapture, mPreview);
|
||||
}
|
||||
|
||||
mCurrentLifecycle = null;
|
||||
}
|
||||
|
||||
private Rect getSensorSize(String cameraId) throws CameraAccessException {
|
||||
CameraCharacteristics characteristics = mCameraManager.getCameraCharacteristics(cameraId);
|
||||
return characteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE);
|
||||
}
|
||||
|
||||
String getActiveCamera() throws CameraInfoUnavailableException {
|
||||
return CameraX.getCameraWithLensFacing(mCameraLensFacing);
|
||||
}
|
||||
|
||||
@UiThread
|
||||
private void transformPreview() {
|
||||
int previewWidth = getPreviewWidth();
|
||||
int previewHeight = getPreviewHeight();
|
||||
int displayOrientation = getDisplayRotationDegrees();
|
||||
|
||||
Matrix matrix = new Matrix();
|
||||
|
||||
// Apply rotation of the display
|
||||
int rotation = -displayOrientation;
|
||||
|
||||
int px = (int) Math.round(previewWidth / 2d);
|
||||
int py = (int) Math.round(previewHeight / 2d);
|
||||
|
||||
matrix.postRotate(rotation, px, py);
|
||||
|
||||
if (displayOrientation == 90 || displayOrientation == 270) {
|
||||
// Swap width and height
|
||||
float xScale = previewWidth / (float) previewHeight;
|
||||
float yScale = previewHeight / (float) previewWidth;
|
||||
|
||||
matrix.postScale(xScale, yScale, px, py);
|
||||
}
|
||||
|
||||
setTransform(matrix);
|
||||
}
|
||||
|
||||
// Update view related information used in use cases
|
||||
private void updateViewInfo() {
|
||||
if (mImageCapture != null) {
|
||||
mImageCapture.setTargetAspectRatio(new Rational(getWidth(), getHeight()));
|
||||
mImageCapture.setTargetRotation(getDisplaySurfaceRotation());
|
||||
}
|
||||
|
||||
if (mVideoCapture != null) {
|
||||
mVideoCapture.setTargetRotation(getDisplaySurfaceRotation());
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresPermission(permission.CAMERA)
|
||||
private Set<LensFacing> getAvailableCameraLensFacing() {
|
||||
// Start with all camera directions
|
||||
Set<LensFacing> available = new LinkedHashSet<>(Arrays.asList(LensFacing.values()));
|
||||
|
||||
// If we're bound to a lifecycle, remove unavailable cameras
|
||||
if (mCurrentLifecycle != null) {
|
||||
if (!hasCameraWithLensFacing(LensFacing.BACK)) {
|
||||
available.remove(LensFacing.BACK);
|
||||
}
|
||||
|
||||
if (!hasCameraWithLensFacing(LensFacing.FRONT)) {
|
||||
available.remove(LensFacing.FRONT);
|
||||
}
|
||||
}
|
||||
|
||||
return available;
|
||||
}
|
||||
|
||||
public FlashMode getFlash() {
|
||||
return mFlash;
|
||||
}
|
||||
|
||||
public void setFlash(FlashMode flash) {
|
||||
this.mFlash = flash;
|
||||
|
||||
if (mImageCapture == null) {
|
||||
// Do nothing if there is no imageCapture
|
||||
return;
|
||||
}
|
||||
|
||||
mImageCapture.setFlashMode(flash);
|
||||
}
|
||||
|
||||
public void enableTorch(boolean torch) {
|
||||
if (mPreview == null) {
|
||||
return;
|
||||
}
|
||||
mPreview.enableTorch(torch);
|
||||
}
|
||||
|
||||
public boolean isTorchOn() {
|
||||
if (mPreview == null) {
|
||||
return false;
|
||||
}
|
||||
return mPreview.isTorchOn();
|
||||
}
|
||||
|
||||
public Context getContext() {
|
||||
return mCameraXView.getContext();
|
||||
}
|
||||
|
||||
public int getWidth() {
|
||||
return mCameraXView.getWidth();
|
||||
}
|
||||
|
||||
public int getHeight() {
|
||||
return mCameraXView.getHeight();
|
||||
}
|
||||
|
||||
public int getDisplayRotationDegrees() {
|
||||
return CameraOrientationUtil.surfaceRotationToDegrees(getDisplaySurfaceRotation());
|
||||
}
|
||||
|
||||
protected int getDisplaySurfaceRotation() {
|
||||
return mCameraXView.getDisplaySurfaceRotation();
|
||||
}
|
||||
|
||||
public void setSurfaceTexture(SurfaceTexture st) {
|
||||
mCameraXView.setSurfaceTexture(st);
|
||||
}
|
||||
|
||||
private int getPreviewWidth() {
|
||||
return mCameraXView.getPreviewWidth();
|
||||
}
|
||||
|
||||
private int getPreviewHeight() {
|
||||
return mCameraXView.getPreviewHeight();
|
||||
}
|
||||
|
||||
private int getMeasuredWidth() {
|
||||
return mCameraXView.getMeasuredWidth();
|
||||
}
|
||||
|
||||
private int getMeasuredHeight() {
|
||||
return mCameraXView.getMeasuredHeight();
|
||||
}
|
||||
|
||||
void setTransform(final Matrix matrix) {
|
||||
if (Looper.myLooper() != Looper.getMainLooper()) {
|
||||
mCameraXView.post(
|
||||
new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
setTransform(matrix);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
mCameraXView.setTransform(matrix);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify the view that the source dimensions have changed.
|
||||
*
|
||||
* <p>This will allow the view to layout the preview to display the correct aspect ratio.
|
||||
*
|
||||
* @param width width of camera source buffers.
|
||||
* @param height height of camera source buffers.
|
||||
*/
|
||||
void onPreviewSourceDimensUpdated(int width, int height) {
|
||||
mCameraXView.onPreviewSourceDimensUpdated(width, height);
|
||||
}
|
||||
|
||||
public CaptureMode getCaptureMode() {
|
||||
return mCaptureMode;
|
||||
}
|
||||
|
||||
public void setCaptureMode(CaptureMode captureMode) {
|
||||
this.mCaptureMode = captureMode;
|
||||
rebindToLifecycle();
|
||||
}
|
||||
|
||||
public long getMaxVideoDuration() {
|
||||
return mMaxVideoDuration;
|
||||
}
|
||||
|
||||
public void setMaxVideoDuration(long duration) {
|
||||
mMaxVideoDuration = duration;
|
||||
}
|
||||
|
||||
public long getMaxVideoSize() {
|
||||
return mMaxVideoSize;
|
||||
}
|
||||
|
||||
public void setMaxVideoSize(long size) {
|
||||
mMaxVideoSize = size;
|
||||
}
|
||||
|
||||
public boolean isPaused() {
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,122 @@
|
|||
package org.thoughtcrime.securesms.mediasend.camerax;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.BitmapRegionDecoder;
|
||||
import android.graphics.Matrix;
|
||||
import android.graphics.Rect;
|
||||
import android.hardware.Camera;
|
||||
import android.os.Build;
|
||||
import android.util.Size;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.camera.core.CameraX;
|
||||
import androidx.camera.core.ImageCapture;
|
||||
import androidx.camera.core.ImageProxy;
|
||||
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.util.Stopwatch;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
public class CameraXUtil {
|
||||
|
||||
private static final String TAG = Log.tag(CameraXUtil.class);
|
||||
|
||||
@RequiresApi(21)
|
||||
public static byte[] toJpegBytes(@NonNull ImageProxy image, int rotation, boolean flip) throws IOException {
|
||||
ImageProxy.PlaneProxy[] planes = image.getPlanes();
|
||||
ByteBuffer buffer = planes[0].getBuffer();
|
||||
Rect cropRect = shouldCropImage(image) ? image.getCropRect() : null;
|
||||
byte[] data = new byte[buffer.capacity()];
|
||||
|
||||
buffer.get(data);
|
||||
|
||||
if (cropRect != null || rotation != 0 || flip) {
|
||||
data = transformByteArray(data, cropRect, rotation, flip);
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
public static int toCameraDirectionInt(@Nullable CameraX.LensFacing facing) {
|
||||
if (facing == CameraX.LensFacing.FRONT) {
|
||||
return Camera.CameraInfo.CAMERA_FACING_FRONT;
|
||||
} else {
|
||||
return Camera.CameraInfo.CAMERA_FACING_BACK;
|
||||
}
|
||||
}
|
||||
|
||||
public static @NonNull CameraX.LensFacing toLensFacing(int cameraDirectionInt) {
|
||||
if (cameraDirectionInt == Camera.CameraInfo.CAMERA_FACING_FRONT) {
|
||||
return CameraX.LensFacing.FRONT;
|
||||
} else {
|
||||
return CameraX.LensFacing.BACK;
|
||||
}
|
||||
}
|
||||
|
||||
public static @NonNull ImageCapture.CaptureMode getOptimalCaptureMode() {
|
||||
return FastCameraModels.contains(Build.MODEL) ? ImageCapture.CaptureMode.MAX_QUALITY
|
||||
: ImageCapture.CaptureMode.MIN_LATENCY;
|
||||
}
|
||||
|
||||
private static byte[] transformByteArray(@NonNull byte[] data, @Nullable Rect cropRect, int rotation, boolean flip) throws IOException {
|
||||
Stopwatch stopwatch = new Stopwatch("transform");
|
||||
Bitmap in;
|
||||
|
||||
if (cropRect != null) {
|
||||
BitmapRegionDecoder decoder = BitmapRegionDecoder.newInstance(data, 0, data.length, false);
|
||||
in = decoder.decodeRegion(cropRect, new BitmapFactory.Options());
|
||||
decoder.recycle();
|
||||
stopwatch.split("crop");
|
||||
} else {
|
||||
in = BitmapFactory.decodeByteArray(data, 0, data.length);
|
||||
}
|
||||
|
||||
Bitmap out = in;
|
||||
|
||||
if (rotation != 0 || flip) {
|
||||
Matrix matrix = new Matrix();
|
||||
matrix.postRotate(rotation);
|
||||
|
||||
if (flip) {
|
||||
matrix.postScale(-1, 1);
|
||||
matrix.postTranslate(in.getWidth(), 0);
|
||||
}
|
||||
|
||||
out = Bitmap.createBitmap(in, 0, 0, in.getWidth(), in.getHeight(), matrix, true);
|
||||
}
|
||||
|
||||
byte[] transformedData = toJpegBytes(out);
|
||||
stopwatch.split("transcode");
|
||||
|
||||
in.recycle();
|
||||
out.recycle();
|
||||
|
||||
stopwatch.stop(TAG);
|
||||
|
||||
return transformedData;
|
||||
}
|
||||
|
||||
@RequiresApi(21)
|
||||
private static boolean shouldCropImage(@NonNull ImageProxy image) {
|
||||
Size sourceSize = new Size(image.getWidth(), image.getHeight());
|
||||
Size targetSize = new Size(image.getCropRect().width(), image.getCropRect().height());
|
||||
|
||||
return !targetSize.equals(sourceSize);
|
||||
}
|
||||
|
||||
private static byte[] toJpegBytes(@NonNull Bitmap bitmap) throws IOException {
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
|
||||
if (!bitmap.compress(Bitmap.CompressFormat.JPEG, 80, out)) {
|
||||
throw new IOException("Failed to compress bitmap.");
|
||||
}
|
||||
|
||||
return out.toByteArray();
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,30 @@
|
|||
package org.thoughtcrime.securesms.mediasend.camerax;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* A set of {@link android.os.Build#MODEL} that are known to both benefit from
|
||||
* {@link androidx.camera.core.ImageCapture.CaptureMode#MAX_QUALITY} and execute it quickly.
|
||||
*
|
||||
*/
|
||||
public class FastCameraModels {
|
||||
|
||||
private static final Set<String> MODELS = new HashSet<String>() {{
|
||||
add("Pixel 2");
|
||||
add("Pixel 2 XL");
|
||||
add("Pixel 3");
|
||||
add("Pixel 3 XL");
|
||||
add("Pixel 3a");
|
||||
add("Pixel 3a XL");
|
||||
}};
|
||||
|
||||
/**
|
||||
* @param model Should be a {@link android.os.Build#MODEL}.
|
||||
*/
|
||||
public static boolean contains(@NonNull String model) {
|
||||
return MODELS.contains(model);
|
||||
}
|
||||
}
|
|
@ -5,6 +5,7 @@ import android.app.AlarmManager;
|
|||
import android.app.NotificationManager;
|
||||
import android.app.job.JobScheduler;
|
||||
import android.content.Context;
|
||||
import android.hardware.display.DisplayManager;
|
||||
import android.media.AudioManager;
|
||||
import android.net.ConnectivityManager;
|
||||
import android.os.Build;
|
||||
|
@ -55,6 +56,10 @@ public class ServiceUtil {
|
|||
return (Vibrator)context.getSystemService(Context.VIBRATOR_SERVICE);
|
||||
}
|
||||
|
||||
public static DisplayManager getDisplayManager(@NonNull Context context) {
|
||||
return (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE);
|
||||
}
|
||||
|
||||
@RequiresApi(26)
|
||||
public static JobScheduler getJobScheduler(Context context) {
|
||||
return (JobScheduler) context.getSystemService(JobScheduler.class);
|
||||
|
|
Loading…
Reference in New Issue