Compare commits

...

58 Commits

Author SHA1 Message Date
solokot 967d4551c5
Update Russian translation
Signed-off-by: Hardcore Sushi <hardcore.sushi@disroot.org>
2024-02-12 16:51:55 +01:00
Ali Beyaz b747d2822a
Add Turkish translation
Signed-off-by: Hardcore Sushi <hardcore.sushi@disroot.org>
2024-02-12 16:47:30 +01:00
CyanWolf e5652666d8
Update Spanish
Signed-off-by: Hardcore Sushi <hardcore.sushi@disroot.org>
2024-02-11 18:12:17 +01:00
Muhmmad14333653 cda0e90b96
Update Arabic translations
Signed-off-by: Hardcore Sushi <hardcore.sushi@disroot.org>
2024-02-11 18:08:54 +01:00
Matéo Duparc 6f43bc7417
Avoid being killed by SELinux when retrieving volume path 2024-02-11 17:55:24 +01:00
Matéo Duparc c26ab661c2
Logcat activity 2024-01-30 18:29:49 +01:00
Matéo Duparc 1c15f9fac8
Allow choosing export method 2024-01-28 15:44:53 +01:00
Matéo Duparc b4635dc2e0
Directory loading indicator 2024-01-13 23:19:22 +01:00
Matéo Duparc f4e47c1827
Allow directory creation on exposed volumes 2024-01-13 21:41:58 +01:00
Matéo Duparc 5474d6eea5
Add .opus & Update build config 2024-01-13 21:25:31 +01:00
Matéo Duparc 719faa31ee
Fix README 2023-10-15 17:09:48 +02:00
Matéo Duparc a41cde1c53
DroidFS v2.1.3 2023-09-28 19:36:55 +02:00
Matéo Duparc b503f134d5
Fix Intent.getParcelableExtra() crash on Android 13 2023-09-24 19:04:49 +02:00
Matéo Duparc 3ba774fda3
Add Version.toString() 2023-09-19 13:47:59 +02:00
Matéo Duparc b2154d319e
Repair corrupted database due to v2.1.1 2023-09-19 13:39:35 +02:00
Matéo Duparc 571a79cc1d
Really fix database upgrade 2023-09-19 11:41:01 +02:00
Matéo Duparc 891a581329
Update dependencies 2023-09-17 20:10:15 +02:00
Matéo Duparc f1a9c1383c
Fix database upgrade 2023-09-17 19:11:52 +02:00
Matéo Duparc ac71ad887d
Fix README 2023-09-10 21:39:28 +02:00
Matéo Duparc e1fe329f49
Add v2.1.0 changelog 2023-09-10 21:11:04 +02:00
Matéo Duparc dfff597ae5
DroidFS v2.1.0 2023-09-10 21:01:39 +02:00
Matéo Duparc bd429648b3
Update documentation 2023-09-10 21:01:04 +02:00
Matéo Duparc 71ff37b170
Fixes 2023-09-10 19:17:51 +02:00
Matéo Duparc 4afe56b13c
Migrate to AndroidX Media3 2023-09-10 19:12:17 +02:00
Matéo Duparc 217334a959
Fix es & de translations 2023-09-09 16:15:03 +02:00
CyanWolf 2666313676
Update Spanish translation
Signed-off-by: Hardcore Sushi <hardcore.sushi@disroot.org>
2023-09-08 21:32:46 +02:00
solokot 04e154a6d9
Updated Russian translation
Signed-off-by: Hardcore Sushi <hardcore.sushi@disroot.org>
2023-09-08 21:30:28 +02:00
Torsten Pfützenreuter d3760e2194
Added German translation
Signed-off-by: Hardcore Sushi <hardcore.sushi@disroot.org>
2023-09-08 21:27:38 +02:00
Matéo Duparc d6c777875e
Fix VolumeProvider createDocument path 2023-09-08 21:22:20 +02:00
Matéo Duparc 8a18270b33
Update dependencies 2023-09-08 21:13:24 +02:00
Matéo Duparc 79db84f81d
Volume provider 2023-09-06 19:27:41 +02:00
Matéo Duparc 6d04349b2e
Prevent volume renaming when open 2023-09-06 19:27:04 +02:00
Matéo Duparc de0194a722
Always open volume after creation 2023-08-20 17:08:10 +02:00
Matéo Duparc 3127a15d9e
Fix ANR on recursive mapping 2023-08-20 16:42:40 +02:00
Matéo Duparc a08da2eacb
MemoryFileProvider 2023-08-20 14:56:46 +02:00
Matéo Duparc 1727170cb6
Limit the number of thumbnails loaded concurrently 2023-08-15 18:33:29 +02:00
Matéo Duparc 8776d2ee28
Add Support section in README 2023-08-15 18:06:39 +02:00
Matéo Duparc 5642e28b44
Fix TODO.md 2023-05-12 20:39:58 +02:00
Matéo Duparc 1b7e5904be
New screenshots 2023-05-12 20:26:45 +02:00
Matéo Duparc cb3fc3c70e
Re-ask only on wrong password 2023-05-11 21:58:55 +02:00
Matéo Duparc 393c458495
Offload file discovery for copy in coroutine 2023-05-11 21:24:29 +02:00
Matéo Duparc cdf98a7190
Handle cryfs inaccessible base dir 2023-05-11 00:02:05 +02:00
Matéo Duparc 2ae41f0f79
Improve file oprations coroutines 2023-05-10 23:41:29 +02:00
Muhmmad14333653 f85f9d1c44
Update arabic translation 2023-05-08 21:36:22 +02:00
Matéo Duparc 9fc981fee8
Fix rotation when rebinding camera use cases 2023-05-08 21:32:04 +02:00
Matéo Duparc ad19b9e645
Update dependencies 2023-05-08 20:58:54 +02:00
Matéo Duparc 87ffbc3cc1
Fix unsafe features doc link 2023-05-06 23:57:23 +02:00
Matéo Duparc b3a25e03e7
Improve video recording: fix freezes & ExoPlayer errors 2023-05-06 23:40:37 +02:00
Matéo Duparc 4c412be7dc
Best error messages when opening volumes 2023-05-03 14:14:40 +02:00
Matéo Duparc f4f3239bb1
Fix volume copying 2023-05-02 14:24:59 +02:00
Matéo Duparc 481558bd56
Add ecryptfs & shufflecake in TODO & Update README 2023-04-29 20:21:46 +02:00
Matéo Duparc 8d0a797469
v2.0.1 changelog 2023-04-26 17:12:38 +02:00
Matéo Duparc a4ce35c95d
WiperService 2023-04-26 16:40:05 +02:00
Matéo Duparc e51bd2ceba
TODO.md & Update dependencies 2023-04-26 16:02:07 +02:00
Matéo Duparc 2bbf003df5
Recover unregistered hidden volumes 2023-04-25 15:06:20 +02:00
Matéo Duparc e83cfc9794
Fix password encoding 2023-04-24 13:37:14 +02:00
ctntt 9d1bfd606f
Themed/monochrome icon support
Signed-off-by: Hardcore Sushi <hardcore.sushi@disroot.org>
2023-04-23 11:43:13 +02:00
Matéo Duparc 49ec2eaf49
Stop always opening files in write mode 2023-04-20 16:38:15 +02:00
125 changed files with 4430 additions and 1516 deletions

View File

@ -45,16 +45,16 @@ $ git clone --depth=1 https://git.ffmpeg.org/ffmpeg.git
If you want Gocryptfs support, you need to download OpenSSL:
```
$ cd ../libgocryptfs
$ wget https://www.openssl.org/source/openssl-1.1.1t.tar.gz
$ wget https://www.openssl.org/source/openssl-1.1.1w.tar.gz
```
Verify OpenSSL signature:
```
$ wget https://www.openssl.org/source/openssl-1.1.1t.tar.gz.asc
$ gpg --verify openssl-1.1.1t.tar.gz.asc openssl-1.1.1t.tar.gz
$ wget https://www.openssl.org/source/openssl-1.1.1w.tar.gz.asc
$ gpg --verify openssl-1.1.1w.tar.gz.asc openssl-1.1.1w.tar.gz
```
Continue **ONLY** if the signature is **VALID**.
```
$ tar -xzf openssl-1.1.1t.tar.gz
$ tar -xzf openssl-1.1.1w.tar.gz
```
If you want CryFS support, initialize libcryfs:
```
@ -62,6 +62,14 @@ $ cd app/libcryfs
$ git submodule update --depth=1 --init
```
To be able to open PDF files internally, [pdf.js](https://github.com/mozilla/pdf.js) must be downloaded:
```
$ mkdir libpdfviewer/app/pdfjs-dist && cd libpdfviewer/app/pdfjs-dist
$ wget https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-3.8.162.tgz
$ tar xf pdfjs-dist-3.8.162.tgz package/build/pdf.min.js package/build/pdf.worker.min.js
$ mv package/build . && rm pdfjs-dist-3.8.162.tgz
```
# Build
Retrieve your Android NDK installation path, usually something like `/home/\<user\>/Android/SDK/ndk/\<NDK version\>`. Then, make it available in your shell:
```
@ -76,7 +84,7 @@ $ ./build.sh ffmpeg
This step is only required if you want Gocryptfs support.
```
$ cd app/libgocryptfs
$ OPENSSL_PATH="./openssl-1.1.1t" ./build.sh
$ OPENSSL_PATH="./openssl-1.1.1w" ./build.sh
```
## Compile APKs
Gradle build libgocryptfs and libcryfs by default.

20
DONATE.txt Normal file
View File

@ -0,0 +1,20 @@
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA256
Here are the DroidFS donation addresses:
Monero (XMR):
86f82JEMd33WfapNZETukJW17eEa6RR4rW3wNEZ2CAZh228EYpDaar4DdDPUc4U3YT4CcFdW4c7462Uzx9Em2BB92Aj9fbT
Bitcoin (BTC):
bc1qeyvpy3tj4rr4my5f5wz9s8a4g4nh4l0kj4h6xy
-----BEGIN PGP SIGNATURE-----
iHUEARYIAB0WIQS2Tv6GzuHQVPCCFxGv44Q0SkXhOgUCZNuhaAAKCRCv44Q0SkXh
OqEUAP0d67oFlGp5IlBHwNI/p2KMHka3LzHdQTBQs40Jus3tVQEAsTZEy/sc6Nwp
C8mAXUTebijFgrlYYQkfVS0RBXHwggo=
=E6ia
-----END PGP SIGNATURE-----

View File

@ -2,14 +2,19 @@
An alternative way to use encrypted virtual filesystems on Android that uses its own internal file explorer instead of mounting volumes.
It currently supports [gocryptfs](https://github.com/rfjakob/gocryptfs) and [CryFS](https://github.com/cryfs/cryfs).
For mortals: An encrypted file manager for Android.
For mortals: Encrypted storage compatible with already existing softwares.
<p align="center">
<img src="https://forge.chapril.org/hardcoresushi/DroidFS/raw/branch/master/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png" height="500">
<img src="https://forge.chapril.org/hardcoresushi/DroidFS/raw/branch/master/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png" height="500">
<img src="https://forge.chapril.org/hardcoresushi/DroidFS/raw/branch/master/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png" height="500">
<img src="https://forge.chapril.org/hardcoresushi/DroidFS/raw/branch/master/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png" height="500">
<img src="https://forge.chapril.org/hardcoresushi/DroidFS/raw/branch/master/fastlane/metadata/android/en-US/images/phoneScreenshots/6.png" height="500">
</p>
# Support
The creator of DroidFS works as a freelance developer and privacy consultant. I am currently looking for new clients! If you are interested, take a look at the [website](https://arkensys.dedyn.io). Alternatively, you can directly support DroidFS by making a [donation](https://forge.chapril.org/hardcoresushi/DroidFS/src/branch/master/DONATE.txt).
Thank you so much ❤️.
# Disclaimer
DroidFS is provided "as is", without any warranty of any kind.
It shouldn't be considered as an absolute safe way to store files.
@ -23,6 +28,8 @@ Do not use this app with volumes containing sensitive data unless you know exact
- Unlocking volumes using fingerprint authentication
- Volume auto-locking when the app goes in background
_For upcoming features, see [TODO.md](https://forge.chapril.org/hardcoresushi/DroidFS/src/branch/master/TODO.md)._
# Unsafe features
Some available features are considered risky and are therefore disabled by default. It is strongly recommended that you read the following documentation if you wish to activate one of these options.
@ -32,23 +39,30 @@ Some available features are considered risky and are therefore disabled by defau
Note: apps with root access don't care about this flag: they can take screenshots or record the screen of any app without any permissions.
</li>
<li><h4>Allow opening files with other applications*:</h4>
Decrypt and open file using external apps. These apps could save and send the files thus opened.
</li>
<li><h4>Allow exporting files:</h4>
Decrypt and write file to disk (external storage). Any app with storage permissions could access exported files.
</li>
<li><h4>Allow sharing files via the android share menu*:</h4>
Decrypt and share file with other apps. These apps could save and send the files thus shared.
</li>
<li><h4>Keep volume open when the app goes in background:</h4>
Don't close the volume when you leave the app but keep running it in the background. Anyone going back to the activity could have access to the volume.
</li>
<li><h4>Allow saving password hash using fingerprint:</h4>
Generate an AES-256 GCM key in the Android Keystore (protected by fingerprint authentication), then use it to encrypt the volume password hash and store it to the DroidFS internal storage. This require Android v6.0+. If your device is not encrypted, extracting the encryption key with physical access may be possible.
</li>
<li><h4>Keep volume open when the app goes in background:</h4>
Don't close the volume when you leave the app but keep running it in the background. Anyone going back to the activity could have access to the volume.
</li>
<li><h4>Allow opening files with other applications*:</h4>
Decrypt and open file using external apps. These apps could save and send the files thus opened.
</li>
<li><h4>Expose open volumes*:</h4>
Allow open volumes to be browsed in the system file explorer (<a href="https://developer.android.com/guide/topics/providers/document-provider">DocumentProvider</a> API). Encrypted files can then be selected from other applications, potentially with permanent access. This feature requires <i>"Keep volume open when the app goes in background"</i> to be enabled.
</li>
<li><h4>Grant write access:</h4>
Files opened with another applications can be modified by them. This applies to both previous unsafe features.
</li>
</ul>
* Features requiring temporary writing of the plain file to disk (DroidFS internal storage). This file could be read by apps with root access or by physical access if your device is not encrypted.
\* These features can work in two ways: temporarily writing the plain file to disk (DroidFS internal storage) or sharing it via memory. By default, DroidFS will choose to keep the file only in memory as it's more secure, but will fallback to disk export if the file is too large to be held in memory. This behavior can be changed with the *"Export method"* parameter in the settings. Please note that some applications require the file to be stored on disk, and therefore do not work with memory-exported files.
# Download
<a href="https://f-droid.org/packages/sushi.hardcore.droidfs">
@ -92,7 +106,7 @@ DroidFS needs some permissions for certain features. However, you are free to de
</ul>
# Limitations
DroidFS works as a wrapper around modified versions of the original encrypted container implementations ([libgocryptfs](https://forge.chapril.org/hardcoresushi/libgocryptfs) and [libcryfs](https://forge.chapril.org/hardcoresushi/libcryfs)). These programs were designed to run on standard x86 Linux systems: they access the underlying file system with file paths and syscalls. However, on Android, you can't access files from other applications using file paths. Instead, one has to use the [ContentProvider](https://developer.android.com/guide/topics/providers/content-providers) API. Obviously, neither Gocryptfs nor CryFS support this API. As a result, DroidFS cannot open volumes provided by other applications (such as cloud storage clients), nor can it allow other applications to access encrypted volumes once opened.
DroidFS works as a wrapper around modified versions of the original encrypted container implementations ([libgocryptfs](https://forge.chapril.org/hardcoresushi/libgocryptfs) and [libcryfs](https://forge.chapril.org/hardcoresushi/libcryfs)). These programs were designed to run on standard x86 Linux systems: they access the underlying file system with file paths and syscalls. However, on Android, you can't access files from other applications using file paths. Instead, one has to use the [ContentProvider](https://developer.android.com/guide/topics/providers/content-providers) API. Obviously, neither Gocryptfs nor CryFS support this API. As a result, DroidFS cannot open volumes provided by other applications (such as cloud storage clients). If you want to synchronize your volumes on a cloud, the cloud application must synchronize the encrypted directory from disk.
Due to Android's storage restrictions, encrypted volumes located on SD cards must be placed under `/Android/data/sushi.hardcore.droidfs/` if you want DroidFS to be able to modify them.

31
TODO.md Normal file
View File

@ -0,0 +1,31 @@
# TODO
Here's a list of features that it would be nice to have in DroidFS. As this is a FLOSS project, there are no special requirements on *when* or even *if* these features will be implemented, but contributions are greatly appreciated.
## Security
- [hardened_malloc](https://github.com/GrapheneOS/hardened_malloc) compatibility ([#181](https://github.com/hardcore-sushi/DroidFS/issues/181))
- Internal keyboard for passwords
## UX
- File associations editor
- Optional discovery before file operations
- Modifiable CryFS scrypt parameters
- Alert dialog showing details of file operations
- Internal file browser to select volumes
## Encryption software support
- [Shufflecake](https://shufflecake.net): plausible deniability for multiple hidden filesystems on Linux (would be absolutely awesome to have but quite difficult)
- [fscrypt](https://www.kernel.org/doc/html/latest/filesystems/fscrypt.html): filesystem encryption at the kernel level
## Health
- F-Droid ABI split
- OpenSSL & FFmpeg as git submodules (useful for F-Droid)
- Remove all android:configChanges from AndroidManifest.xml
- More efficient thumbnails cache
- Guide for translators
- Usage & code documentation
- Automated tests
## And:
- All the [feature requests on the GitHub repo](https://github.com/hardcore-sushi/DroidFS/issues?q=is%3Aissue+is%3Aopen+label%3Aenhancement)
- All the [feature requests on the Gitea repo](https://forge.chapril.org/hardcoresushi/DroidFS/issues?q=&state=open&labels=748)

View File

@ -5,6 +5,9 @@ project(DroidFS)
option(GOCRYPTFS "build libgocryptfs" ON)
option(CRYFS "build libcryfs" ON)
add_library(memfile SHARED src/main/native/memfile.c)
target_link_libraries(memfile log)
if (GOCRYPTFS)
add_library(gocryptfs SHARED IMPORTED)
set_target_properties(
@ -72,4 +75,5 @@ target_link_libraries(
avformat
avcodec
avutil
log
)

View File

@ -20,21 +20,25 @@ if (hasProperty("nosplits")) {
}
android {
compileSdkVersion 33
buildToolsVersion "33.0.0"
ndkVersion "25.1.8937393"
compileSdk 34
ndkVersion "26.1.10909125"
namespace "sushi.hardcore.droidfs"
compileOptions {
targetCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_17
sourceCompatibility JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "17"
}
defaultConfig {
applicationId "sushi.hardcore.droidfs"
minSdkVersion 21
targetSdkVersion 32
versionCode 30
versionName "2.0.0"
versionCode 36
versionName "2.1.3"
ndk {
abiFilters "x86", "x86_64", "armeabi-v7a", "arm64-v8a"
@ -54,12 +58,13 @@ android {
splits {
abi {
enable true
reset() // fix unknown bug (https://ru.stackoverflow.com/questions/1557805/abis-armeabi-mips-mips64-riscv64-are-not-supported-for-platform)
universalApk true
}
}
}
applicationVariants.all { variant ->
applicationVariants.configureEach { variant ->
variant.resValue "string", "versionName", variant.versionName
buildConfigField "boolean", "CRYFS_DISABLED", "${project.ext.disableCryFS}"
buildConfigField "boolean", "GOCRYPTFS_DISABLED", "${project.ext.disableGocryptfs}"
@ -67,6 +72,7 @@ android {
buildFeatures {
viewBinding true
buildConfig true
}
buildTypes {
@ -98,34 +104,34 @@ android {
dependencies {
implementation project(":libpdfviewer:app")
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
implementation 'androidx.core:core-ktx:1.10.0'
implementation 'androidx.core:core-ktx:1.12.0'
implementation "androidx.appcompat:appcompat:1.6.1"
implementation "androidx.constraintlayout:constraintlayout:2.1.4"
def lifecycle_version = "2.6.1"
def lifecycle_version = "2.6.2"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-process:$lifecycle_version"
implementation "androidx.sqlite:sqlite-ktx:2.3.1"
implementation "androidx.preference:preference-ktx:1.2.0"
implementation "androidx.preference:preference-ktx:1.2.1"
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
implementation 'com.google.android.material:material:1.8.0'
implementation "com.github.bumptech.glide:glide:4.13.2"
implementation 'com.google.android.material:material:1.9.0'
implementation 'com.github.bumptech.glide:glide:4.16.0'
implementation "androidx.biometric:biometric-ktx:1.2.0-alpha05"
def exoplayer_version = "2.18.5"
implementation "com.google.android.exoplayer:exoplayer-core:$exoplayer_version"
implementation "com.google.android.exoplayer:exoplayer-ui:$exoplayer_version"
def media3_version = "1.1.1"
implementation "androidx.media3:media3-exoplayer:$media3_version"
implementation 'androidx.media3:media3-ui:1.1.1'
implementation "androidx.media3:media3-datasource:$media3_version"
implementation "androidx.concurrent:concurrent-futures:1.1.0"
def camerax_version = "1.3.0-alpha05"
def camerax_version = "1.3.0-rc02"
implementation "androidx.camera:camera-camera2:$camerax_version"
implementation "androidx.camera:camera-lifecycle:$camerax_version"
implementation "androidx.camera:camera-view:$camerax_version"
implementation "androidx.camera:camera-extensions:$camerax_version"
def autoValueVersion = "1.10.1"
def autoValueVersion = '1.10.4'
implementation "com.google.auto.value:auto-value-annotations:$autoValueVersion"
annotationProcessor "com.google.auto.value:auto-value:$autoValueVersion"
}

@ -1 +1 @@
Subproject commit f40c2bbdcde77d8128da181edb72bf0e7a2168b5
Subproject commit 6388eaf433a4196f10389921d5e346c90ee3d793

@ -1 +1 @@
Subproject commit 79f9a10e35847e46f8563941345355f15f2dba7c
Subproject commit 4f32853ae5ac70811b451cac60ed36fd5b93cbc8

View File

@ -1,24 +1,4 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
-keepattributes SourceFile,LineNumberTable
-keep class sushi.hardcore.droidfs.SettingsActivity$**
-keep class sushi.hardcore.droidfs.explorers.ExplorerElement
@ -28,4 +8,17 @@
-keepclassmembers class sushi.hardcore.droidfs.video_recording.FFmpegMuxer {
void writePacket(byte[]);
void seek(long);
}
# Required for Intent.getParcelableExtra() to work on Android 13
-keep class sushi.hardcore.droidfs.VolumeData {
public int describeContents();
}
-keep class sushi.hardcore.droidfs.VolumeData$* {
static public android.os.Parcelable$Creator CREATOR;
}
-keep class sushi.hardcore.droidfs.filesystems.EncryptedVolume {
public int describeContents();
}
-keep class sushi.hardcore.droidfs.filesystems.EncryptedVolume$* {
static public android.os.Parcelable$Creator CREATOR;
}

View File

@ -3,10 +3,6 @@
xmlns:tools="http://schemas.android.com/tools"
android:installLocation="auto">
<permission
android:name="${applicationId}.WRITE_TEMPORARY_STORAGE"
android:protectionLevel="signature" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" tools:ignore="ScopedStorage" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
@ -57,7 +53,9 @@
<activity android:name=".file_viewers.AudioPlayer" android:configChanges="screenSize|orientation" />
<activity android:name=".file_viewers.TextEditor" android:configChanges="screenSize|orientation" />
<activity android:name=".CameraActivity" android:screenOrientation="nosensor" />
<activity android:name=".LogcatActivity"/>
<service android:name=".WiperService" android:exported="false" android:stopWithTask="false"/>
<service android:name=".file_operations.FileOperationService" android:exported="false"/>
<receiver android:name=".file_operations.NotificationBroadcastReceiver" android:exported="false">
@ -67,10 +65,21 @@
</receiver>
<provider
android:name=".content_providers.RestrictedFileProvider"
android:name=".content_providers.TemporaryFileProvider"
android:authorities="${applicationId}.temporary_provider"
android:exported="true"/>
<provider
android:authorities="${applicationId}.volume_provider"
android:name=".content_providers.VolumeProvider"
android:exported="true"
android:writePermission="${applicationId}.WRITE_TEMPORARY_STORAGE" />
android:grantUriPermissions="true"
android:permission="android.permission.MANAGE_DOCUMENTS">
<intent-filter>
<action android:name="android.content.action.DOCUMENTS_PROVIDER" />
</intent-filter>
</provider>
</application>
</manifest>

View File

@ -58,8 +58,8 @@ public final class SucklessPendingRecording {
private final OutputOptions mOutputOptions;
private Consumer<VideoRecordEvent> mEventListener;
private Executor mListenerExecutor;
private boolean mAudioEnabled = false;
private boolean mIsPersistent = false;
SucklessPendingRecording(@NonNull Context context, @NonNull SucklessRecorder recorder,
@NonNull OutputOptions options) {
@ -104,6 +104,10 @@ public final class SucklessPendingRecording {
return mAudioEnabled;
}
boolean isPersistent() {
return mIsPersistent;
}
/**
* Enables audio to be recorded for this recording.
*
@ -139,6 +143,69 @@ public final class SucklessPendingRecording {
return this;
}
/**
* Configures the recording to be a persistent recording.
*
* <p>A persistent recording will only be stopped by explicitly calling
* {@link Recording#stop()} or {@link Recording#close()} and will ignore events that would
* normally cause recording to stop, such as lifecycle events or explicit unbinding of a
* {@link VideoCapture} use case that the recording's {@link Recorder} is attached to.
*
* <p>Even though lifecycle events or explicit unbinding use cases won't stop a persistent
* recording, it will still stop the camera from producing data, resulting in the in-progress
* persistent recording stopping getting data until the camera stream is activated again. For
* example, when the activity goes into background, the recording will keep waiting for new
* data to be recorded until the activity is back to foreground.
*
* <p>A {@link Recorder} instance is recommended to be associated with a single
* {@link VideoCapture} instance, especially when using persistent recording. Otherwise, there
* might be unexpected behavior. Any in-progress persistent recording created from the same
* {@link Recorder} should be stopped before starting a new recording, even if the
* {@link Recorder} is associated with a different {@link VideoCapture}.
*
* <p>To switch to a different camera stream while a recording is in progress, first create
* the recording as persistent recording, then rebind the {@link VideoCapture} it's
* associated with to a different camera. The implementation may be like:
* <pre>{@code
* // Prepare the Recorder and VideoCapture, then bind the VideoCapture to the back camera.
* Recorder recorder = Recorder.Builder().build();
* VideoCapture videoCapture = VideoCapture.withOutput(recorder);
* cameraProvider.bindToLifecycle(
* lifecycleOwner, CameraSelector.DEFAULT_BACK_CAMERA, videoCapture);
*
* // Prepare the persistent recording and start it.
* Recording recording = recorder
* .prepareRecording(context, outputOptions)
* .asPersistentRecording()
* .start(eventExecutor, eventListener);
*
* // Record from the back camera for a period of time.
*
* // Rebind the VideoCapture to the front camera.
* cameraProvider.unbindAll();
* cameraProvider.bindToLifecycle(
* lifecycleOwner, CameraSelector.DEFAULT_FRONT_CAMERA, videoCapture);
*
* // Record from the front camera for a period of time.
*
* // Stop the recording explicitly.
* recording.stop();
* }</pre>
*
* <p>The audio data will still be recorded after the {@link VideoCapture} is unbound.
* {@link Recording#pause() Pause} the recording first and {@link Recording#resume() resume} it
* later to stop recording audio while rebinding use cases.
*
* <p>If the recording is unable to receive data from the new camera, possibly because of
* incompatible surface combination, an exception will be thrown when binding to lifecycle.
*/
@ExperimentalPersistentRecording
@NonNull
public SucklessPendingRecording asPersistentRecording() {
mIsPersistent = true;
return this;
}
/**
* Starts the recording, making it an active recording.
*
@ -159,7 +226,13 @@ public final class SucklessPendingRecording {
*
* <p>If the returned {@link SucklessRecording} is garbage collected, the recording will be
* automatically stopped. A reference to the active recording must be maintained as long as
* the recording needs to be active.
* the recording needs to be active. If the recording is garbage collected, the
* {@link VideoRecordEvent.Finalize} event will contain error
* {@link VideoRecordEvent.Finalize#ERROR_RECORDING_GARBAGE_COLLECTED}.
*
* <p>The {@link Recording} will be stopped automatically if the {@link VideoCapture} its
* {@link Recorder} is attached to is unbound unless it's created
* {@link #asPersistentRecording() as a persistent recording}.
*
* @throws IllegalStateException if the associated Recorder currently has an unfinished
* active recording.

View File

@ -16,6 +16,7 @@
package androidx.camera.video;
import static androidx.camera.video.AudioStats.AUDIO_AMPLITUDE_NONE;
import static androidx.camera.video.VideoRecordEvent.Finalize.ERROR_DURATION_LIMIT_REACHED;
import static androidx.camera.video.VideoRecordEvent.Finalize.ERROR_ENCODING_FAILED;
import static androidx.camera.video.VideoRecordEvent.Finalize.ERROR_FILE_SIZE_LIMIT_REACHED;
@ -54,6 +55,8 @@ import androidx.annotation.RequiresPermission;
import androidx.annotation.RestrictTo;
import androidx.annotation.VisibleForTesting;
import androidx.camera.core.AspectRatio;
import androidx.camera.core.CameraInfo;
import androidx.camera.core.DynamicRange;
import androidx.camera.core.Logger;
import androidx.camera.core.SurfaceRequest;
import androidx.camera.core.impl.MutableStateObservable;
@ -76,7 +79,7 @@ import androidx.camera.video.internal.compat.Api26Impl;
import androidx.camera.video.internal.compat.quirk.DeactivateEncoderSurfaceBeforeStopEncoderQuirk;
import androidx.camera.video.internal.compat.quirk.DeviceQuirks;
import androidx.camera.video.internal.compat.quirk.EncoderNotUsePersistentInputSurfaceQuirk;
import androidx.camera.video.internal.config.MimeInfo;
import androidx.camera.video.internal.config.AudioMimeInfo;
import androidx.camera.video.internal.encoder.AudioEncoderConfig;
import androidx.camera.video.internal.encoder.BufferCopiedEncodedData;
import androidx.camera.video.internal.encoder.EncodeException;
@ -223,9 +226,9 @@ public final class SucklessRecorder implements VideoOutput {
*/
DISABLED,
/**
* The recording is being recorded with audio.
* Audio recording is enabled for the running recording.
*/
ACTIVE,
ENABLED,
/**
* The audio encoder encountered errors.
*/
@ -340,10 +343,14 @@ public final class SucklessRecorder implements VideoOutput {
////////////////////////////////////////////////////////////////////////////////////////////////
// Members only accessed on mSequentialExecutor //
////////////////////////////////////////////////////////////////////////////////////////////////
private RecordingRecord mInProgressRecording = null;
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
RecordingRecord mInProgressRecording = null;
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
boolean mInProgressRecordingStopping = false;
private SurfaceRequest.TransformationInfo mSurfaceTransformationInfo = null;
@Nullable
private SurfaceRequest.TransformationInfo mInProgressTransformationInfo = null;
@Nullable
private SurfaceRequest.TransformationInfo mSourceTransformationInfo = null;
private VideoValidatedEncoderProfilesProxy mResolvedEncoderProfiles = null;
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
final List<ListenableFuture<Void>> mEncodingFutures = new ArrayList<>();
@ -424,13 +431,15 @@ public final class SucklessRecorder implements VideoOutput {
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
ScheduledFuture<?> mSourceNonStreamingTimeout = null;
// The Recorder has to be reset first before being configured again.
private boolean mNeedsReset = false;
private boolean mNeedsResetBeforeNextStart = false;
@NonNull
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
VideoEncoderSession mVideoEncoderSession;
@Nullable
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
VideoEncoderSession mVideoEncoderSessionToRelease = null;
double mAudioAmplitude = 0;
private boolean mShouldSendResumeEvent = false;
//--------------------------------------------------------------------------------------------//
SucklessRecorder(@Nullable Executor executor, @NonNull MediaSpec mediaSpec,
@ -487,6 +496,13 @@ public final class SucklessRecorder implements VideoOutput {
mSequentialExecutor.execute(() -> onSourceStateChangedInternal(newState));
}
@RestrictTo(RestrictTo.Scope.LIBRARY)
@Override
@NonNull
public VideoCapabilities getMediaCapabilities(@NonNull CameraInfo cameraInfo) {
return getVideoCapabilities(cameraInfo);
}
@NonNull
public SucklessPendingRecording prepareRecording(@NonNull Context context, @NonNull MuxerOutputOptions outputOptions) {
return prepareRecordingInternal(context, outputOptions);
@ -756,7 +772,8 @@ public final class SucklessRecorder implements VideoOutput {
}
}
void stop(@NonNull SucklessRecording activeRecording) {
void stop(@NonNull SucklessRecording activeRecording, @VideoRecordError int error,
@Nullable Throwable errorCause) {
RecordingRecord pendingRecordingToFinalize = null;
synchronized (mLock) {
if (!isSameRecording(activeRecording, mPendingRecordingRecord) && !isSameRecording(
@ -801,7 +818,7 @@ public final class SucklessRecorder implements VideoOutput {
long explicitlyStopTimeUs = TimeUnit.NANOSECONDS.toMicros(System.nanoTime());
RecordingRecord finalActiveRecordingRecord = mActiveRecordingRecord;
mSequentialExecutor.execute(() -> stopInternal(finalActiveRecordingRecord,
explicitlyStopTimeUs, ERROR_NONE, null));
explicitlyStopTimeUs, error, errorCause));
break;
case ERROR:
// In an error state, the recording will already be finalized. Treat as a
@ -811,9 +828,31 @@ public final class SucklessRecorder implements VideoOutput {
}
if (pendingRecordingToFinalize != null) {
if (error == VideoRecordEvent.Finalize.ERROR_RECORDING_GARBAGE_COLLECTED) {
Logger.e(TAG, "Recording was stopped due to recording being garbage collected "
+ "before any valid data has been produced.");
}
finalizePendingRecording(pendingRecordingToFinalize, ERROR_NO_VALID_DATA,
new RuntimeException("Recording was stopped before any data could be "
+ "produced."));
+ "produced.", errorCause));
}
}
void mute(@NonNull SucklessRecording activeRecording, boolean muted) {
synchronized (mLock) {
if (!isSameRecording(activeRecording, mPendingRecordingRecord) && !isSameRecording(
activeRecording, mActiveRecordingRecord)) {
// If this Recording is no longer active, log and treat as a no-op.
// This is not technically an error since the recording can be finalized
// asynchronously.
Logger.d(TAG,
"mute() called on a recording that is no longer active: "
+ activeRecording.getOutputOptions());
return;
}
RecordingRecord finalRecordingRecord = isSameRecording(activeRecording,
mPendingRecordingRecord) ? mPendingRecordingRecord : mActiveRecordingRecord;
mSequentialExecutor.execute(() -> muteInternal(finalRecordingRecord, muted));
}
}
@ -825,7 +864,8 @@ public final class SucklessRecorder implements VideoOutput {
recordingToFinalize.getOutputOptions(),
RecordingStats.of(/*duration=*/0L,
/*bytes=*/0L,
AudioStats.of(AudioStats.AUDIO_STATE_DISABLED, mAudioErrorCause)),
AudioStats.of(AudioStats.AUDIO_STATE_DISABLED, mAudioErrorCause,
AUDIO_AMPLITUDE_NONE)),
OutputResults.of(Uri.EMPTY),
error,
cause));
@ -856,14 +896,15 @@ public final class SucklessRecorder implements VideoOutput {
// If we're inactive and have no active surface, we'll reset the encoder directly.
// Otherwise, we'll wait for the active surface's surface request listener to
// reset the encoder.
requestReset(ERROR_SOURCE_INACTIVE, null);
requestReset(ERROR_SOURCE_INACTIVE, null, false);
} else {
// The source becomes inactive, the incoming new surface request has to be cached
// and be serviced after the Recorder is reset when receiving the previous
// surface request complete callback.
mNeedsReset = true;
if (mInProgressRecording != null) {
// Stop any in progress recording with "source inactive" error
mNeedsResetBeforeNextStart = true;
if (mInProgressRecording != null && !mInProgressRecording.isPersistent()) {
// Stop the in progress recording with "source inactive" error if it's not a
// persistent recording.
onInProgressRecordingInternalError(mInProgressRecording, ERROR_SOURCE_INACTIVE,
null);
}
@ -887,7 +928,8 @@ public final class SucklessRecorder implements VideoOutput {
* the surface request complete callback first.
*/
@ExecutedBy("mSequentialExecutor")
void requestReset(@VideoRecordError int errorCode, @Nullable Throwable errorCause) {
void requestReset(@VideoRecordError int errorCode, @Nullable Throwable errorCause,
boolean videoOnly) {
boolean shouldReset = false;
boolean shouldStop = false;
synchronized (mLock) {
@ -909,14 +951,22 @@ public final class SucklessRecorder implements VideoOutput {
case PAUSED:
// Fall-through
case RECORDING:
Preconditions.checkState(mInProgressRecording != null, "In-progress recording"
+ " shouldn't be null when in state " + mState);
if (mActiveRecordingRecord != mInProgressRecording) {
throw new AssertionError("In-progress recording does not match the active"
+ " recording. Unable to reset encoder.");
}
// If there's an active recording, stop it first then release the resources
// at onRecordingFinalized().
shouldStop = true;
// Fall-through
// If there's an active persistent recording, reset the Recorder directly.
// Otherwise, stop the recording first then release the Recorder at
// onRecordingFinalized().
if (isPersistentRecordingInProgress()) {
shouldReset = true;
} else {
shouldStop = true;
setState(State.RESETTING);
}
break;
case STOPPING:
// Already stopping. Set state to RESETTING so resources will be released once
// onRecordingFinalized() runs.
@ -931,14 +981,17 @@ public final class SucklessRecorder implements VideoOutput {
// These calls must not be posted to the executor to ensure they are executed inline on
// the sequential executor and the state changes above are correctly handled.
if (shouldReset) {
reset();
if (videoOnly) {
resetVideo();
} else {
reset();
}
} else if (shouldStop) {
stopInternal(mInProgressRecording, Encoder.NO_TIMESTAMP, errorCode, errorCause);
}
}
@ExecutedBy("mSequentialExecutor")
private void configureInternal(@NonNull SurfaceRequest surfaceRequest,
@NonNull Timebase videoSourceTimebase) {
if (surfaceRequest.isServiced()) {
@ -946,16 +999,19 @@ public final class SucklessRecorder implements VideoOutput {
return;
}
surfaceRequest.setTransformationInfoListener(mSequentialExecutor,
(transformationInfo) -> mSurfaceTransformationInfo = transformationInfo);
(transformationInfo) -> mSourceTransformationInfo = transformationInfo);
Size surfaceSize = surfaceRequest.getResolution();
// Fetch and cache nearest encoder profiles, if one exists.
VideoCapabilities capabilities =
VideoCapabilities.from(surfaceRequest.getCamera().getCameraInfo());
Quality highestSupportedQuality = capabilities.findHighestSupportedQualityFor(surfaceSize);
DynamicRange dynamicRange = surfaceRequest.getDynamicRange();
VideoCapabilities capabilities = getVideoCapabilities(
surfaceRequest.getCamera().getCameraInfo());
Quality highestSupportedQuality = capabilities.findHighestSupportedQualityFor(surfaceSize,
dynamicRange);
Logger.d(TAG, "Using supported quality of " + highestSupportedQuality
+ " for surface size " + surfaceSize);
if (highestSupportedQuality != Quality.NONE) {
mResolvedEncoderProfiles = capabilities.getProfiles(highestSupportedQuality);
mResolvedEncoderProfiles = capabilities.getProfiles(highestSupportedQuality,
dynamicRange);
if (mResolvedEncoderProfiles == null) {
throw new AssertionError("Camera advertised available quality but did not "
+ "produce EncoderProfiles for advertised quality.");
@ -968,9 +1024,14 @@ public final class SucklessRecorder implements VideoOutput {
@ExecutedBy("mSequentialExecutor")
private void setupVideo(@NonNull SurfaceRequest request, @NonNull Timebase timebase) {
safeToCloseVideoEncoder().addListener(() -> {
if (request.isServiced() || mVideoEncoderSession.isConfiguredSurfaceRequest(request)) {
if (request.isServiced()
|| (mVideoEncoderSession.isConfiguredSurfaceRequest(request)
&& !isPersistentRecordingInProgress())) {
// Ignore the surface request if it's already serviced. Or the video encoder
// session is already configured, unless there's a persistent recording is running.
Logger.w(TAG, "Ignore the SurfaceRequest " + request + " isServiced: "
+ request.isServiced() + " VideoEncoderSession: " + mVideoEncoderSession);
+ request.isServiced() + " VideoEncoderSession: " + mVideoEncoderSession
+ " has been configured with a persistent in-progress recording.");
return;
}
VideoEncoderSession videoEncoderSession =
@ -1002,6 +1063,12 @@ public final class SucklessRecorder implements VideoOutput {
}, mSequentialExecutor);
}
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
@ExecutedBy("mSequentialExecutor")
boolean isPersistentRecordingInProgress() {
return mInProgressRecording != null && mInProgressRecording.isPersistent();
}
@NonNull
@ExecutedBy("mSequentialExecutor")
private ListenableFuture<Void> safeToCloseVideoEncoder() {
@ -1037,7 +1104,9 @@ public final class SucklessRecorder implements VideoOutput {
mVideoEncoderSessionToRelease = videoEncoderSession;
setLatestSurface(null);
requestReset(ERROR_SOURCE_INACTIVE, null);
// Only reset video if the in-progress recording is persistent.
requestReset(ERROR_SOURCE_INACTIVE, null,
isPersistentRecordingInProgress());
}
@Override
@ -1052,17 +1121,14 @@ public final class SucklessRecorder implements VideoOutput {
void onConfigured() {
RecordingRecord recordingToStart = null;
RecordingRecord pendingRecordingToFinalize = null;
boolean continuePersistentRecording = false;
@VideoRecordError int error = ERROR_NONE;
Throwable errorCause = null;
boolean startRecordingPaused = false;
boolean recordingPaused = false;
synchronized (mLock) {
switch (mState) {
case IDLING:
// Fall-through
case RECORDING:
// Fall-through
case PAUSED:
// Fall-through
case RESETTING:
throw new AssertionError(
"Incorrectly invoke onConfigured() in state " + mState);
@ -1072,6 +1138,15 @@ public final class SucklessRecorder implements VideoOutput {
+ "STOPPING state when it's not waiting for a new surface.");
}
break;
case PAUSED:
recordingPaused = true;
// Fall-through
case RECORDING:
Preconditions.checkState(isPersistentRecordingInProgress(),
"Unexpectedly invoke onConfigured() when there's a non-persistent "
+ "in-progress recording");
continuePersistentRecording = true;
break;
case CONFIGURING:
setState(State.IDLING);
break;
@ -1080,7 +1155,7 @@ public final class SucklessRecorder implements VideoOutput {
"onConfigured() was invoked when the Recorder had encountered error");
break;
case PENDING_PAUSED:
startRecordingPaused = true;
recordingPaused = true;
// Fall through
case PENDING_RECORDING:
if (mActiveRecordingRecord != null) {
@ -1101,9 +1176,21 @@ public final class SucklessRecorder implements VideoOutput {
}
}
if (recordingToStart != null) {
if (continuePersistentRecording) {
updateEncoderCallbacks(mInProgressRecording, true);
mVideoEncoder.start();
if (mShouldSendResumeEvent) {
mInProgressRecording.updateVideoRecordEvent(VideoRecordEvent.resume(
mInProgressRecording.getOutputOptions(),
getInProgressRecordingStats()));
mShouldSendResumeEvent = false;
}
if (recordingPaused) {
mVideoEncoder.pause();
}
} else if (recordingToStart != null) {
// Start new active recording inline on sequential executor (but unlocked).
startRecording(recordingToStart, startRecordingPaused);
startRecording(recordingToStart, recordingPaused);
} else if (pendingRecordingToFinalize != null) {
finalizePendingRecording(pendingRecordingToFinalize, error, errorCause);
}
@ -1144,7 +1231,7 @@ public final class SucklessRecorder implements VideoOutput {
throws AudioSourceAccessException, InvalidConfigException {
MediaSpec mediaSpec = getObservableData(mMediaSpec);
// Resolve the audio mime info
MimeInfo audioMimeInfo = resolveAudioMimeInfo(mediaSpec, mResolvedEncoderProfiles);
AudioMimeInfo audioMimeInfo = resolveAudioMimeInfo(mediaSpec, mResolvedEncoderProfiles);
Timebase audioSourceTimebase = Timebase.UPTIME;
// Select and create the audio source
@ -1295,8 +1382,10 @@ public final class SucklessRecorder implements VideoOutput {
return;
}
if (mSurfaceTransformationInfo != null) {
mediaMuxer.setOrientationHint(mSurfaceTransformationInfo.getRotationDegrees());
SurfaceRequest.TransformationInfo transformationInfo = mSourceTransformationInfo;
if (transformationInfo != null) {
setInProgressTransformationInfo(transformationInfo);
mediaMuxer.setOrientationHint(transformationInfo.getRotationDegrees());
}
mVideoTrackIndex = mediaMuxer.addTrack(mVideoOutputConfig.getMediaFormat());
@ -1368,13 +1457,13 @@ public final class SucklessRecorder implements VideoOutput {
// Fall-through
case ERROR_SOURCE:
// Fall-through
case ACTIVE:
case ENABLED:
// Fall-through
case DISABLED:
throw new AssertionError(
"Incorrectly invoke startInternal in audio state " + mAudioState);
case IDLING:
setAudioState(recordingToStart.hasAudioEnabled() ? AudioState.ACTIVE
setAudioState(recordingToStart.hasAudioEnabled() ? AudioState.ENABLED
: AudioState.DISABLED);
break;
case INITIALIZING:
@ -1384,8 +1473,10 @@ public final class SucklessRecorder implements VideoOutput {
"The Recorder doesn't support recording with audio");
}
try {
setupAudio(recordingToStart);
setAudioState(AudioState.ACTIVE);
if (!mInProgressRecording.isPersistent() || mAudioEncoder == null) {
setupAudio(recordingToStart);
}
setAudioState(AudioState.ENABLED);
} catch (AudioSourceAccessException | InvalidConfigException e) {
Logger.e(TAG, "Unable to create audio resource with error: ", e);
AudioState audioState;
@ -1401,9 +1492,9 @@ public final class SucklessRecorder implements VideoOutput {
break;
}
initEncoderAndAudioSourceCallbacks(recordingToStart);
updateEncoderCallbacks(recordingToStart, false);
if (isAudioEnabled()) {
mAudioSource.start();
mAudioSource.start(recordingToStart.isMuted());
mAudioEncoder.start();
}
mVideoEncoder.start();
@ -1414,7 +1505,17 @@ public final class SucklessRecorder implements VideoOutput {
}
@ExecutedBy("mSequentialExecutor")
private void initEncoderAndAudioSourceCallbacks(@NonNull RecordingRecord recordingToStart) {
private void updateEncoderCallbacks(@NonNull RecordingRecord recordingToStart,
boolean videoOnly) {
// If there are uncompleted futures, cancel them first.
if (!mEncodingFutures.isEmpty()) {
ListenableFuture<List<Void>> listFuture = Futures.allAsList(mEncodingFutures);
if (!listFuture.isDone()) {
listFuture.cancel(true);
}
mEncodingFutures.clear();
}
mEncodingFutures.add(CallbackToFutureAdapter.getFuture(
completer -> {
mVideoEncoder.setEncoderCallback(new EncoderCallback() {
@ -1510,7 +1611,7 @@ public final class SucklessRecorder implements VideoOutput {
return "videoEncodingFuture";
}));
if (isAudioEnabled()) {
if (isAudioEnabled() && !videoOnly) {
mEncodingFutures.add(CallbackToFutureAdapter.getFuture(
completer -> {
Consumer<Throwable> audioErrorConsumer = throwable -> {
@ -1535,8 +1636,6 @@ public final class SucklessRecorder implements VideoOutput {
public void onSilenceStateChanged(boolean silenced) {
if (mIsAudioSourceSilenced != silenced) {
mIsAudioSourceSilenced = silenced;
mAudioErrorCause = silenced ? new IllegalStateException(
"The audio source has been silenced.") : null;
updateInProgressStatusEvent();
} else {
Logger.w(TAG, "Audio source silenced transitions"
@ -1552,6 +1651,11 @@ public final class SucklessRecorder implements VideoOutput {
audioErrorConsumer.accept(throwable);
}
}
@Override
public void onAmplitudeValue(double maxAmplitude) {
mAudioAmplitude = maxAmplitude;
}
});
mAudioEncoder.setEncoderCallback(new EncoderCallback() {
@ -1579,9 +1683,9 @@ public final class SucklessRecorder implements VideoOutput {
@Override
public void onEncodedData(@NonNull EncodedData encodedData) {
if (mAudioState == AudioState.DISABLED) {
throw new AssertionError(
"Audio is not enabled but audio encoded data is "
+ "produced.");
encodedData.close();
throw new AssertionError("Audio is not enabled but audio "
+ "encoded data is being produced.");
}
// If the media muxer doesn't yet exist, we may need to create and
@ -1638,12 +1742,16 @@ public final class SucklessRecorder implements VideoOutput {
@Override