mirror of
https://github.com/oxen-io/session-android.git
synced 2025-06-09 09:18:49 +00:00
Improve camera capture with CameraX.
This commit is contained in:
parent
4593014d00
commit
73b8f11b5a
@ -3,7 +3,7 @@
|
|||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
package="org.thoughtcrime.securesms">
|
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"
|
<permission android:name="org.thoughtcrime.securesms.ACCESS_SECRETS"
|
||||||
android:label="Access to TextSecure Secrets"
|
android:label="Access to TextSecure Secrets"
|
||||||
|
46
build.gradle
46
build.gradle
@ -54,7 +54,7 @@ repositories {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation 'androidx.appcompat:appcompat:1.0.2'
|
implementation 'androidx.appcompat:appcompat:1.1.0-beta01'
|
||||||
implementation 'androidx.recyclerview:recyclerview:1.0.0'
|
implementation 'androidx.recyclerview:recyclerview:1.0.0'
|
||||||
implementation 'com.google.android.material:material:1.0.0'
|
implementation 'com.google.android.material:material:1.0.0'
|
||||||
implementation 'androidx.legacy:legacy-support-v13: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.multidex:multidex:2.0.1'
|
||||||
implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0'
|
implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0'
|
||||||
implementation 'androidx.lifecycle:lifecycle-common-java8: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') {
|
implementation('com.google.firebase:firebase-messaging:17.3.4') {
|
||||||
exclude group: 'com.google.firebase', module: 'firebase-core'
|
exclude group: 'com.google.firebase', module: 'firebase-core'
|
||||||
@ -173,12 +175,14 @@ dependencyVerification {
|
|||||||
'com.tomergoldst.android:tooltips:4c56697dd1ad64b8066535c61f961a6d901e7ae5d97ae27084ba40ad620349b6',
|
'com.tomergoldst.android:tooltips:4c56697dd1ad64b8066535c61f961a6d901e7ae5d97ae27084ba40ad620349b6',
|
||||||
'com.takisoft.fix:colorpicker:f5d0dbabe406a1800498ca9c1faf34db36e021d8488bf10360f29961fe3ab0d1',
|
'com.takisoft.fix:colorpicker:f5d0dbabe406a1800498ca9c1faf34db36e021d8488bf10360f29961fe3ab0d1',
|
||||||
'com.codewaves.stickyheadergrid:stickyheadergrid:5b4aa6a52a957cfd55f60f4220c11c0c371385a3cb9786cae03c260dcdef5794',
|
'com.codewaves.stickyheadergrid:stickyheadergrid:5b4aa6a52a957cfd55f60f4220c11c0c371385a3cb9786cae03c260dcdef5794',
|
||||||
'androidx.appcompat:appcompat:a3080cdd5e5c56cb72f9d428b1657d4380011ec211cfedf76e084b95f6bf0d03',
|
'androidx.appcompat:appcompat:49ad229add44f822fcb3c8405c3fddbd72660da6a839ce29e13158f5980514fd',
|
||||||
'com.melnykov:floatingactionbutton:15d58d4fac0f7a288d0e5301bbaf501a146f5b3f5921277811bf99bd3b397263',
|
'com.melnykov:floatingactionbutton:15d58d4fac0f7a288d0e5301bbaf501a146f5b3f5921277811bf99bd3b397263',
|
||||||
'androidx.recyclerview:recyclerview:06956fb1ac014027ca9d2b40469a4b42aa61b4957bb11848e1ff352701ab4548',
|
'androidx.recyclerview:recyclerview:06956fb1ac014027ca9d2b40469a4b42aa61b4957bb11848e1ff352701ab4548',
|
||||||
'androidx.legacy:legacy-support-v13:65f5fcb57644d381d471a00fdf50f90b808be6b48a8ae57fb4ea39b7da8cca86',
|
'androidx.legacy:legacy-support-v13:65f5fcb57644d381d471a00fdf50f90b808be6b48a8ae57fb4ea39b7da8cca86',
|
||||||
'androidx.cardview:cardview:1193c04c22a3d6b5946dae9f4e8c59d6adde6a71b6bd5d87fb99d82dda1afec7',
|
'androidx.cardview:cardview:1193c04c22a3d6b5946dae9f4e8c59d6adde6a71b6bd5d87fb99d82dda1afec7',
|
||||||
'androidx.gridlayout:gridlayout:a7e5dc6f39dbc3dc6ac6d57b02a9c6fd792e80f0e45ddb3bb08e8f03d23c8755',
|
'androidx.gridlayout:gridlayout:a7e5dc6f39dbc3dc6ac6d57b02a9c6fd792e80f0e45ddb3bb08e8f03d23c8755',
|
||||||
|
'androidx.camera:camera-camera2:9dc33e45da983ebd29a888401ac700323ff573821eee3fa4d993dfa3d316ee2e',
|
||||||
|
'androidx.camera:camera-core:bf32bfcb5d103d865c6af1221a1d82e994c917b53c0bc080f1e9750bdc21cbb9',
|
||||||
'androidx.exifinterface:exifinterface:ee48be10aab8f54efff4c14b77d11e10b9eeee4379d5ef6bf297a2923c55cc11',
|
'androidx.exifinterface:exifinterface:ee48be10aab8f54efff4c14b77d11e10b9eeee4379d5ef6bf297a2923c55cc11',
|
||||||
'androidx.constraintlayout:constraintlayout:5ff864def9d41cd04e08348d69591143bae3ceff4284cf8608bceb98c36ac830',
|
'androidx.constraintlayout:constraintlayout:5ff864def9d41cd04e08348d69591143bae3ceff4284cf8608bceb98c36ac830',
|
||||||
'androidx.multidex:multidex:42dd32ff9f97f85771b82a20003a8d70f68ab7b4ba328964312ce0732693db09',
|
'androidx.multidex:multidex:42dd32ff9f97f85771b82a20003a8d70f68ab7b4ba328964312ce0732693db09',
|
||||||
@ -223,41 +227,47 @@ dependencyVerification {
|
|||||||
'com.google.android.gms:play-services-stats:5b2d8281adbfd6e74d2295c94bab9ea80fc9a84dfbb397995673f5af4d4c6368',
|
'com.google.android.gms:play-services-stats:5b2d8281adbfd6e74d2295c94bab9ea80fc9a84dfbb397995673f5af4d4c6368',
|
||||||
'com.google.android.gms:play-services-basement:e08bfd1e87c4e50ef76161d7ac76b873aeb975367eeb3afa4abe62ea1887c7c6',
|
'com.google.android.gms:play-services-basement:e08bfd1e87c4e50ef76161d7ac76b873aeb975367eeb3afa4abe62ea1887c7c6',
|
||||||
'androidx.legacy:legacy-support-v4:78fec1485f0f388a4749022dd51416857127cd2544ae1c3fd0b16589055480b0',
|
'androidx.legacy:legacy-support-v4:78fec1485f0f388a4749022dd51416857127cd2544ae1c3fd0b16589055480b0',
|
||||||
'androidx.fragment:fragment:65dd32d71fe65a32e77989a6cfb1ad09307038927f82a740c7611162d0b518f8',
|
'androidx.fragment:fragment:9656d81c472b5142bbc3471ef7259fbc93905dc38e823c63a99e48819881b6e7',
|
||||||
'androidx.vectordrawable:vectordrawable-animated:26c3a0cf0a9a9a7d235a0b00f2f37e431d52d9952751e3eb7c90b4b52c236cf1',
|
'androidx.appcompat:appcompat-resources:53c0a33d07c4bab48d4c8169bf30053aa14965af4a775b56092a9fc7079802b1',
|
||||||
'androidx.legacy:legacy-support-core-ui:0d1260c6e7e6a337f875df71b516931e703f716e90889817cd3a20fa5ac3d947',
|
'androidx.legacy:legacy-support-core-ui:0d1260c6e7e6a337f875df71b516931e703f716e90889817cd3a20fa5ac3d947',
|
||||||
|
'androidx.drawerlayout:drawerlayout:9402442cdc5a43cf62fb14f8cf98c63342d4d9d9b805c8033c6cf7e802749ac1',
|
||||||
'androidx.legacy:legacy-support-core-utils:a7edcf01d5b52b3034073027bc4775b78a4764bb6202bb91d61c829add8dd1c7',
|
'androidx.legacy:legacy-support-core-utils:a7edcf01d5b52b3034073027bc4775b78a4764bb6202bb91d61c829add8dd1c7',
|
||||||
'androidx.vectordrawable:vectordrawable:4ca358957b9510e52fc388e01c9d33c2d655d406bfe6e71984e9afea9f715ed2',
|
|
||||||
'androidx.transition:transition:a00a0f763f401abcecda9b0eafcb738929c5801b111a9a414b81a193d0f4008d',
|
'androidx.transition:transition:a00a0f763f401abcecda9b0eafcb738929c5801b111a9a414b81a193d0f4008d',
|
||||||
'androidx.media:media:b23b527b2bac870c4a7451e6982d7132e413e88d7f27dbeb1fc7640a720cd9ee',
|
'androidx.media:media:b23b527b2bac870c4a7451e6982d7132e413e88d7f27dbeb1fc7640a720cd9ee',
|
||||||
'androidx.loader:loader:11f735cb3b55c458d470bed9e25254375b518b4b1bad6926783a7026db0f5025',
|
|
||||||
'androidx.viewpager:viewpager:147af4e14a1984010d8f155e5e19d781f03c1d70dfed02a8e0d18428b8fc8682',
|
'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.coordinatorlayout:coordinatorlayout:e508c695489493374d942bf7b4ee02abf7571d25aac4c622e57d6cd5cd29eb73',
|
||||||
'androidx.drawerlayout:drawerlayout:9402442cdc5a43cf62fb14f8cf98c63342d4d9d9b805c8033c6cf7e802749ac1',
|
|
||||||
'androidx.slidingpanelayout:slidingpanelayout:76bffb7cefbf780794d8817002dad1562f3e27c0a9f746d62401c8edb30aeede',
|
'androidx.slidingpanelayout:slidingpanelayout:76bffb7cefbf780794d8817002dad1562f3e27c0a9f746d62401c8edb30aeede',
|
||||||
'androidx.customview:customview:20e5b8f6526a34595a604f56718da81167c0b40a7a94a57daa355663f2594df2',
|
'androidx.customview:customview:20e5b8f6526a34595a604f56718da81167c0b40a7a94a57daa355663f2594df2',
|
||||||
'androidx.swiperefreshlayout:swiperefreshlayout:9761b3a809c9b093fd06a3c4bbc645756dec0e95b5c9da419bc9f2a3f3026e8d',
|
'androidx.swiperefreshlayout:swiperefreshlayout:9761b3a809c9b093fd06a3c4bbc645756dec0e95b5c9da419bc9f2a3f3026e8d',
|
||||||
'androidx.asynclayoutinflater:asynclayoutinflater:f7eab60c57addd94bb06275832fe7600611beaaae1a1ec597c231956faf96c8b',
|
'androidx.asynclayoutinflater:asynclayoutinflater:f7eab60c57addd94bb06275832fe7600611beaaae1a1ec597c231956faf96c8b',
|
||||||
'androidx.core:core:b1a90522c22cad8c5fb7a4f912493dbcde463c6a37b4148dfb9423763460f998',
|
'androidx.core:core:45c7a50ad1f366e62db496d8cef7730d5ee1681215007d1a19e6b6d800a12842',
|
||||||
'androidx.versionedparcelable:versionedparcelable:f6438a93ed8016ccddca0e140a70be0b0110e0424edaa1472f84f98fed2e1ce3',
|
|
||||||
'androidx.collection:collection:9c8d117b5c2bc120a1cdfeb857e05b495b16c36013570372a708f7827e3ac9f9',
|
|
||||||
'androidx.cursoradapter:cursoradapter:a81c8fe78815fa47df5b749deb52727ad11f9397da58b16017f4eb2c11e28564',
|
'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:c82609ced8c498f0a701a30fb6771bb7480860daee84d82e0a81ee86edf7ba39',
|
||||||
'androidx.lifecycle:lifecycle-livedata-core:fde334ec7e22744c0f5bfe7caf1a84c9d717327044400577bdf9bd921ec4f7bc',
|
'androidx.lifecycle:lifecycle-livedata-core:fde334ec7e22744c0f5bfe7caf1a84c9d717327044400577bdf9bd921ec4f7bc',
|
||||||
'androidx.arch.core:core-runtime:87e65fc767c712b437649c7cee2431ebb4bed6daef82e501d4125b3ed3f65f8e',
|
'androidx.arch.core:core-runtime:87e65fc767c712b437649c7cee2431ebb4bed6daef82e501d4125b3ed3f65f8e',
|
||||||
'androidx.arch.core:core-common:4b80b337779b526e64b0ee0ca9e0df43b808344d145f8e9b1c42a134dac57ad8',
|
'androidx.concurrent:concurrent-listenablefuture-callback:14dce0acbffd705cfe9fb378960f851a9d8fc3f293d1157c310c9624a561d0a8',
|
||||||
'androidx.lifecycle:lifecycle-common:7bad7a188804adea6fa1f35d5ef99b705f20bd93ecadde484760ff86b535fefc',
|
'androidx.concurrent:concurrent-listenablefuture:f9ef396ca4a43b9685d28bec117b278aa9171de0e446e5138e931074e3462feb',
|
||||||
'androidx.lifecycle:lifecycle-viewmodel:d6460aea1b6bad80ab14cf88297e9e43bfde8d87c3e5c28f2c508233ffbcc062',
|
|
||||||
'com.github.bumptech.glide:gifdecoder:7ee9402ae1c48fac9232b67e81f881c217b907b3252e49ce57bdb97937ebb270',
|
'com.github.bumptech.glide:gifdecoder:7ee9402ae1c48fac9232b67e81f881c217b907b3252e49ce57bdb97937ebb270',
|
||||||
|
'androidx.versionedparcelable:versionedparcelable:948c751f6352d4c0f93f15fa1bf506c59083bc7754264dd9a325a6da0e2eec05',
|
||||||
|
'androidx.collection:collection:632a0e5407461de774409352940e292a291037724207a787820c77daf7d33b72',
|
||||||
|
'androidx.interpolator:interpolator:33193135a64fe21fa2c35eec6688f1a76e512606c0fc83dc1b689e37add7732a',
|
||||||
'androidx.documentfile:documentfile:865a061ef2fad16522f8433536b8d47208c46ff7c7745197dfa1eeb481869487',
|
'androidx.documentfile:documentfile:865a061ef2fad16522f8433536b8d47208c46ff7c7745197dfa1eeb481869487',
|
||||||
'androidx.localbroadcastmanager:localbroadcastmanager:e71c328ceef5c4a7d76f2d86df1b65d65fe2acf868b1a4efd84a3f34336186d8',
|
'androidx.localbroadcastmanager:localbroadcastmanager:e71c328ceef5c4a7d76f2d86df1b65d65fe2acf868b1a4efd84a3f34336186d8',
|
||||||
'androidx.print:print:1d5c7f3135a1bba661fc373fd72e11eb0a4adbb3396787826dd8e4190d5d9edd',
|
'androidx.print:print:1d5c7f3135a1bba661fc373fd72e11eb0a4adbb3396787826dd8e4190d5d9edd',
|
||||||
'androidx.interpolator:interpolator:33193135a64fe21fa2c35eec6688f1a76e512606c0fc83dc1b689e37add7732a',
|
'androidx.lifecycle:lifecycle-process:d8ff6fd844559743050c9ae010a6df230f2a3dbdf3e14498316f30bd8df836b5',
|
||||||
'androidx.annotation:annotation:0baae9755f7caf52aa80cd04324b91ba93af55d4d1d17dcc9a7b53d99ef7c016',
|
'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',
|
'androidx.constraintlayout:constraintlayout-solver:965c177e64fbd81bd1d27b402b66ef9d7bc7b5cb5f718044bf7a453abc542045',
|
||||||
|
'com.google.auto.value:auto-value-annotations:0e951fee8c31f60270bc46553a8586001b7b93dbb12aec06373aa99a150392c0',
|
||||||
'org.signal:signal-metadata-android:02323bc29317fa9d3b62fab0b507c94ba2e9bcc4a78d588888ffd313853757b3',
|
'org.signal:signal-metadata-android:02323bc29317fa9d3b62fab0b507c94ba2e9bcc4a78d588888ffd313853757b3',
|
||||||
'org.whispersystems:signal-service-java:6a1218cd6cebe6afbb613a00110a5c72708b3af5a7896d495ac4ed50ba58f07e',
|
'org.whispersystems:signal-service-java:6a1218cd6cebe6afbb613a00110a5c72708b3af5a7896d495ac4ed50ba58f07e',
|
||||||
'com.github.bumptech.glide:disklrucache:4696a81340eb6beee21ab93f703ed6e7ae49fb4ce3bc2fbc546e5bacd21b96b9',
|
'com.github.bumptech.glide:disklrucache:4696a81340eb6beee21ab93f703ed6e7ae49fb4ce3bc2fbc546e5bacd21b96b9',
|
||||||
|
17
res/layout/camerax_fragment.xml
Normal file
17
res/layout/camerax_fragment.xml
Normal file
@ -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;
|
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
|
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 {
|
private enum Stage {
|
||||||
SURFACE_AVAILABLE, CAMERA_PROPERTIES_AVAILABLE
|
SURFACE_AVAILABLE, CAMERA_PROPERTIES_AVAILABLE
|
||||||
}
|
}
|
||||||
|
23
src/org/thoughtcrime/securesms/mediasend/CameraFragment.java
Normal file
23
src/org/thoughtcrime/securesms/mediasend/CameraFragment.java
Normal file
@ -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();
|
||||||
|
}
|
||||||
|
}
|
199
src/org/thoughtcrime/securesms/mediasend/CameraXFragment.java
Normal file
199
src/org/thoughtcrime/securesms/mediasend/CameraXFragment.java
Normal file
@ -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;
|
package org.thoughtcrime.securesms.mediasend;
|
||||||
|
|
||||||
import android.Manifest;
|
import android.Manifest;
|
||||||
|
|
||||||
|
import androidx.fragment.app.FragmentTransaction;
|
||||||
import androidx.lifecycle.ViewModelProviders;
|
import androidx.lifecycle.ViewModelProviders;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
@ -50,7 +52,7 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
|
|||||||
MediaPickerItemFragment.Controller,
|
MediaPickerItemFragment.Controller,
|
||||||
MediaSendFragment.Controller,
|
MediaSendFragment.Controller,
|
||||||
ImageEditorFragment.Controller,
|
ImageEditorFragment.Controller,
|
||||||
Camera1Fragment.Controller
|
CameraFragment.Controller
|
||||||
{
|
{
|
||||||
private static final String TAG = MediaSendActivity.class.getSimpleName();
|
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);
|
boolean isCamera = getIntent().getBooleanExtra(KEY_IS_CAMERA, false);
|
||||||
|
|
||||||
if (isCamera) {
|
if (isCamera) {
|
||||||
Fragment fragment = Camera1Fragment.newInstance();
|
Fragment fragment = CameraFragment.newInstance();
|
||||||
getSupportFragmentManager().beginTransaction()
|
getSupportFragmentManager().beginTransaction()
|
||||||
.replace(R.id.mediasend_fragment_container, fragment, TAG_CAMERA)
|
.replace(R.id.mediasend_fragment_container, fragment, TAG_CAMERA)
|
||||||
.commit();
|
.commit();
|
||||||
@ -216,7 +218,7 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
|
|||||||
@Override
|
@Override
|
||||||
public void onMediaSelected(@NonNull Media media) {
|
public void onMediaSelected(@NonNull Media media) {
|
||||||
viewModel.onSingleMediaSelected(this, media);
|
viewModel.onSingleMediaSelected(this, media);
|
||||||
navigateToMediaSend(recipient, transport, dynamicLanguage.getCurrentLocale());
|
navigateToMediaSend(recipient, transport, dynamicLanguage.getCurrentLocale(), false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -305,7 +307,7 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
|
|||||||
Log.i(TAG, "Camera capture stored: " + media.getUri().toString());
|
Log.i(TAG, "Camera capture stored: " + media.getUri().toString());
|
||||||
|
|
||||||
viewModel.onImageCaptured(media);
|
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);
|
animateButtonVisibility(countButton, countButton.getVisibility(), buttonState.isVisible() ? View.VISIBLE : View.GONE);
|
||||||
|
|
||||||
if (buttonState.getCount() > 0) {
|
if (buttonState.getCount() > 0) {
|
||||||
countButton.setOnClickListener(v -> navigateToMediaSend(recipient, transport, locale));
|
countButton.setOnClickListener(v -> navigateToMediaSend(recipient, transport, locale, false));
|
||||||
if (buttonState.isVisible()) {
|
if (buttonState.isVisible()) {
|
||||||
animateButtonTextChange(countButton);
|
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);
|
MediaSendFragment fragment = MediaSendFragment.newInstance(recipient, transport, locale);
|
||||||
String backstackTag = null;
|
String backstackTag = null;
|
||||||
|
|
||||||
@ -365,9 +367,15 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
|
|||||||
backstackTag = TAG_SEND;
|
backstackTag = TAG_SEND;
|
||||||
}
|
}
|
||||||
|
|
||||||
getSupportFragmentManager().beginTransaction()
|
FragmentTransaction transaction = 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)
|
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)
|
.addToBackStack(backstackTag)
|
||||||
.commit();
|
.commit();
|
||||||
}
|
}
|
||||||
@ -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)
|
.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))
|
.withPermanentDenialDialog(getString(R.string.ConversationActivity_signal_needs_the_camera_permission_to_take_photos_or_video))
|
||||||
.onAllGranted(() -> {
|
.onAllGranted(() -> {
|
||||||
Camera1Fragment fragment = getOrCreateCameraFragment();
|
Fragment fragment = getOrCreateCameraFragment();
|
||||||
getSupportFragmentManager().beginTransaction()
|
getSupportFragmentManager().beginTransaction()
|
||||||
.setCustomAnimations(R.anim.slide_from_right, R.anim.slide_to_left, R.anim.slide_from_left, R.anim.slide_to_right)
|
.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)
|
.replace(R.id.mediasend_fragment_container, fragment, TAG_CAMERA)
|
||||||
@ -390,11 +398,11 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
|
|||||||
.execute();
|
.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
private Camera1Fragment getOrCreateCameraFragment() {
|
private Fragment getOrCreateCameraFragment() {
|
||||||
Camera1Fragment fragment = (Camera1Fragment) getSupportFragmentManager().findFragmentByTag(TAG_CAMERA);
|
Fragment fragment = getSupportFragmentManager().findFragmentByTag(TAG_CAMERA);
|
||||||
|
|
||||||
return fragment != null ? fragment
|
return fragment != null ? fragment
|
||||||
: Camera1Fragment.newInstance();
|
: CameraFragment.newInstance();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void animateButtonVisibility(@NonNull View button, int oldVisibility, int newVisibility) {
|
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();
|
||||||
|
}
|
||||||
|
}
|
1066
src/org/thoughtcrime/securesms/mediasend/camerax/CameraXView.java
Normal file
1066
src/org/thoughtcrime/securesms/mediasend/camerax/CameraXView.java
Normal file
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.NotificationManager;
|
||||||
import android.app.job.JobScheduler;
|
import android.app.job.JobScheduler;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.hardware.display.DisplayManager;
|
||||||
import android.media.AudioManager;
|
import android.media.AudioManager;
|
||||||
import android.net.ConnectivityManager;
|
import android.net.ConnectivityManager;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
@ -55,6 +56,10 @@ public class ServiceUtil {
|
|||||||
return (Vibrator)context.getSystemService(Context.VIBRATOR_SERVICE);
|
return (Vibrator)context.getSystemService(Context.VIBRATOR_SERVICE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static DisplayManager getDisplayManager(@NonNull Context context) {
|
||||||
|
return (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE);
|
||||||
|
}
|
||||||
|
|
||||||
@RequiresApi(26)
|
@RequiresApi(26)
|
||||||
public static JobScheduler getJobScheduler(Context context) {
|
public static JobScheduler getJobScheduler(Context context) {
|
||||||
return (JobScheduler) context.getSystemService(JobScheduler.class);
|
return (JobScheduler) context.getSystemService(JobScheduler.class);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user