Compare commits
91 Commits
v2.0.0-alp
...
master
Author | SHA1 | Date |
---|---|---|
|
3ba774fda3 | |
|
b2154d319e | |
|
571a79cc1d | |
|
891a581329 | |
|
f1a9c1383c | |
|
ac71ad887d | |
|
e1fe329f49 | |
|
dfff597ae5 | |
|
bd429648b3 | |
|
71ff37b170 | |
|
4afe56b13c | |
|
217334a959 | |
![]() |
2666313676 | |
![]() |
04e154a6d9 | |
![]() |
d3760e2194 | |
|
d6c777875e | |
|
8a18270b33 | |
|
79db84f81d | |
|
6d04349b2e | |
|
de0194a722 | |
|
3127a15d9e | |
|
a08da2eacb | |
|
1727170cb6 | |
|
8776d2ee28 | |
|
5642e28b44 | |
|
1b7e5904be | |
|
cb3fc3c70e | |
|
393c458495 | |
|
cdf98a7190 | |
|
2ae41f0f79 | |
![]() |
f85f9d1c44 | |
|
9fc981fee8 | |
|
ad19b9e645 | |
|
87ffbc3cc1 | |
|
b3a25e03e7 | |
|
4c412be7dc | |
|
f4f3239bb1 | |
|
481558bd56 | |
|
8d0a797469 | |
|
a4ce35c95d | |
|
e51bd2ceba | |
|
2bbf003df5 | |
|
e83cfc9794 | |
![]() |
9d1bfd606f | |
|
49ec2eaf49 | |
|
8c9c6a20b9 | |
|
f6d1fc8b67 | |
![]() |
de3a1a9538 | |
|
0a089c46ca | |
|
05f4610407 | |
![]() |
451f36c770 | |
|
df3f84f526 | |
|
24215a8b31 | |
|
eb4e13af46 | |
|
aea17aa7cb | |
|
e918a2f94c | |
|
e6761d1798 | |
|
c434d79c06 | |
|
821c853a22 | |
|
22b1522192 | |
|
5090a7aa03 | |
|
1a1d3ea570 | |
|
2d165c4a20 | |
|
883874a5ab | |
|
6e500c23e5 | |
|
a726f7a7d0 | |
|
1e75e9a32f | |
|
5e9656970a | |
|
5dbef99949 | |
|
d2f11c85d1 | |
|
25dbcca854 | |
![]() |
545275dabc | |
![]() |
077f5cc856 | |
![]() |
2e07ee5333 | |
|
34aad2596d | |
|
cdc269f2f7 | |
|
991e435e5e | |
|
7c2f87109a | |
|
4df1086734 | |
|
7cdfc32c31 | |
|
8f5afca823 | |
|
11cc15536f | |
|
2d19895e6d | |
|
e2539a53b9 | |
|
17c32f2144 | |
|
a5b6de1138 | |
|
d1ca164934 | |
|
1a21a43f05 | |
|
4d164944c1 | |
|
8709abd7d7 | |
|
e01932acda |
20
BUILD.md
20
BUILD.md
|
@ -24,7 +24,7 @@ Email: `Hardcore Sushi <hardcore.sushi@disroot.org>`
|
|||
# Download sources
|
||||
Download DroidFS source code:
|
||||
```
|
||||
$ git clone --depth=1 https://github.com/hardcore-sushi/DroidFS.git
|
||||
$ git clone --depth=1 https://forge.chapril.org/hardcoresushi/DroidFS.git
|
||||
```
|
||||
Verify sources:
|
||||
```
|
||||
|
@ -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.1p.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.1p.tar.gz.asc
|
||||
$ gpg --verify openssl-1.1.1p.tar.gz.asc openssl-1.1.1p.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.1p.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.1p" ./build.sh
|
||||
$ OPENSSL_PATH="./openssl-1.1.1w" ./build.sh
|
||||
```
|
||||
## Compile APKs
|
||||
Gradle build libgocryptfs and libcryfs by default.
|
||||
|
|
|
@ -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-----
|
60
README.md
60
README.md
|
@ -1,45 +1,67 @@
|
|||
# DroidFS
|
||||
DroidFS is an alternative way to use encrypted overlay filesystems on Android that uses its own internal file explorer instead of mounting virtual volumes.
|
||||
It currently supports [gocryptfs](https://github.com/rfjakob/gocryptfs) and [CryFS](https://github.com/cryfs/cryfs) (alpha).
|
||||
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: 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.fr.to). 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.
|
||||
DroidFS cannot protect you from screen recording apps, keyloggers, apk backdooring, compromised root accesses, memory dumps etc.
|
||||
Do not use this app with volumes containing sensitive data unless you know exactly what you are doing.
|
||||
|
||||
# Features
|
||||
- Compatible with original encrypted volume implementations
|
||||
- Internal support for video, audio, images, text and PDF files
|
||||
- Built-in camera to take on-the-fly encrypted photos and videos
|
||||
- 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
|
||||
DroidFS allows you to enable/disable unsafe features to fit your needs between security and comfort.
|
||||
It is strongly recommended to read the documentation of a feature before enabling it.
|
||||
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.
|
||||
|
||||
<ul>
|
||||
<li><h4>Allow screenshots:</h4>
|
||||
Disable the secure flag of DroidFS activities. This will allow you to take screenshots from the app, but will also allow other apps to record the screen while using DroidFS.
|
||||
|
||||
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>
|
||||
<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.
|
||||
</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 may require temporarily writing the plain file to disk (DroidFS internal storage). This file can be read by applications with root access or by physical access if your device is not encrypted. For files small enough and on a 3.17+ kernel, DroidFS will try to use memory-only storage using `memfd_create(2)` (can break some apps).
|
||||
|
||||
# Download
|
||||
<a href="https://f-droid.org/packages/sushi.hardcore.droidfs">
|
||||
|
@ -65,17 +87,17 @@ __Don't install the APK if the checksums don't match!__
|
|||
F-Droid APKs should be signed with the F-Droid key. More details [here](https://f-droid.org/docs/Release_Channels_and_Signing_Keys).
|
||||
|
||||
# Permissions
|
||||
DroidFS need some permissions to work properly. Here is why:
|
||||
DroidFS needs some permissions for certain features. However, you are free to deny them if you do not wish to use these features.
|
||||
|
||||
<ul>
|
||||
<li><h4>Read & write access to shared storage:</h4>
|
||||
Required for creating, opening and modifying volumes and for importing/exporting files to/from volumes.
|
||||
Required to access volumes located on shared storage.
|
||||
</li>
|
||||
<li><h4>Biometric/Fingerprint hardware:</h4>
|
||||
Required to encrypt/decrypt password hashes using a fingerprint protected key.
|
||||
</li>
|
||||
<li><h4>Camera:</h4>
|
||||
Needed to take photos & videos directly encrypted inside DroidFS. You can deny this permission if you don't want to use it.
|
||||
Required to take encrypted photos or videos directly from the app.
|
||||
</li>
|
||||
<li><h4>Record audio:</h4>
|
||||
Required if you want sound on video recorded with DroidFS.
|
||||
|
@ -83,7 +105,7 @@ DroidFS need some permissions to work properly. Here is why:
|
|||
</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.
|
||||
|
||||
|
@ -102,5 +124,5 @@ Thanks to these open source projects that DroidFS uses:
|
|||
### Borrowed code:
|
||||
- [MaterialFiles](https://github.com/zhanghai/MaterialFiles) for Kotlin natural sorting implementation
|
||||
### Libraries:
|
||||
- [Glide](https://github.com/bumptech/glide/) to display pictures
|
||||
- [Glide](https://github.com/bumptech/glide) to display pictures
|
||||
- [ExoPlayer](https://github.com/google/ExoPlayer) to play media files
|
||||
|
|
|
@ -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)
|
|
@ -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
|
||||
)
|
|
@ -13,22 +13,32 @@ if (hasProperty("disableGocryptfs")) {
|
|||
ext.disableGocryptfs = false
|
||||
}
|
||||
|
||||
if (hasProperty("nosplits")) {
|
||||
ext.splits = false
|
||||
} else {
|
||||
ext.splits = true
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion 31
|
||||
buildToolsVersion "31"
|
||||
ndkVersion "23.1.7779620"
|
||||
compileSdk 34
|
||||
ndkVersion "25.2.9519653"
|
||||
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
|
||||
//noinspection ExpiredTargetSdkVersion
|
||||
targetSdkVersion 29
|
||||
versionCode 28
|
||||
versionName "2.0.0-alpha1"
|
||||
targetSdkVersion 32
|
||||
versionCode 35
|
||||
versionName "2.1.2"
|
||||
|
||||
ndk {
|
||||
abiFilters "x86", "x86_64", "armeabi-v7a", "arm64-v8a"
|
||||
|
@ -44,7 +54,7 @@ android {
|
|||
}
|
||||
}
|
||||
|
||||
if (!file("fdroid").exists()) {
|
||||
if (project.ext.splits) {
|
||||
splits {
|
||||
abi {
|
||||
enable true
|
||||
|
@ -53,7 +63,7 @@ android {
|
|||
}
|
||||
}
|
||||
|
||||
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}"
|
||||
|
@ -61,6 +71,7 @@ android {
|
|||
|
||||
buildFeatures {
|
||||
viewBinding true
|
||||
buildConfig true
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
|
@ -80,32 +91,46 @@ android {
|
|||
path file('CMakeLists.txt')
|
||||
}
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main {
|
||||
java {
|
||||
exclude 'androidx/camera/video/originals/**'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation project(":libpdfviewer:app")
|
||||
implementation fileTree(dir: "libs", include: ["*.jar"])
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
||||
implementation 'androidx.core:core-ktx:1.7.0'
|
||||
implementation "androidx.appcompat:appcompat:1.4.1"
|
||||
implementation "androidx.constraintlayout:constraintlayout:2.1.3"
|
||||
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.2"
|
||||
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
|
||||
implementation "androidx.lifecycle:lifecycle-process:$lifecycle_version"
|
||||
|
||||
implementation "androidx.sqlite:sqlite-ktx:2.2.0"
|
||||
implementation "androidx.preference:preference-ktx:1.2.0"
|
||||
implementation "androidx.sqlite:sqlite-ktx:2.3.1"
|
||||
implementation "androidx.preference:preference-ktx:1.2.1"
|
||||
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
|
||||
implementation 'com.google.android.material:material:1.5.0'
|
||||
implementation "com.github.bumptech.glide:glide:4.12.0"
|
||||
implementation "androidx.biometric:biometric-ktx:1.2.0-alpha04"
|
||||
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.17.1"
|
||||
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.1.0-beta03"
|
||||
def camerax_version = "1.3.0-rc01"
|
||||
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.4'
|
||||
implementation "com.google.auto.value:auto-value-annotations:$autoValueVersion"
|
||||
annotationProcessor "com.google.auto.value:auto-value:$autoValueVersion"
|
||||
}
|
||||
|
|
|
@ -63,6 +63,7 @@ else
|
|||
--disable-sndio \
|
||||
--disable-schannel \
|
||||
--disable-securetransport \
|
||||
--disable-vulkan \
|
||||
--disable-xlib \
|
||||
--disable-zlib \
|
||||
--disable-cuvid \
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit 356cf8a1604776cb2cc4f4ff873936f7b396bd49
|
||||
Subproject commit 6388eaf433a4196f10389921d5e346c90ee3d793
|
|
@ -1 +1 @@
|
|||
Subproject commit e6e4c201dbf3834de1a49a8b67b4b54239d24249
|
||||
Subproject commit 4f32853ae5ac70811b451cac60ed36fd5b93cbc8
|
|
@ -25,7 +25,7 @@
|
|||
-keepclassmembers class sushi.hardcore.droidfs.explorers.ExplorerElement {
|
||||
static sushi.hardcore.droidfs.explorers.ExplorerElement new(...);
|
||||
}
|
||||
-keepclassmembers class sushi.hardcore.droidfs.video_recording.MediaMuxer {
|
||||
-keepclassmembers class sushi.hardcore.droidfs.video_recording.FFmpegMuxer {
|
||||
void writePacket(byte[]);
|
||||
void seek(long);
|
||||
}
|
|
@ -1,13 +1,9 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="sushi.hardcore.droidfs"
|
||||
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" />
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
|
@ -25,12 +21,14 @@
|
|||
tools:node="remove" /> <!--removing this permission automatically added by exoplayer-->
|
||||
|
||||
<application
|
||||
android:allowBackup="false"
|
||||
android:icon="@mipmap/icon_launcher"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:requestLegacyExternalStorage="true"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/BaseTheme">
|
||||
android:theme="@style/BaseTheme"
|
||||
android:name=".VolumeManagerApp"
|
||||
android:fullBackupContent="false"
|
||||
android:dataExtractionRules="@xml/backup_rules">
|
||||
<activity android:name=".MainActivity" android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
@ -49,13 +47,14 @@
|
|||
<activity android:name=".explorers.ExplorerActivity"/>
|
||||
<activity android:name=".explorers.ExplorerActivityPick"/>
|
||||
<activity android:name=".explorers.ExplorerActivityDrop"/>
|
||||
<activity android:name=".file_viewers.ImageViewer" android:configChanges="screenSize|orientation" /> <!-- don't reload content on configuration change -->
|
||||
<activity android:name=".file_viewers.ImageViewer"/>
|
||||
<activity android:name=".file_viewers.VideoPlayer" android:configChanges="screenSize|orientation" />
|
||||
<activity android:name=".file_viewers.PdfViewer" android:configChanges="screenSize|orientation" />
|
||||
<activity android:name=".file_viewers.PdfViewer" android:configChanges="screenSize|orientation" android:theme="@style/AppTheme" />
|
||||
<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" />
|
||||
|
||||
<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">
|
||||
|
@ -65,10 +64,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>
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
package androidx.camera.video
|
||||
|
||||
import android.media.MediaCodec
|
||||
import android.media.MediaFormat
|
||||
import java.nio.ByteBuffer
|
||||
|
||||
interface MediaMuxer {
|
||||
fun setOrientationHint(degree: Int)
|
||||
fun release()
|
||||
fun addTrack(mediaFormat: MediaFormat): Int
|
||||
fun start()
|
||||
fun writeSampleData(trackIndex: Int, buffer: ByteBuffer, bufferInfo: MediaCodec.BufferInfo)
|
||||
fun stop()
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
package androidx.camera.video
|
||||
|
||||
import android.location.Location
|
||||
|
||||
class MuxerOutputOptions(private val mediaMuxer: MediaMuxer): OutputOptions(MuxerOutputOptionsInternal()) {
|
||||
|
||||
private class MuxerOutputOptionsInternal: OutputOptionsInternal() {
|
||||
override fun getFileSizeLimit(): Long = FILE_SIZE_UNLIMITED.toLong()
|
||||
|
||||
override fun getDurationLimitMillis(): Long = DURATION_UNLIMITED.toLong()
|
||||
|
||||
override fun getLocation(): Location? = null
|
||||
}
|
||||
|
||||
fun getMediaMuxer(): MediaMuxer = mediaMuxer
|
||||
}
|
|
@ -0,0 +1,254 @@
|
|||
/*
|
||||
* Copyright 2021 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.
|
||||
*/
|
||||
|
||||
package androidx.camera.video;
|
||||
|
||||
import android.Manifest;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.CheckResult;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.annotation.RequiresPermission;
|
||||
import androidx.camera.core.impl.utils.ContextUtil;
|
||||
import androidx.core.content.PermissionChecker;
|
||||
import androidx.core.util.Consumer;
|
||||
import androidx.core.util.Preconditions;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
/**
|
||||
* A recording that can be started at a future time.
|
||||
*
|
||||
* <p>A pending recording allows for configuration of a recording before it is started. Once a
|
||||
* pending recording is started with {@link #start(Executor, Consumer)}, any changes to the pending
|
||||
* recording will not affect the actual recording; any modifications to the recording will need
|
||||
* to occur through the controls of the {@link SucklessRecording} class returned by
|
||||
* {@link #start(Executor, Consumer)}.
|
||||
*
|
||||
* <p>A pending recording can be created using one of the {@link Recorder} methods for starting a
|
||||
* recording such as {@link Recorder#prepareRecording(Context, MediaStoreOutputOptions)}.
|
||||
|
||||
* <p>There may be more settings that can only be changed per-recorder instead of per-recording,
|
||||
* because it requires expensive operations like reconfiguring the camera. For those settings, use
|
||||
* the {@link Recorder.Builder} methods to configure before creating the {@link Recorder}
|
||||
* instance, then create the pending recording with it.
|
||||
*/
|
||||
@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
|
||||
@SuppressLint("RestrictedApi")
|
||||
public final class SucklessPendingRecording {
|
||||
|
||||
private final Context mContext;
|
||||
private final SucklessRecorder mRecorder;
|
||||
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) {
|
||||
// Application context is sufficient for all our needs, so store that to avoid leaking
|
||||
// unused resources. For attribution, ContextUtil.getApplicationContext() will retain the
|
||||
// attribution tag from the original context.
|
||||
mContext = ContextUtil.getApplicationContext(context);
|
||||
mRecorder = recorder;
|
||||
mOutputOptions = options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an application context which was retrieved from the {@link Context} used to
|
||||
* create this object.
|
||||
*/
|
||||
@NonNull
|
||||
Context getApplicationContext() {
|
||||
return mContext;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
SucklessRecorder getRecorder() {
|
||||
return mRecorder;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
OutputOptions getOutputOptions() {
|
||||
return mOutputOptions;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
Executor getListenerExecutor() {
|
||||
return mListenerExecutor;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
Consumer<VideoRecordEvent> getEventListener() {
|
||||
return mEventListener;
|
||||
}
|
||||
|
||||
boolean isAudioEnabled() {
|
||||
return mAudioEnabled;
|
||||
}
|
||||
|
||||
boolean isPersistent() {
|
||||
return mIsPersistent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables audio to be recorded for this recording.
|
||||
*
|
||||
* <p>This method must be called prior to {@link #start(Executor, Consumer)} to enable audio
|
||||
* in the recording. If this method is not called, the {@link SucklessRecording} generated by
|
||||
* {@link #start(Executor, Consumer)} will not contain audio, and
|
||||
* {@link AudioStats#getAudioState()} will always return
|
||||
* {@link AudioStats#AUDIO_STATE_DISABLED} for all {@link RecordingStats} send to the listener
|
||||
* set passed to {@link #start(Executor, Consumer)}.
|
||||
*
|
||||
* <p>Recording with audio requires the {@link android.Manifest.permission#RECORD_AUDIO}
|
||||
* permission; without it, recording will fail at {@link #start(Executor, Consumer)} with an
|
||||
* {@link IllegalStateException}.
|
||||
*
|
||||
* @return this pending recording
|
||||
* @throws IllegalStateException if the {@link Recorder} this recording is associated to
|
||||
* doesn't support audio.
|
||||
* @throws SecurityException if the {@link Manifest.permission#RECORD_AUDIO} permission
|
||||
* is denied for the current application.
|
||||
*/
|
||||
@RequiresPermission(Manifest.permission.RECORD_AUDIO)
|
||||
@NonNull
|
||||
public SucklessPendingRecording withAudioEnabled() {
|
||||
// Check permissions and throw a security exception if RECORD_AUDIO is not granted.
|
||||
if (PermissionChecker.checkSelfPermission(mContext, Manifest.permission.RECORD_AUDIO)
|
||||
== PermissionChecker.PERMISSION_DENIED) {
|
||||
throw new SecurityException("Attempted to enable audio for recording but application "
|
||||
+ "does not have RECORD_AUDIO permission granted.");
|
||||
}
|
||||
Preconditions.checkState(mRecorder.isAudioSupported(), "The Recorder this recording is "
|
||||
+ "associated to doesn't support audio.");
|
||||
mAudioEnabled = true;
|
||||
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.
|
||||
*
|
||||
* <p>Only a single recording can be active at a time, so if another recording is active,
|
||||
* this will throw an {@link IllegalStateException}.
|
||||
*
|
||||
* <p>If there are no errors starting the recording, the returned {@link SucklessRecording}
|
||||
* can be used to {@link SucklessRecording#pause() pause}, {@link SucklessRecording#resume() resume},
|
||||
* or {@link SucklessRecording#stop() stop} the recording.
|
||||
*
|
||||
* <p>Upon successfully starting the recording, a {@link VideoRecordEvent.Start} event will
|
||||
* be the first event sent to the provided event listener.
|
||||
*
|
||||
* <p>If errors occur while starting the recording, a {@link VideoRecordEvent.Finalize} event
|
||||
* will be the first event sent to the provided listener, and information about the error can
|
||||
* be found in that event's {@link VideoRecordEvent.Finalize#getError()} method. The returned
|
||||
* {@link SucklessRecording} will be in a finalized state, and all controls will be no-ops.
|
||||
*
|
||||
* <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. 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.
|
||||
* @param listenerExecutor the executor that the event listener will be run on.
|
||||
* @param listener the event listener to handle video record events.
|
||||
*/
|
||||
@NonNull
|
||||
@CheckResult
|
||||
public SucklessRecording start(
|
||||
@NonNull Executor listenerExecutor,
|
||||
@NonNull Consumer<VideoRecordEvent> listener) {
|
||||
Preconditions.checkNotNull(listenerExecutor, "Listener Executor can't be null.");
|
||||
Preconditions.checkNotNull(listener, "Event listener can't be null");
|
||||
mListenerExecutor = listenerExecutor;
|
||||
mEventListener = listener;
|
||||
return mRecorder.start(this);
|
||||
}
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,263 @@
|
|||
/*
|
||||
* Copyright 2021 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.
|
||||
*/
|
||||
|
||||
package androidx.camera.video;
|
||||
|
||||
import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.annotation.RestrictTo;
|
||||
import androidx.camera.core.impl.utils.CloseGuardHelper;
|
||||
import androidx.core.util.Consumer;
|
||||
import androidx.core.util.Preconditions;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
/**
|
||||
* Provides controls for the currently active recording.
|
||||
*
|
||||
* <p>An active recording is created by starting a pending recording with
|
||||
* {@link PendingRecording#start(Executor, Consumer)}. If there are no errors starting the
|
||||
* recording, upon creation, an active recording will provide controls to pause, resume or stop a
|
||||
* recording. If errors occur while starting the recording, the active recording will be
|
||||
* instantiated in a {@link VideoRecordEvent.Finalize finalized} state, and all controls will be
|
||||
* no-ops. The state of the recording can be observed by the video record event listener provided
|
||||
* to {@link PendingRecording#start(Executor, Consumer)} when starting the recording.
|
||||
*
|
||||
* <p>Either {@link #stop()} or {@link #close()} can be called when it is desired to
|
||||
* stop the recording. If {@link #stop()} or {@link #close()} are not called on this object
|
||||
* before it is no longer referenced, it will be automatically stopped at a future point in time
|
||||
* when the object is garbage collected, and no new recordings can be started from the same
|
||||
* {@link Recorder} that generated the object until that occurs.
|
||||
*/
|
||||
@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
|
||||
@SuppressLint("RestrictedApi")
|
||||
public final class SucklessRecording implements AutoCloseable {
|
||||
|
||||
// Indicates the recording has been explicitly stopped by users.
|
||||
private final AtomicBoolean mIsClosed = new AtomicBoolean(false);
|
||||
private final SucklessRecorder mRecorder;
|
||||
private final long mRecordingId;
|
||||
private final OutputOptions mOutputOptions;
|
||||
private final boolean mIsPersistent;
|
||||
private final CloseGuardHelper mCloseGuard = CloseGuardHelper.create();
|
||||
|
||||
SucklessRecording(@NonNull SucklessRecorder recorder, long recordingId, @NonNull OutputOptions options,
|
||||
boolean isPersistent, boolean finalizedOnCreation) {
|
||||
mRecorder = recorder;
|
||||
mRecordingId = recordingId;
|
||||
mOutputOptions = options;
|
||||
mIsPersistent = isPersistent;
|
||||
|
||||
if (finalizedOnCreation) {
|
||||
mIsClosed.set(true);
|
||||
} else {
|
||||
mCloseGuard.open("stop");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an {@link SucklessRecording} from a {@link PendingRecording} and recording ID.
|
||||
*
|
||||
* <p>The recording ID is expected to be unique to the recorder that generated the pending
|
||||
* recording.
|
||||
*/
|
||||
@NonNull
|
||||
static SucklessRecording from(@NonNull SucklessPendingRecording pendingRecording, long recordingId) {
|
||||
Preconditions.checkNotNull(pendingRecording, "The given PendingRecording cannot be null.");
|
||||
return new SucklessRecording(pendingRecording.getRecorder(),
|
||||
recordingId,
|
||||
pendingRecording.getOutputOptions(),
|
||||
pendingRecording.isPersistent(),
|
||||
/*finalizedOnCreation=*/false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an {@link SucklessRecording} from a {@link PendingRecording} and recording ID in a
|
||||
* finalized state.
|
||||
*
|
||||
* <p>This can be used if there was an error setting up the active recording and it would not
|
||||
* be able to be started.
|
||||
*
|
||||
* <p>The recording ID is expected to be unique to the recorder that generated the pending
|
||||
* recording.
|
||||
*/
|
||||
@NonNull
|
||||
static SucklessRecording createFinalizedFrom(@NonNull SucklessPendingRecording pendingRecording,
|
||||
long recordingId) {
|
||||
Preconditions.checkNotNull(pendingRecording, "The given PendingRecording cannot be null.");
|
||||
return new SucklessRecording(pendingRecording.getRecorder(),
|
||||
recordingId,
|
||||
pendingRecording.getOutputOptions(),
|
||||
pendingRecording.isPersistent(),
|
||||
/*finalizedOnCreation=*/true);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
OutputOptions getOutputOptions() {
|
||||
return mOutputOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether this recording is a persistent recording.
|
||||
*
|
||||
* <p>A persistent recording will only be stopped by explicitly calling of
|
||||
* {@link Recording#stop()} and will ignore the lifecycle events or source state changes.
|
||||
* Users are responsible of stopping a persistent recording.
|
||||
*
|
||||
* @return {@code true} if the recording is a persistent recording, otherwise {@code false}.
|
||||
*/
|
||||
@ExperimentalPersistentRecording
|
||||
public boolean isPersistent() {
|
||||
return mIsPersistent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pauses the current recording if active.
|
||||
*
|
||||
* <p>Successful pausing of a recording will generate a {@link VideoRecordEvent.Pause} event
|
||||
* which will be sent to the listener passed to
|
||||
* {@link PendingRecording#start(Executor, Consumer)}.
|
||||
*
|
||||
* <p>If the recording has already been paused or has been finalized internally, this is a
|
||||
* no-op.
|
||||
*
|
||||
* @throws IllegalStateException if the recording has been stopped with
|
||||
* {@link #close()} or {@link #stop()}.
|
||||
*/
|
||||
public void pause() {
|
||||
if (mIsClosed.get()) {
|
||||
throw new IllegalStateException("The recording has been stopped.");
|
||||
}
|
||||
mRecorder.pause(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resumes the current recording if paused.
|
||||
*
|
||||
* <p>Successful resuming of a recording will generate a {@link VideoRecordEvent.Resume} event
|
||||
* which will be sent to the listener passed to
|
||||
* {@link PendingRecording#start(Executor, Consumer)}.
|
||||
*
|
||||
* <p>If the recording is active or has been finalized internally, this is a no-op.
|
||||
*
|
||||
* @throws IllegalStateException if the recording has been stopped with
|
||||
* {@link #close()} or {@link #stop()}.
|
||||
*/
|
||||
public void resume() {
|
||||
if (mIsClosed.get()) {
|
||||
throw new IllegalStateException("The recording has been stopped.");
|
||||
}
|
||||
mRecorder.resume(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the recording, as if calling {@link #close()}.
|
||||
*
|
||||
* <p>This method is equivalent to calling {@link #close()}.
|
||||
*/
|
||||
public void stop() {
|
||||
close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Mutes or un-mutes the current recording.
|
||||
*
|
||||
* <p>The output file will contain an audio track even the whole recording is muted. Create a
|
||||
* recording without calling {@link PendingRecording#withAudioEnabled()} to record a file
|
||||
* with no audio track.
|
||||
*
|
||||
* <p>Muting or unmuting a recording that isn't created
|
||||
* {@link PendingRecording#withAudioEnabled()} with audio enabled is no-op.
|
||||
*
|
||||
* @param muted mutes the recording if {@code true}, un-mutes otherwise.
|
||||
*/
|
||||
public void mute(boolean muted) {
|
||||
if (mIsClosed.get()) {
|
||||
throw new IllegalStateException("The recording has been stopped.");
|
||||
}
|
||||
mRecorder.mute(this, muted);
|
||||
}
|
||||
|
||||
/**
|
||||
* Close this recording.
|
||||
*
|
||||
* <p>Once {@link #stop()} or {@code close()} called, all methods for controlling the state of
|
||||
* this recording besides {@link #stop()} or {@code close()} will throw an
|
||||
* {@link IllegalStateException}.
|
||||
*
|
||||
* <p>Once an active recording has been closed, the next recording can be started with
|
||||
* {@link PendingRecording#start(Executor, Consumer)}.
|
||||
*
|
||||
* <p>This method is idempotent; if the recording has already been closed or has been
|
||||
* finalized internally, calling {@link #stop()} or {@code close()} is a no-op.
|
||||
*
|
||||
* <p>This method is invoked automatically on active recording instances managed by the {@code
|
||||
* try-with-resources} statement.
|
||||
*/
|
||||
@Override
|
||||
public void close() {
|
||||
stopWithError(VideoRecordEvent.Finalize.ERROR_NONE, /*errorCause=*/ null);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("GenericException") // super.finalize() throws Throwable
|
||||
protected void finalize() throws Throwable {
|
||||
try {
|
||||
mCloseGuard.warnIfOpen();
|
||||
stopWithError(VideoRecordEvent.Finalize.ERROR_RECORDING_GARBAGE_COLLECTED,
|
||||
new RuntimeException("Recording stopped due to being garbage collected."));
|
||||
} finally {
|
||||
super.finalize();
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns the recording ID which is unique to the recorder that generated this recording. */
|
||||
long getRecordingId() {
|
||||
return mRecordingId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the recording is closed.
|
||||
*
|
||||
* <p>The returned value does not reflect the state of the recording; it only reflects
|
||||
* whether {@link #stop()} or {@link #close()} was called on this object.
|
||||
*
|
||||
* <p>The state of the recording should be checked from the listener passed to
|
||||
* {@link PendingRecording#start(Executor, Consumer)}. Once the active recording is
|
||||
* stopped, a {@link VideoRecordEvent.Finalize} event will be sent to the listener.
|
||||
*
|
||||
*/
|
||||
@RestrictTo(LIBRARY_GROUP)
|
||||
public boolean isClosed() {
|
||||
return mIsClosed.get();
|
||||
}
|
||||
|
||||
private void stopWithError(@VideoRecordEvent.Finalize.VideoRecordError int error,
|
||||
@Nullable Throwable errorCause) {
|
||||
mCloseGuard.close();
|
||||
if (mIsClosed.getAndSet(true)) {
|
||||
return;
|
||||
}
|
||||
mRecorder.stop(this, error, errorCause);
|
||||
}
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff