Compare commits

..

1 Commits

Author SHA1 Message Date
e851f381a0
Volume provider (unsecure!!!) 2023-08-28 00:35:12 +02:00
82 changed files with 871 additions and 2639 deletions

View File

@ -1,21 +1,18 @@
# Introduction # Introduction
DroidFS relies on modified versions of the original encrypted filesystems programs to open volumes. [CryFS](https://github.com/cryfs/cryfs) is written in C++ while [gocryptfs](https://github.com/rfjakob/gocryptfs) is written in [Go](https://golang.org). Thus, building DroidFS requires the compilation of native code. However, for the sake of simplicity, the application has been designed in a modular way: you can build a version of DroidFS that supports both Gocryptfs and CryFS, or only one of the two. DroidFS relies on modified versions of the original encrypted filesystems programs to open volumes. [CryFS](https://github.com/cryfs/cryfs) is written in C++ while [gocryptfs](https://github.com/rfjakob/gocryptfs) is written in [Go](https://golang.org). Thus, building DroidFS requires the compilation of native code. However, for the sake of simplicity, the application has been designed in a modular way: you can build a version of DroidFS that supports both Gocryptfs and CryFS, or only one of the two.
Moreover, DroidFS aims to be accessible to as many people as possible. If you encounter any problems or need help with the build, feel free to open an issue, a discussion, or contact me (currently the main developer) by [email](mailto:gh@arkensys.dedyn.io) or on [Matrix](https://matrix.org): @hardcoresushi:matrix.underworld.fr Moreover, DroidFS aims to be accessible to as many people as possible. If you encounter any problems or need help with the build, feel free to open an issue, a discussion, or contact me by [email](mailto:hardcore.sushi@disroot.org) or on [Matrix](https://matrix.org): @hardcoresushi:matrix.underworld.fr
# Setup # Setup
The following two steps assume you're using a Debian-based Linux distribution. Package names might be similar for other distributions. Don't hesitate to ask if you're having trouble with this.
Install required packages: Install required packages:
``` ```
$ sudo apt-get install openjdk-17-jdk-headless build-essential pkg-config git gnupg2 wget apksigner npm $ sudo apt-get install openjdk-11-jdk-headless build-essential pkg-config git gnupg2 wget apksigner
``` ```
You also need to manually install the [Android SDK](https://developer.android.com/studio/index.html#command-tools) and the [Android Native Development Kit (NDK)](https://github.com/android/ndk/wiki/Unsupported-Downloads#r25c) version `25.2.9519653` (r25c). libcryfs cannot be built with newer NDK versions at the moment due to compatibility issues with [boost](https://www.boost.org). If you succeed in building it with a more recent version of NDK, please report it. You also need to manually install the [Android SDK](https://developer.android.com/studio/index.html#command-tools) and the [Android Native Development Kit (NDK)](https://developer.android.com/ndk/downloads) (r23 versions are recommended).
If you want a support for Gocryptfs volumes, you need to install [Go](https://golang.org/doc/install): If you want a support for Gocryptfs volumes, you must install [Go](https://golang.org/doc/install) and libssl:
``` ```
$ sudo apt-get install golang-go $ sudo apt-get install golang-go libssl-dev
``` ```
The code should be authenticated before being built. To verify the signatures, you will need my PGP key: The code should be authenticated before being built. To verify the signatures, you will need my PGP key:
``` ```
@ -48,16 +45,16 @@ $ git clone --depth=1 https://git.ffmpeg.org/ffmpeg.git
If you want Gocryptfs support, you need to download OpenSSL: If you want Gocryptfs support, you need to download OpenSSL:
``` ```
$ cd ../libgocryptfs $ cd ../libgocryptfs
$ wget https://openssl.org/source/openssl-3.3.1.tar.gz $ wget https://www.openssl.org/source/openssl-1.1.1t.tar.gz
``` ```
Verify OpenSSL signature: Verify OpenSSL signature:
``` ```
$ https://openssl.org/source/openssl-3.3.1.tar.gz.asc $ wget https://www.openssl.org/source/openssl-1.1.1t.tar.gz.asc
$ gpg --verify openssl-3.3.1.tar.gz.asc openssl-3.3.1.tar.gz $ gpg --verify openssl-1.1.1t.tar.gz.asc openssl-1.1.1t.tar.gz
``` ```
Continue **ONLY** if the signature is **VALID**. Continue **ONLY** if the signature is **VALID**.
``` ```
$ tar -xzf openssl-3.3.1.tar.gz $ tar -xzf openssl-1.1.1t.tar.gz
``` ```
If you want CryFS support, initialize libcryfs: If you want CryFS support, initialize libcryfs:
``` ```
@ -79,8 +76,8 @@ $ ./build.sh ffmpeg
This step is only required if you want Gocryptfs support. This step is only required if you want Gocryptfs support.
``` ```
$ cd app/libgocryptfs $ cd app/libgocryptfs
$ ANDROID_NDK_ROOT="$ANDROID_NDK_HOME" OPENSSL_PATH="./openssl-3.3.1" ./build.sh $ OPENSSL_PATH="./openssl-1.1.1t" ./build.sh
``` ```
## Compile APKs ## Compile APKs
Gradle build libgocryptfs and libcryfs by default. Gradle build libgocryptfs and libcryfs by default.

View File

@ -11,7 +11,7 @@ For mortals: Encrypted storage compatible with already existing softwares.
</p> </p>
# Support # 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). 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 ❤️. Thank you so much ❤️.
@ -39,30 +39,23 @@ 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. 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>
<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> <li><h4>Allow exporting files:</h4>
Decrypt and write file to disk (external storage). Any app with storage permissions could access exported files. Decrypt and write file to disk (external storage). Any app with storage permissions could access exported files.
</li> </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. Decrypt and share file with other apps. These apps could save and send the files thus shared.
</li> </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> <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. 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>
<li><h4>Allow opening files with other applications*:</h4> <li><h4>Allow saving password hash using fingerprint:</h4>
Decrypt and open file using external apps. These apps could save and send the files thus opened. 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>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> </li>
</ul> </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 # Download
<a href="https://f-droid.org/packages/sushi.hardcore.droidfs"> <a href="https://f-droid.org/packages/sushi.hardcore.droidfs">
@ -106,7 +99,7 @@ DroidFS needs some permissions for certain features. However, you are free to de
</ul> </ul>
# Limitations # 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). If you want to synchronize your volumes on a cloud, the cloud application must synchronize the encrypted directory from disk. 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.
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. 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.

View File

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

View File

@ -74,7 +74,7 @@ else
--disable-appkit \ --disable-appkit \
--disable-alsa \ --disable-alsa \
--disable-debug \ --disable-debug \
&& >/dev/null &&
make -j $(nproc --all) >/dev/null) && make -j $(nproc --all) >/dev/null) &&
mkdir -p build/$1/libavformat build/$1/libavcodec build/$1/libavutil && mkdir -p build/$1/libavformat build/$1/libavcodec build/$1/libavutil &&
cp $FFMPEG_DIR/libavformat/*.h $FFMPEG_DIR/libavformat/libavformat.so build/$1/libavformat && cp $FFMPEG_DIR/libavformat/*.h $FFMPEG_DIR/libavformat/libavformat.so build/$1/libavformat &&

@ -1 +1 @@
Subproject commit 0398d48b0963c01092976c5c7012b02327e564f0 Subproject commit 3c56f86d86afacaf4a07ae77aa3d146764d587ec

@ -1 +1 @@
Subproject commit 4f32853ae5ac70811b451cac60ed36fd5b93cbc8 Subproject commit ab3e7886767d31f32baebcd72ebe5f098a70d65b

View File

@ -1,4 +1,24 @@
-keepattributes SourceFile,LineNumberTable # 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
-keep class sushi.hardcore.droidfs.SettingsActivity$** -keep class sushi.hardcore.droidfs.SettingsActivity$**
-keep class sushi.hardcore.droidfs.explorers.ExplorerElement -keep class sushi.hardcore.droidfs.explorers.ExplorerElement
@ -8,17 +28,4 @@
-keepclassmembers class sushi.hardcore.droidfs.video_recording.FFmpegMuxer { -keepclassmembers class sushi.hardcore.droidfs.video_recording.FFmpegMuxer {
void writePacket(byte[]); void writePacket(byte[]);
void seek(long); 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

@ -53,7 +53,6 @@
<activity android:name=".file_viewers.AudioPlayer" android:configChanges="screenSize|orientation" /> <activity android:name=".file_viewers.AudioPlayer" android:configChanges="screenSize|orientation" />
<activity android:name=".file_viewers.TextEditor" android:configChanges="screenSize|orientation" /> <activity android:name=".file_viewers.TextEditor" android:configChanges="screenSize|orientation" />
<activity android:name=".CameraActivity" android:screenOrientation="nosensor" /> <activity android:name=".CameraActivity" android:screenOrientation="nosensor" />
<activity android:name=".LogcatActivity"/>
<service android:name=".WiperService" android:exported="false" android:stopWithTask="false"/> <service android:name=".WiperService" android:exported="false" android:stopWithTask="false"/>
<service android:name=".file_operations.FileOperationService" android:exported="false"/> <service android:name=".file_operations.FileOperationService" android:exported="false"/>

View File

@ -58,8 +58,8 @@ public final class SucklessPendingRecording {
private final OutputOptions mOutputOptions; private final OutputOptions mOutputOptions;
private Consumer<VideoRecordEvent> mEventListener; private Consumer<VideoRecordEvent> mEventListener;
private Executor mListenerExecutor; private Executor mListenerExecutor;
private boolean mAudioEnabled = false; private boolean mAudioEnabled = false;
private boolean mIsPersistent = false;
SucklessPendingRecording(@NonNull Context context, @NonNull SucklessRecorder recorder, SucklessPendingRecording(@NonNull Context context, @NonNull SucklessRecorder recorder,
@NonNull OutputOptions options) { @NonNull OutputOptions options) {
@ -104,10 +104,6 @@ public final class SucklessPendingRecording {
return mAudioEnabled; return mAudioEnabled;
} }
boolean isPersistent() {
return mIsPersistent;
}
/** /**
* Enables audio to be recorded for this recording. * Enables audio to be recorded for this recording.
* *
@ -143,69 +139,6 @@ public final class SucklessPendingRecording {
return this; 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. * Starts the recording, making it an active recording.
* *
@ -226,13 +159,7 @@ public final class SucklessPendingRecording {
* *
* <p>If the returned {@link SucklessRecording} is garbage collected, the recording will be * <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 * 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 * the recording needs to be active.
* {@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 * @throws IllegalStateException if the associated Recorder currently has an unfinished
* active recording. * active recording.

View File

@ -16,7 +16,6 @@
package androidx.camera.video; 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_DURATION_LIMIT_REACHED;
import static androidx.camera.video.VideoRecordEvent.Finalize.ERROR_ENCODING_FAILED; import static androidx.camera.video.VideoRecordEvent.Finalize.ERROR_ENCODING_FAILED;
import static androidx.camera.video.VideoRecordEvent.Finalize.ERROR_FILE_SIZE_LIMIT_REACHED; import static androidx.camera.video.VideoRecordEvent.Finalize.ERROR_FILE_SIZE_LIMIT_REACHED;
@ -55,8 +54,6 @@ import androidx.annotation.RequiresPermission;
import androidx.annotation.RestrictTo; import androidx.annotation.RestrictTo;
import androidx.annotation.VisibleForTesting; import androidx.annotation.VisibleForTesting;
import androidx.camera.core.AspectRatio; import androidx.camera.core.AspectRatio;
import androidx.camera.core.CameraInfo;
import androidx.camera.core.DynamicRange;
import androidx.camera.core.Logger; import androidx.camera.core.Logger;
import androidx.camera.core.SurfaceRequest; import androidx.camera.core.SurfaceRequest;
import androidx.camera.core.impl.MutableStateObservable; import androidx.camera.core.impl.MutableStateObservable;
@ -79,7 +76,7 @@ import androidx.camera.video.internal.compat.Api26Impl;
import androidx.camera.video.internal.compat.quirk.DeactivateEncoderSurfaceBeforeStopEncoderQuirk; import androidx.camera.video.internal.compat.quirk.DeactivateEncoderSurfaceBeforeStopEncoderQuirk;
import androidx.camera.video.internal.compat.quirk.DeviceQuirks; import androidx.camera.video.internal.compat.quirk.DeviceQuirks;
import androidx.camera.video.internal.compat.quirk.EncoderNotUsePersistentInputSurfaceQuirk; import androidx.camera.video.internal.compat.quirk.EncoderNotUsePersistentInputSurfaceQuirk;
import androidx.camera.video.internal.config.AudioMimeInfo; import androidx.camera.video.internal.config.MimeInfo;
import androidx.camera.video.internal.encoder.AudioEncoderConfig; import androidx.camera.video.internal.encoder.AudioEncoderConfig;
import androidx.camera.video.internal.encoder.BufferCopiedEncodedData; import androidx.camera.video.internal.encoder.BufferCopiedEncodedData;
import androidx.camera.video.internal.encoder.EncodeException; import androidx.camera.video.internal.encoder.EncodeException;
@ -343,14 +340,10 @@ public final class SucklessRecorder implements VideoOutput {
//////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////
// Members only accessed on mSequentialExecutor // // Members only accessed on mSequentialExecutor //
//////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////
@SuppressWarnings("WeakerAccess") /* synthetic accessor */ private RecordingRecord mInProgressRecording = null;
RecordingRecord mInProgressRecording = null;
@SuppressWarnings("WeakerAccess") /* synthetic accessor */ @SuppressWarnings("WeakerAccess") /* synthetic accessor */
boolean mInProgressRecordingStopping = false; boolean mInProgressRecordingStopping = false;
@Nullable private SurfaceRequest.TransformationInfo mSurfaceTransformationInfo = null;
private SurfaceRequest.TransformationInfo mInProgressTransformationInfo = null;
@Nullable
private SurfaceRequest.TransformationInfo mSourceTransformationInfo = null;
private VideoValidatedEncoderProfilesProxy mResolvedEncoderProfiles = null; private VideoValidatedEncoderProfilesProxy mResolvedEncoderProfiles = null;
@SuppressWarnings("WeakerAccess") /* synthetic accessor */ @SuppressWarnings("WeakerAccess") /* synthetic accessor */
final List<ListenableFuture<Void>> mEncodingFutures = new ArrayList<>(); final List<ListenableFuture<Void>> mEncodingFutures = new ArrayList<>();
@ -431,15 +424,13 @@ public final class SucklessRecorder implements VideoOutput {
@SuppressWarnings("WeakerAccess") /* synthetic accessor */ @SuppressWarnings("WeakerAccess") /* synthetic accessor */
ScheduledFuture<?> mSourceNonStreamingTimeout = null; ScheduledFuture<?> mSourceNonStreamingTimeout = null;
// The Recorder has to be reset first before being configured again. // The Recorder has to be reset first before being configured again.
private boolean mNeedsResetBeforeNextStart = false; private boolean mNeedsReset = false;
@NonNull @NonNull
@SuppressWarnings("WeakerAccess") /* synthetic accessor */ @SuppressWarnings("WeakerAccess") /* synthetic accessor */
VideoEncoderSession mVideoEncoderSession; VideoEncoderSession mVideoEncoderSession;
@Nullable @Nullable
@SuppressWarnings("WeakerAccess") /* synthetic accessor */ @SuppressWarnings("WeakerAccess") /* synthetic accessor */
VideoEncoderSession mVideoEncoderSessionToRelease = null; VideoEncoderSession mVideoEncoderSessionToRelease = null;
double mAudioAmplitude = 0;
private boolean mShouldSendResumeEvent = false;
//--------------------------------------------------------------------------------------------// //--------------------------------------------------------------------------------------------//
SucklessRecorder(@Nullable Executor executor, @NonNull MediaSpec mediaSpec, SucklessRecorder(@Nullable Executor executor, @NonNull MediaSpec mediaSpec,
@ -496,13 +487,6 @@ public final class SucklessRecorder implements VideoOutput {
mSequentialExecutor.execute(() -> onSourceStateChangedInternal(newState)); mSequentialExecutor.execute(() -> onSourceStateChangedInternal(newState));
} }
@RestrictTo(RestrictTo.Scope.LIBRARY)
@Override
@NonNull
public VideoCapabilities getMediaCapabilities(@NonNull CameraInfo cameraInfo) {
return getVideoCapabilities(cameraInfo);
}
@NonNull @NonNull
public SucklessPendingRecording prepareRecording(@NonNull Context context, @NonNull MuxerOutputOptions outputOptions) { public SucklessPendingRecording prepareRecording(@NonNull Context context, @NonNull MuxerOutputOptions outputOptions) {
return prepareRecordingInternal(context, outputOptions); return prepareRecordingInternal(context, outputOptions);
@ -772,8 +756,7 @@ public final class SucklessRecorder implements VideoOutput {
} }
} }
void stop(@NonNull SucklessRecording activeRecording, @VideoRecordError int error, void stop(@NonNull SucklessRecording activeRecording) {
@Nullable Throwable errorCause) {
RecordingRecord pendingRecordingToFinalize = null; RecordingRecord pendingRecordingToFinalize = null;
synchronized (mLock) { synchronized (mLock) {
if (!isSameRecording(activeRecording, mPendingRecordingRecord) && !isSameRecording( if (!isSameRecording(activeRecording, mPendingRecordingRecord) && !isSameRecording(
@ -818,7 +801,7 @@ public final class SucklessRecorder implements VideoOutput {
long explicitlyStopTimeUs = TimeUnit.NANOSECONDS.toMicros(System.nanoTime()); long explicitlyStopTimeUs = TimeUnit.NANOSECONDS.toMicros(System.nanoTime());
RecordingRecord finalActiveRecordingRecord = mActiveRecordingRecord; RecordingRecord finalActiveRecordingRecord = mActiveRecordingRecord;
mSequentialExecutor.execute(() -> stopInternal(finalActiveRecordingRecord, mSequentialExecutor.execute(() -> stopInternal(finalActiveRecordingRecord,
explicitlyStopTimeUs, error, errorCause)); explicitlyStopTimeUs, ERROR_NONE, null));
break; break;
case ERROR: case ERROR:
// In an error state, the recording will already be finalized. Treat as a // In an error state, the recording will already be finalized. Treat as a
@ -828,13 +811,9 @@ public final class SucklessRecorder implements VideoOutput {
} }
if (pendingRecordingToFinalize != null) { 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, finalizePendingRecording(pendingRecordingToFinalize, ERROR_NO_VALID_DATA,
new RuntimeException("Recording was stopped before any data could be " new RuntimeException("Recording was stopped before any data could be "
+ "produced.", errorCause)); + "produced."));
} }
} }
@ -864,8 +843,7 @@ public final class SucklessRecorder implements VideoOutput {
recordingToFinalize.getOutputOptions(), recordingToFinalize.getOutputOptions(),
RecordingStats.of(/*duration=*/0L, RecordingStats.of(/*duration=*/0L,
/*bytes=*/0L, /*bytes=*/0L,
AudioStats.of(AudioStats.AUDIO_STATE_DISABLED, mAudioErrorCause, AudioStats.of(AudioStats.AUDIO_STATE_DISABLED, mAudioErrorCause)),
AUDIO_AMPLITUDE_NONE)),
OutputResults.of(Uri.EMPTY), OutputResults.of(Uri.EMPTY),
error, error,
cause)); cause));
@ -896,15 +874,14 @@ public final class SucklessRecorder implements VideoOutput {
// If we're inactive and have no active surface, we'll reset the encoder directly. // 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 // Otherwise, we'll wait for the active surface's surface request listener to
// reset the encoder. // reset the encoder.
requestReset(ERROR_SOURCE_INACTIVE, null, false); requestReset(ERROR_SOURCE_INACTIVE, null);
} else { } else {
// The source becomes inactive, the incoming new surface request has to be cached // 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 // and be serviced after the Recorder is reset when receiving the previous
// surface request complete callback. // surface request complete callback.
mNeedsResetBeforeNextStart = true; mNeedsReset = true;
if (mInProgressRecording != null && !mInProgressRecording.isPersistent()) { if (mInProgressRecording != null) {
// Stop the in progress recording with "source inactive" error if it's not a // Stop any in progress recording with "source inactive" error
// persistent recording.
onInProgressRecordingInternalError(mInProgressRecording, ERROR_SOURCE_INACTIVE, onInProgressRecordingInternalError(mInProgressRecording, ERROR_SOURCE_INACTIVE,
null); null);
} }
@ -928,8 +905,7 @@ public final class SucklessRecorder implements VideoOutput {
* the surface request complete callback first. * the surface request complete callback first.
*/ */
@ExecutedBy("mSequentialExecutor") @ExecutedBy("mSequentialExecutor")
void requestReset(@VideoRecordError int errorCode, @Nullable Throwable errorCause, void requestReset(@VideoRecordError int errorCode, @Nullable Throwable errorCause) {
boolean videoOnly) {
boolean shouldReset = false; boolean shouldReset = false;
boolean shouldStop = false; boolean shouldStop = false;
synchronized (mLock) { synchronized (mLock) {
@ -951,22 +927,14 @@ public final class SucklessRecorder implements VideoOutput {
case PAUSED: case PAUSED:
// Fall-through // Fall-through
case RECORDING: case RECORDING:
Preconditions.checkState(mInProgressRecording != null, "In-progress recording"
+ " shouldn't be null when in state " + mState);
if (mActiveRecordingRecord != mInProgressRecording) { if (mActiveRecordingRecord != mInProgressRecording) {
throw new AssertionError("In-progress recording does not match the active" throw new AssertionError("In-progress recording does not match the active"
+ " recording. Unable to reset encoder."); + " recording. Unable to reset encoder.");
} }
// If there's an active persistent recording, reset the Recorder directly. // If there's an active recording, stop it first then release the resources
// Otherwise, stop the recording first then release the Recorder at // at onRecordingFinalized().
// onRecordingFinalized(). shouldStop = true;
if (isPersistentRecordingInProgress()) { // Fall-through
shouldReset = true;
} else {
shouldStop = true;
setState(State.RESETTING);
}
break;
case STOPPING: case STOPPING:
// Already stopping. Set state to RESETTING so resources will be released once // Already stopping. Set state to RESETTING so resources will be released once
// onRecordingFinalized() runs. // onRecordingFinalized() runs.
@ -981,17 +949,14 @@ public final class SucklessRecorder implements VideoOutput {
// These calls must not be posted to the executor to ensure they are executed inline on // 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. // the sequential executor and the state changes above are correctly handled.
if (shouldReset) { if (shouldReset) {
if (videoOnly) { reset();
resetVideo();
} else {
reset();
}
} else if (shouldStop) { } else if (shouldStop) {
stopInternal(mInProgressRecording, Encoder.NO_TIMESTAMP, errorCode, errorCause); stopInternal(mInProgressRecording, Encoder.NO_TIMESTAMP, errorCode, errorCause);
} }
} }
@ExecutedBy("mSequentialExecutor") @ExecutedBy("mSequentialExecutor")
private void configureInternal(@NonNull SurfaceRequest surfaceRequest, private void configureInternal(@NonNull SurfaceRequest surfaceRequest,
@NonNull Timebase videoSourceTimebase) { @NonNull Timebase videoSourceTimebase) {
if (surfaceRequest.isServiced()) { if (surfaceRequest.isServiced()) {
@ -999,19 +964,16 @@ public final class SucklessRecorder implements VideoOutput {
return; return;
} }
surfaceRequest.setTransformationInfoListener(mSequentialExecutor, surfaceRequest.setTransformationInfoListener(mSequentialExecutor,
(transformationInfo) -> mSourceTransformationInfo = transformationInfo); (transformationInfo) -> mSurfaceTransformationInfo = transformationInfo);
Size surfaceSize = surfaceRequest.getResolution(); Size surfaceSize = surfaceRequest.getResolution();
// Fetch and cache nearest encoder profiles, if one exists. // Fetch and cache nearest encoder profiles, if one exists.
DynamicRange dynamicRange = surfaceRequest.getDynamicRange(); LegacyVideoCapabilities capabilities =
VideoCapabilities capabilities = getVideoCapabilities( LegacyVideoCapabilities.from(surfaceRequest.getCamera().getCameraInfo());
surfaceRequest.getCamera().getCameraInfo()); Quality highestSupportedQuality = capabilities.findHighestSupportedQualityFor(surfaceSize);
Quality highestSupportedQuality = capabilities.findHighestSupportedQualityFor(surfaceSize,
dynamicRange);
Logger.d(TAG, "Using supported quality of " + highestSupportedQuality Logger.d(TAG, "Using supported quality of " + highestSupportedQuality
+ " for surface size " + surfaceSize); + " for surface size " + surfaceSize);
if (highestSupportedQuality != Quality.NONE) { if (highestSupportedQuality != Quality.NONE) {
mResolvedEncoderProfiles = capabilities.getProfiles(highestSupportedQuality, mResolvedEncoderProfiles = capabilities.getProfiles(highestSupportedQuality);
dynamicRange);
if (mResolvedEncoderProfiles == null) { if (mResolvedEncoderProfiles == null) {
throw new AssertionError("Camera advertised available quality but did not " throw new AssertionError("Camera advertised available quality but did not "
+ "produce EncoderProfiles for advertised quality."); + "produce EncoderProfiles for advertised quality.");
@ -1024,14 +986,9 @@ public final class SucklessRecorder implements VideoOutput {
@ExecutedBy("mSequentialExecutor") @ExecutedBy("mSequentialExecutor")
private void setupVideo(@NonNull SurfaceRequest request, @NonNull Timebase timebase) { private void setupVideo(@NonNull SurfaceRequest request, @NonNull Timebase timebase) {
safeToCloseVideoEncoder().addListener(() -> { safeToCloseVideoEncoder().addListener(() -> {
if (request.isServiced() if (request.isServiced() || mVideoEncoderSession.isConfiguredSurfaceRequest(request)) {
|| (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: " 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; return;
} }
VideoEncoderSession videoEncoderSession = VideoEncoderSession videoEncoderSession =
@ -1063,12 +1020,6 @@ public final class SucklessRecorder implements VideoOutput {
}, mSequentialExecutor); }, mSequentialExecutor);
} }
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
@ExecutedBy("mSequentialExecutor")
boolean isPersistentRecordingInProgress() {
return mInProgressRecording != null && mInProgressRecording.isPersistent();
}
@NonNull @NonNull
@ExecutedBy("mSequentialExecutor") @ExecutedBy("mSequentialExecutor")
private ListenableFuture<Void> safeToCloseVideoEncoder() { private ListenableFuture<Void> safeToCloseVideoEncoder() {
@ -1104,9 +1055,7 @@ public final class SucklessRecorder implements VideoOutput {
mVideoEncoderSessionToRelease = videoEncoderSession; mVideoEncoderSessionToRelease = videoEncoderSession;
setLatestSurface(null); setLatestSurface(null);
// Only reset video if the in-progress recording is persistent. requestReset(ERROR_SOURCE_INACTIVE, null);
requestReset(ERROR_SOURCE_INACTIVE, null,
isPersistentRecordingInProgress());
} }
@Override @Override
@ -1121,14 +1070,17 @@ public final class SucklessRecorder implements VideoOutput {
void onConfigured() { void onConfigured() {
RecordingRecord recordingToStart = null; RecordingRecord recordingToStart = null;
RecordingRecord pendingRecordingToFinalize = null; RecordingRecord pendingRecordingToFinalize = null;
boolean continuePersistentRecording = false;
@VideoRecordError int error = ERROR_NONE; @VideoRecordError int error = ERROR_NONE;
Throwable errorCause = null; Throwable errorCause = null;
boolean recordingPaused = false; boolean startRecordingPaused = false;
synchronized (mLock) { synchronized (mLock) {
switch (mState) { switch (mState) {
case IDLING: case IDLING:
// Fall-through // Fall-through
case RECORDING:
// Fall-through
case PAUSED:
// Fall-through
case RESETTING: case RESETTING:
throw new AssertionError( throw new AssertionError(
"Incorrectly invoke onConfigured() in state " + mState); "Incorrectly invoke onConfigured() in state " + mState);
@ -1138,15 +1090,6 @@ public final class SucklessRecorder implements VideoOutput {
+ "STOPPING state when it's not waiting for a new surface."); + "STOPPING state when it's not waiting for a new surface.");
} }
break; 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: case CONFIGURING:
setState(State.IDLING); setState(State.IDLING);
break; break;
@ -1155,7 +1098,7 @@ public final class SucklessRecorder implements VideoOutput {
"onConfigured() was invoked when the Recorder had encountered error"); "onConfigured() was invoked when the Recorder had encountered error");
break; break;
case PENDING_PAUSED: case PENDING_PAUSED:
recordingPaused = true; startRecordingPaused = true;
// Fall through // Fall through
case PENDING_RECORDING: case PENDING_RECORDING:
if (mActiveRecordingRecord != null) { if (mActiveRecordingRecord != null) {
@ -1176,21 +1119,9 @@ public final class SucklessRecorder implements VideoOutput {
} }
} }
if (continuePersistentRecording) { if (recordingToStart != null) {
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). // Start new active recording inline on sequential executor (but unlocked).
startRecording(recordingToStart, recordingPaused); startRecording(recordingToStart, startRecordingPaused);
} else if (pendingRecordingToFinalize != null) { } else if (pendingRecordingToFinalize != null) {
finalizePendingRecording(pendingRecordingToFinalize, error, errorCause); finalizePendingRecording(pendingRecordingToFinalize, error, errorCause);
} }
@ -1231,7 +1162,7 @@ public final class SucklessRecorder implements VideoOutput {
throws AudioSourceAccessException, InvalidConfigException { throws AudioSourceAccessException, InvalidConfigException {
MediaSpec mediaSpec = getObservableData(mMediaSpec); MediaSpec mediaSpec = getObservableData(mMediaSpec);
// Resolve the audio mime info // Resolve the audio mime info
AudioMimeInfo audioMimeInfo = resolveAudioMimeInfo(mediaSpec, mResolvedEncoderProfiles); MimeInfo audioMimeInfo = resolveAudioMimeInfo(mediaSpec, mResolvedEncoderProfiles);
Timebase audioSourceTimebase = Timebase.UPTIME; Timebase audioSourceTimebase = Timebase.UPTIME;
// Select and create the audio source // Select and create the audio source
@ -1382,10 +1313,8 @@ public final class SucklessRecorder implements VideoOutput {
return; return;
} }
SurfaceRequest.TransformationInfo transformationInfo = mSourceTransformationInfo; if (mSurfaceTransformationInfo != null) {
if (transformationInfo != null) { mediaMuxer.setOrientationHint(mSurfaceTransformationInfo.getRotationDegrees());
setInProgressTransformationInfo(transformationInfo);
mediaMuxer.setOrientationHint(transformationInfo.getRotationDegrees());
} }
mVideoTrackIndex = mediaMuxer.addTrack(mVideoOutputConfig.getMediaFormat()); mVideoTrackIndex = mediaMuxer.addTrack(mVideoOutputConfig.getMediaFormat());
@ -1473,9 +1402,7 @@ public final class SucklessRecorder implements VideoOutput {
"The Recorder doesn't support recording with audio"); "The Recorder doesn't support recording with audio");
} }
try { try {
if (!mInProgressRecording.isPersistent() || mAudioEncoder == null) { setupAudio(recordingToStart);
setupAudio(recordingToStart);
}
setAudioState(AudioState.ENABLED); setAudioState(AudioState.ENABLED);
} catch (AudioSourceAccessException | InvalidConfigException e) { } catch (AudioSourceAccessException | InvalidConfigException e) {
Logger.e(TAG, "Unable to create audio resource with error: ", e); Logger.e(TAG, "Unable to create audio resource with error: ", e);
@ -1492,7 +1419,7 @@ public final class SucklessRecorder implements VideoOutput {
break; break;
} }
updateEncoderCallbacks(recordingToStart, false); initEncoderAndAudioSourceCallbacks(recordingToStart);
if (isAudioEnabled()) { if (isAudioEnabled()) {
mAudioSource.start(recordingToStart.isMuted()); mAudioSource.start(recordingToStart.isMuted());
mAudioEncoder.start(); mAudioEncoder.start();
@ -1505,17 +1432,7 @@ public final class SucklessRecorder implements VideoOutput {
} }
@ExecutedBy("mSequentialExecutor") @ExecutedBy("mSequentialExecutor")
private void updateEncoderCallbacks(@NonNull RecordingRecord recordingToStart, private void initEncoderAndAudioSourceCallbacks(@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( mEncodingFutures.add(CallbackToFutureAdapter.getFuture(
completer -> { completer -> {
mVideoEncoder.setEncoderCallback(new EncoderCallback() { mVideoEncoder.setEncoderCallback(new EncoderCallback() {
@ -1611,7 +1528,7 @@ public final class SucklessRecorder implements VideoOutput {
return "videoEncodingFuture"; return "videoEncodingFuture";
})); }));
if (isAudioEnabled() && !videoOnly) { if (isAudioEnabled()) {
mEncodingFutures.add(CallbackToFutureAdapter.getFuture( mEncodingFutures.add(CallbackToFutureAdapter.getFuture(
completer -> { completer -> {
Consumer<Throwable> audioErrorConsumer = throwable -> { Consumer<Throwable> audioErrorConsumer = throwable -> {
@ -1651,11 +1568,6 @@ public final class SucklessRecorder implements VideoOutput {
audioErrorConsumer.accept(throwable); audioErrorConsumer.accept(throwable);
} }
} }
@Override
public void onAmplitudeValue(double maxAmplitude) {
mAudioAmplitude = maxAmplitude;
}
}); });
mAudioEncoder.setEncoderCallback(new EncoderCallback() { mAudioEncoder.setEncoderCallback(new EncoderCallback() {
@ -1742,16 +1654,12 @@ public final class SucklessRecorder implements VideoOutput {
@Override @Override
public void onFailure(@NonNull Throwable t) { public void onFailure(@NonNull Throwable t) {
Preconditions.checkState(mInProgressRecording != null, Logger.d(TAG, "Encodings end with error: " + t);
"In-progress recording shouldn't be null"); // If the media muxer hasn't been set up, assume the encoding fails
// If a persistent recording requires reconfiguring the video encoder, // because of no valid data has been produced.
// the previous encoder future has to be canceled without finalizing the finalizeInProgressRecording(
// in-progress recording. mMediaMuxer == null ? ERROR_NO_VALID_DATA : ERROR_ENCODING_FAILED,
if (!mInProgressRecording.isPersistent()) { t);
Logger.d(TAG, "Encodings end with error: " + t);
finalizeInProgressRecording(mMediaMuxer == null ? ERROR_NO_VALID_DATA
: ERROR_ENCODING_FAILED, t);
}
} }
}, },
// Can use direct executor since completers are always completed on sequential // Can use direct executor since completers are always completed on sequential
@ -1892,20 +1800,11 @@ public final class SucklessRecorder implements VideoOutput {
if (isAudioEnabled()) { if (isAudioEnabled()) {
mAudioEncoder.start(); mAudioEncoder.start();
} }
// If a persistent recording is resumed immediately after the VideoCapture is rebound mVideoEncoder.start();
// to a camera, it's possible that the encoder hasn't been created yet. Then the
// encoder will be started once it's initialized. So only start the encoder when it's mInProgressRecording.updateVideoRecordEvent(VideoRecordEvent.resume(
// not null. mInProgressRecording.getOutputOptions(),
if (mVideoEncoder != null) { getInProgressRecordingStats()));
mVideoEncoder.start();
mInProgressRecording.updateVideoRecordEvent(VideoRecordEvent.resume(
mInProgressRecording.getOutputOptions(),
getInProgressRecordingStats()));
} else {
// Instead sending here, send the Resume event once the encoder is initialized
// and started.
mShouldSendResumeEvent = true;
}
} }
} }
@ -1999,12 +1898,13 @@ public final class SucklessRecorder implements VideoOutput {
mAudioEncoder = null; mAudioEncoder = null;
mAudioOutputConfig = null; mAudioOutputConfig = null;
} }
tryReleaseVideoEncoder();
if (mAudioSource != null) { if (mAudioSource != null) {
releaseCurrentAudioSource(); releaseCurrentAudioSource();
} }
setAudioState(AudioState.INITIALIZING); setAudioState(AudioState.INITIALIZING);
resetVideo(); onReset();
} }
@SuppressWarnings("FutureReturnValueIgnored") @SuppressWarnings("FutureReturnValueIgnored")
@ -2026,8 +1926,7 @@ public final class SucklessRecorder implements VideoOutput {
} }
@ExecutedBy("mSequentialExecutor") @ExecutedBy("mSequentialExecutor")
private void onResetVideo() { private void onReset() {
boolean shouldConfigure = true;
synchronized (mLock) { synchronized (mLock) {
switch (mState) { switch (mState) {
case PENDING_PAUSED: case PENDING_PAUSED:
@ -2040,10 +1939,6 @@ public final class SucklessRecorder implements VideoOutput {
case PAUSED: case PAUSED:
// Fall-through // Fall-through
case RECORDING: case RECORDING:
if (isPersistentRecordingInProgress()) {
shouldConfigure = false;
break;
}
// Fall-through // Fall-through
case IDLING: case IDLING:
// Fall-through // Fall-through
@ -2058,24 +1953,14 @@ public final class SucklessRecorder implements VideoOutput {
} }
} }
mNeedsResetBeforeNextStart = false; mNeedsReset = false;
// If the latest surface request hasn't been serviced, use it to re-configure the Recorder. // If the latest surface request hasn't been serviced, use it to re-configure the Recorder.
if (shouldConfigure && mLatestSurfaceRequest != null if (mLatestSurfaceRequest != null && !mLatestSurfaceRequest.isServiced()) {
&& !mLatestSurfaceRequest.isServiced()) {
configureInternal(mLatestSurfaceRequest, mVideoSourceTimebase); configureInternal(mLatestSurfaceRequest, mVideoSourceTimebase);
} }
} }
@ExecutedBy("mSequentialExecutor")
private void resetVideo() {
if (mVideoEncoder != null) {
Logger.d(TAG, "Releasing video encoder.");
tryReleaseVideoEncoder();
}
onResetVideo();
}
@ExecutedBy("mSequentialExecutor") @ExecutedBy("mSequentialExecutor")
private int internalAudioStateToAudioStatsState(@NonNull AudioState audioState) { private int internalAudioStateToAudioStatsState(@NonNull AudioState audioState) {
switch (audioState) { switch (audioState) {
@ -2178,9 +2063,7 @@ public final class SucklessRecorder implements VideoOutput {
mRecordingStopError = ERROR_UNKNOWN; mRecordingStopError = ERROR_UNKNOWN;
mRecordingStopErrorCause = null; mRecordingStopErrorCause = null;
mAudioErrorCause = null; mAudioErrorCause = null;
mAudioAmplitude = AUDIO_AMPLITUDE_NONE;
clearPendingAudioRingBuffer(); clearPendingAudioRingBuffer();
setInProgressTransformationInfo(null);
switch (mAudioState) { switch (mAudioState) {
case IDLING: case IDLING:
@ -2363,7 +2246,7 @@ public final class SucklessRecorder implements VideoOutput {
startRecordingPaused = true; startRecordingPaused = true;
// Fall-through // Fall-through
case PENDING_RECORDING: case PENDING_RECORDING:
if (mActiveRecordingRecord != null || mNeedsResetBeforeNextStart) { if (mActiveRecordingRecord != null || mNeedsReset) {
// Active recording is still finalizing or the Recorder is expected to be // Active recording is still finalizing or the Recorder is expected to be
// reset. Pending recording will be serviced in onRecordingFinalized() or // reset. Pending recording will be serviced in onRecordingFinalized() or
// in onReset(). // in onReset().
@ -2478,8 +2361,7 @@ public final class SucklessRecorder implements VideoOutput {
@NonNull @NonNull
RecordingStats getInProgressRecordingStats() { RecordingStats getInProgressRecordingStats() {
return RecordingStats.of(mRecordingDurationNs, mRecordingBytes, return RecordingStats.of(mRecordingDurationNs, mRecordingBytes,
AudioStats.of(internalAudioStateToAudioStatsState(mAudioState), mAudioErrorCause, AudioStats.of(internalAudioStateToAudioStatsState(mAudioState), mAudioErrorCause));
mAudioAmplitude));
} }
@SuppressWarnings("WeakerAccess") /* synthetic accessor */ @SuppressWarnings("WeakerAccess") /* synthetic accessor */
@ -2533,7 +2415,7 @@ public final class SucklessRecorder implements VideoOutput {
if (streamState == null) { if (streamState == null) {
streamState = internalStateToStreamState(mState); streamState = internalStateToStreamState(mState);
} }
mStreamInfo.setState(StreamInfo.of(mStreamId, streamState, mInProgressTransformationInfo)); mStreamInfo.setState(StreamInfo.of(mStreamId, streamState));
} }
@ExecutedBy("mSequentialExecutor") @ExecutedBy("mSequentialExecutor")
@ -2555,20 +2437,7 @@ public final class SucklessRecorder implements VideoOutput {
} }
Logger.d(TAG, "Transitioning streamId: " + mStreamId + " --> " + streamId); Logger.d(TAG, "Transitioning streamId: " + mStreamId + " --> " + streamId);
mStreamId = streamId; mStreamId = streamId;
mStreamInfo.setState(StreamInfo.of(streamId, internalStateToStreamState(mState), mStreamInfo.setState(StreamInfo.of(streamId, internalStateToStreamState(mState)));
mInProgressTransformationInfo));
}
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
@ExecutedBy("mSequentialExecutor")
void setInProgressTransformationInfo(
@Nullable SurfaceRequest.TransformationInfo transformationInfo) {
Logger.d(TAG, "Update stream transformation info: " + transformationInfo);
mInProgressTransformationInfo = transformationInfo;
synchronized (mLock) {
mStreamInfo.setState(StreamInfo.of(mStreamId, internalStateToStreamState(mState),
transformationInfo));
}
} }
/** /**
@ -2591,8 +2460,8 @@ public final class SucklessRecorder implements VideoOutput {
if (mNonPendingState != state) { if (mNonPendingState != state) {
mNonPendingState = state; mNonPendingState = state;
mStreamInfo.setState(StreamInfo.of(mStreamId, internalStateToStreamState(state), mStreamInfo.setState(
mInProgressTransformationInfo)); StreamInfo.of(mStreamId, internalStateToStreamState(state)));
} }
} }
@ -2640,21 +2509,6 @@ public final class SucklessRecorder implements VideoOutput {
return defaultMuxerFormat; return defaultMuxerFormat;
} }
/**
* Returns the {@link VideoCapabilities} of Recorder with respect to input camera information.
*
* <p>{@link VideoCapabilities} provides methods to query supported dynamic ranges and
* qualities. This information can be used for things like checking if HDR is supported for
* configuring VideoCapture to record HDR video.
*
* @param cameraInfo info about the camera.
* @return VideoCapabilities with respect to the input camera info.
*/
@NonNull
public static VideoCapabilities getVideoCapabilities(@NonNull CameraInfo cameraInfo) {
return RecorderVideoCapabilities.from(cameraInfo);
}
@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java @RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
@AutoValue @AutoValue
abstract static class RecordingRecord implements AutoCloseable { abstract static class RecordingRecord implements AutoCloseable {
@ -2683,7 +2537,6 @@ public final class SucklessRecorder implements VideoOutput {
pendingRecording.getListenerExecutor(), pendingRecording.getListenerExecutor(),
pendingRecording.getEventListener(), pendingRecording.getEventListener(),
pendingRecording.isAudioEnabled(), pendingRecording.isAudioEnabled(),
pendingRecording.isPersistent(),
recordingId recordingId
); );
} }
@ -2699,8 +2552,6 @@ public final class SucklessRecorder implements VideoOutput {
abstract boolean hasAudioEnabled(); abstract boolean hasAudioEnabled();
abstract boolean isPersistent();
abstract long getRecordingId(); abstract long getRecordingId();
/** /**
@ -2931,12 +2782,7 @@ public final class SucklessRecorder implements VideoOutput {
throw new AssertionError("One-time media muxer creation has already occurred for" throw new AssertionError("One-time media muxer creation has already occurred for"
+ " recording " + this); + " recording " + this);
} }
return mediaMuxerSupplier.get(muxerOutputFormat, outputUriCreatedCallback);
try {
return mediaMuxerSupplier.get(muxerOutputFormat, outputUriCreatedCallback);
} catch (RuntimeException e) {
throw new IOException("Failed to create MediaMuxer by " + e, e);
}
} }
/** /**

View File

@ -21,7 +21,6 @@ import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi; import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo; import androidx.annotation.RestrictTo;
import androidx.camera.core.impl.utils.CloseGuardHelper; import androidx.camera.core.impl.utils.CloseGuardHelper;
@ -57,15 +56,13 @@ public final class SucklessRecording implements AutoCloseable {
private final SucklessRecorder mRecorder; private final SucklessRecorder mRecorder;
private final long mRecordingId; private final long mRecordingId;
private final OutputOptions mOutputOptions; private final OutputOptions mOutputOptions;
private final boolean mIsPersistent;
private final CloseGuardHelper mCloseGuard = CloseGuardHelper.create(); private final CloseGuardHelper mCloseGuard = CloseGuardHelper.create();
SucklessRecording(@NonNull SucklessRecorder recorder, long recordingId, @NonNull OutputOptions options, SucklessRecording(@NonNull SucklessRecorder recorder, long recordingId, @NonNull OutputOptions options,
boolean isPersistent, boolean finalizedOnCreation) { boolean finalizedOnCreation) {
mRecorder = recorder; mRecorder = recorder;
mRecordingId = recordingId; mRecordingId = recordingId;
mOutputOptions = options; mOutputOptions = options;
mIsPersistent = isPersistent;
if (finalizedOnCreation) { if (finalizedOnCreation) {
mIsClosed.set(true); mIsClosed.set(true);
@ -86,7 +83,6 @@ public final class SucklessRecording implements AutoCloseable {
return new SucklessRecording(pendingRecording.getRecorder(), return new SucklessRecording(pendingRecording.getRecorder(),
recordingId, recordingId,
pendingRecording.getOutputOptions(), pendingRecording.getOutputOptions(),
pendingRecording.isPersistent(),
/*finalizedOnCreation=*/false); /*finalizedOnCreation=*/false);
} }
@ -107,7 +103,6 @@ public final class SucklessRecording implements AutoCloseable {
return new SucklessRecording(pendingRecording.getRecorder(), return new SucklessRecording(pendingRecording.getRecorder(),
recordingId, recordingId,
pendingRecording.getOutputOptions(), pendingRecording.getOutputOptions(),
pendingRecording.isPersistent(),
/*finalizedOnCreation=*/true); /*finalizedOnCreation=*/true);
} }
@ -116,20 +111,6 @@ public final class SucklessRecording implements AutoCloseable {
return mOutputOptions; 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. * Pauses the current recording if active.
* *
@ -215,7 +196,11 @@ public final class SucklessRecording implements AutoCloseable {
*/ */
@Override @Override
public void close() { public void close() {
stopWithError(VideoRecordEvent.Finalize.ERROR_NONE, /*errorCause=*/ null); mCloseGuard.close();
if (mIsClosed.getAndSet(true)) {
return;
}
mRecorder.stop(this);
} }
@Override @Override
@ -223,8 +208,7 @@ public final class SucklessRecording implements AutoCloseable {
protected void finalize() throws Throwable { protected void finalize() throws Throwable {
try { try {
mCloseGuard.warnIfOpen(); mCloseGuard.warnIfOpen();
stopWithError(VideoRecordEvent.Finalize.ERROR_RECORDING_GARBAGE_COLLECTED, stop();
new RuntimeException("Recording stopped due to being garbage collected."));
} finally { } finally {
super.finalize(); super.finalize();
} }
@ -250,14 +234,5 @@ public final class SucklessRecording implements AutoCloseable {
public boolean isClosed() { public boolean isClosed() {
return mIsClosed.get(); 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);
}
} }

View File

@ -35,7 +35,6 @@ import android.media.MediaCodec.BufferInfo;
import android.media.MediaCodecInfo; import android.media.MediaCodecInfo;
import android.media.MediaFormat; import android.media.MediaFormat;
import android.os.Bundle; import android.os.Bundle;
import android.os.SystemClock;
import android.util.Range; import android.util.Range;
import android.view.Surface; import android.view.Surface;
@ -1055,7 +1054,6 @@ public class SucklessEncoderImpl implements Encoder {
if (mIsVideoEncoder) { if (mIsVideoEncoder) {
Timebase inputTimebase; Timebase inputTimebase;
if (DeviceQuirks.get(CameraUseInconsistentTimebaseQuirk.class) != null) { if (DeviceQuirks.get(CameraUseInconsistentTimebaseQuirk.class) != null) {
Logger.w(mTag, "CameraUseInconsistentTimebaseQuirk is enabled");
inputTimebase = null; inputTimebase = null;
} else { } else {
inputTimebase = mInputTimebase; inputTimebase = mInputTimebase;
@ -1067,7 +1065,7 @@ public class SucklessEncoderImpl implements Encoder {
} }
@Override @Override
public void onInputBufferAvailable(@NonNull MediaCodec mediaCodec, int index) { public void onInputBufferAvailable(MediaCodec mediaCodec, int index) {
mEncoderExecutor.execute(() -> { mEncoderExecutor.execute(() -> {
if (mStopped) { if (mStopped) {
Logger.w(mTag, "Receives input frame after codec is reset."); Logger.w(mTag, "Receives input frame after codec is reset.");
@ -1133,15 +1131,6 @@ public class SucklessEncoderImpl implements Encoder {
if (checkBufferInfo(bufferInfo)) { if (checkBufferInfo(bufferInfo)) {
if (!mHasFirstData) { if (!mHasFirstData) {
mHasFirstData = true; mHasFirstData = true;
// Only print the first data to avoid flooding the log.
Logger.d(mTag,
"data timestampUs = " + bufferInfo.presentationTimeUs
+ ", data timebase = " + mInputTimebase
+ ", current system uptimeMs = "
+ SystemClock.uptimeMillis()
+ ", current system realtimeMs = "
+ SystemClock.elapsedRealtime()
);
} }
BufferInfo outBufferInfo = resolveOutputBufferInfo(bufferInfo); BufferInfo outBufferInfo = resolveOutputBufferInfo(bufferInfo);
mLastSentAdjustedTimeUs = outBufferInfo.presentationTimeUs; mLastSentAdjustedTimeUs = outBufferInfo.presentationTimeUs;

View File

@ -5,7 +5,7 @@ Create the `new` folder if needed:
mkdir -p new mkdir -p new
``` ```
Put the new CameraX files from upstream (`androidx.camera.video.Recorder`, `androidx.camera.video.Recording`, `androidx.camera.video.PendingRecording` and `androidx.camera.video.internal.encoder.EncoderImpl`) in the `new` folder. Put new CameraX files from upstream in the `new` folder.
Perform the 3 way merge: Perform the 3 way merge:
``` ```

View File

@ -35,7 +35,6 @@ import android.media.MediaCodec.BufferInfo;
import android.media.MediaCodecInfo; import android.media.MediaCodecInfo;
import android.media.MediaFormat; import android.media.MediaFormat;
import android.os.Bundle; import android.os.Bundle;
import android.os.SystemClock;
import android.util.Range; import android.util.Range;
import android.view.Surface; import android.view.Surface;
@ -1054,7 +1053,6 @@ public class EncoderImpl implements Encoder {
if (mIsVideoEncoder) { if (mIsVideoEncoder) {
Timebase inputTimebase; Timebase inputTimebase;
if (DeviceQuirks.get(CameraUseInconsistentTimebaseQuirk.class) != null) { if (DeviceQuirks.get(CameraUseInconsistentTimebaseQuirk.class) != null) {
Logger.w(mTag, "CameraUseInconsistentTimebaseQuirk is enabled");
inputTimebase = null; inputTimebase = null;
} else { } else {
inputTimebase = mInputTimebase; inputTimebase = mInputTimebase;
@ -1066,7 +1064,7 @@ public class EncoderImpl implements Encoder {
} }
@Override @Override
public void onInputBufferAvailable(@NonNull MediaCodec mediaCodec, int index) { public void onInputBufferAvailable(MediaCodec mediaCodec, int index) {
mEncoderExecutor.execute(() -> { mEncoderExecutor.execute(() -> {
if (mStopped) { if (mStopped) {
Logger.w(mTag, "Receives input frame after codec is reset."); Logger.w(mTag, "Receives input frame after codec is reset.");
@ -1132,15 +1130,6 @@ public class EncoderImpl implements Encoder {
if (checkBufferInfo(bufferInfo)) { if (checkBufferInfo(bufferInfo)) {
if (!mHasFirstData) { if (!mHasFirstData) {
mHasFirstData = true; mHasFirstData = true;
// Only print the first data to avoid flooding the log.
Logger.d(mTag,
"data timestampUs = " + bufferInfo.presentationTimeUs
+ ", data timebase = " + mInputTimebase
+ ", current system uptimeMs = "
+ SystemClock.uptimeMillis()
+ ", current system realtimeMs = "
+ SystemClock.elapsedRealtime()
);
} }
BufferInfo outBufferInfo = resolveOutputBufferInfo(bufferInfo); BufferInfo outBufferInfo = resolveOutputBufferInfo(bufferInfo);
mLastSentAdjustedTimeUs = outBufferInfo.presentationTimeUs; mLastSentAdjustedTimeUs = outBufferInfo.presentationTimeUs;

View File

@ -56,8 +56,8 @@ public final class PendingRecording {
private final OutputOptions mOutputOptions; private final OutputOptions mOutputOptions;
private Consumer<VideoRecordEvent> mEventListener; private Consumer<VideoRecordEvent> mEventListener;
private Executor mListenerExecutor; private Executor mListenerExecutor;
private boolean mAudioEnabled = false; private boolean mAudioEnabled = false;
private boolean mIsPersistent = false;
PendingRecording(@NonNull Context context, @NonNull Recorder recorder, PendingRecording(@NonNull Context context, @NonNull Recorder recorder,
@NonNull OutputOptions options) { @NonNull OutputOptions options) {
@ -102,10 +102,6 @@ public final class PendingRecording {
return mAudioEnabled; return mAudioEnabled;
} }
boolean isPersistent() {
return mIsPersistent;
}
/** /**
* Enables audio to be recorded for this recording. * Enables audio to be recorded for this recording.
* *
@ -141,69 +137,6 @@ public final class PendingRecording {
return this; 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 PendingRecording asPersistentRecording() {
mIsPersistent = true;
return this;
}
/** /**
* Starts the recording, making it an active recording. * Starts the recording, making it an active recording.
* *
@ -224,13 +157,7 @@ public final class PendingRecording {
* *
* <p>If the returned {@link Recording} is garbage collected, the recording will be * <p>If the returned {@link Recording} is garbage collected, the recording will be
* automatically stopped. A reference to the active recording must be maintained as long as * 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 * the recording needs to be active.
* {@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 * @throws IllegalStateException if the associated Recorder currently has an unfinished
* active recording. * active recording.

View File

@ -16,7 +16,6 @@
package androidx.camera.video; 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_DURATION_LIMIT_REACHED;
import static androidx.camera.video.VideoRecordEvent.Finalize.ERROR_ENCODING_FAILED; import static androidx.camera.video.VideoRecordEvent.Finalize.ERROR_ENCODING_FAILED;
import static androidx.camera.video.VideoRecordEvent.Finalize.ERROR_FILE_SIZE_LIMIT_REACHED; import static androidx.camera.video.VideoRecordEvent.Finalize.ERROR_FILE_SIZE_LIMIT_REACHED;
@ -58,8 +57,6 @@ import androidx.annotation.RequiresPermission;
import androidx.annotation.RestrictTo; import androidx.annotation.RestrictTo;
import androidx.annotation.VisibleForTesting; import androidx.annotation.VisibleForTesting;
import androidx.camera.core.AspectRatio; import androidx.camera.core.AspectRatio;
import androidx.camera.core.CameraInfo;
import androidx.camera.core.DynamicRange;
import androidx.camera.core.Logger; import androidx.camera.core.Logger;
import androidx.camera.core.SurfaceRequest; import androidx.camera.core.SurfaceRequest;
import androidx.camera.core.impl.MutableStateObservable; import androidx.camera.core.impl.MutableStateObservable;
@ -82,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.DeactivateEncoderSurfaceBeforeStopEncoderQuirk;
import androidx.camera.video.internal.compat.quirk.DeviceQuirks; import androidx.camera.video.internal.compat.quirk.DeviceQuirks;
import androidx.camera.video.internal.compat.quirk.EncoderNotUsePersistentInputSurfaceQuirk; import androidx.camera.video.internal.compat.quirk.EncoderNotUsePersistentInputSurfaceQuirk;
import androidx.camera.video.internal.config.AudioMimeInfo; import androidx.camera.video.internal.config.MimeInfo;
import androidx.camera.video.internal.encoder.AudioEncoderConfig; import androidx.camera.video.internal.encoder.AudioEncoderConfig;
import androidx.camera.video.internal.encoder.BufferCopiedEncodedData; import androidx.camera.video.internal.encoder.BufferCopiedEncodedData;
import androidx.camera.video.internal.encoder.EncodeException; import androidx.camera.video.internal.encoder.EncodeException;
@ -346,14 +343,10 @@ public final class Recorder implements VideoOutput {
//////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////
// Members only accessed on mSequentialExecutor // // Members only accessed on mSequentialExecutor //
//////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////
@SuppressWarnings("WeakerAccess") /* synthetic accessor */ private RecordingRecord mInProgressRecording = null;
RecordingRecord mInProgressRecording = null;
@SuppressWarnings("WeakerAccess") /* synthetic accessor */ @SuppressWarnings("WeakerAccess") /* synthetic accessor */
boolean mInProgressRecordingStopping = false; boolean mInProgressRecordingStopping = false;
@Nullable private SurfaceRequest.TransformationInfo mSurfaceTransformationInfo = null;
private SurfaceRequest.TransformationInfo mInProgressTransformationInfo = null;
@Nullable
private SurfaceRequest.TransformationInfo mSourceTransformationInfo = null;
private VideoValidatedEncoderProfilesProxy mResolvedEncoderProfiles = null; private VideoValidatedEncoderProfilesProxy mResolvedEncoderProfiles = null;
@SuppressWarnings("WeakerAccess") /* synthetic accessor */ @SuppressWarnings("WeakerAccess") /* synthetic accessor */
final List<ListenableFuture<Void>> mEncodingFutures = new ArrayList<>(); final List<ListenableFuture<Void>> mEncodingFutures = new ArrayList<>();
@ -434,15 +427,13 @@ public final class Recorder implements VideoOutput {
@SuppressWarnings("WeakerAccess") /* synthetic accessor */ @SuppressWarnings("WeakerAccess") /* synthetic accessor */
ScheduledFuture<?> mSourceNonStreamingTimeout = null; ScheduledFuture<?> mSourceNonStreamingTimeout = null;
// The Recorder has to be reset first before being configured again. // The Recorder has to be reset first before being configured again.
private boolean mNeedsResetBeforeNextStart = false; private boolean mNeedsReset = false;
@NonNull @NonNull
@SuppressWarnings("WeakerAccess") /* synthetic accessor */ @SuppressWarnings("WeakerAccess") /* synthetic accessor */
VideoEncoderSession mVideoEncoderSession; VideoEncoderSession mVideoEncoderSession;
@Nullable @Nullable
@SuppressWarnings("WeakerAccess") /* synthetic accessor */ @SuppressWarnings("WeakerAccess") /* synthetic accessor */
VideoEncoderSession mVideoEncoderSessionToRelease = null; VideoEncoderSession mVideoEncoderSessionToRelease = null;
double mAudioAmplitude = 0;
private boolean mShouldSendResumeEvent = false;
//--------------------------------------------------------------------------------------------// //--------------------------------------------------------------------------------------------//
Recorder(@Nullable Executor executor, @NonNull MediaSpec mediaSpec, Recorder(@Nullable Executor executor, @NonNull MediaSpec mediaSpec,
@ -499,13 +490,6 @@ public final class Recorder implements VideoOutput {
mSequentialExecutor.execute(() -> onSourceStateChangedInternal(newState)); mSequentialExecutor.execute(() -> onSourceStateChangedInternal(newState));
} }
@RestrictTo(RestrictTo.Scope.LIBRARY)
@Override
@NonNull
public VideoCapabilities getMediaCapabilities(@NonNull CameraInfo cameraInfo) {
return getVideoCapabilities(cameraInfo);
}
/** /**
* Prepares a recording that will be saved to a {@link File}. * Prepares a recording that will be saved to a {@link File}.
* *
@ -862,8 +846,7 @@ public final class Recorder implements VideoOutput {
} }
} }
void stop(@NonNull Recording activeRecording, @VideoRecordError int error, void stop(@NonNull Recording activeRecording) {
@Nullable Throwable errorCause) {
RecordingRecord pendingRecordingToFinalize = null; RecordingRecord pendingRecordingToFinalize = null;
synchronized (mLock) { synchronized (mLock) {
if (!isSameRecording(activeRecording, mPendingRecordingRecord) && !isSameRecording( if (!isSameRecording(activeRecording, mPendingRecordingRecord) && !isSameRecording(
@ -908,7 +891,7 @@ public final class Recorder implements VideoOutput {
long explicitlyStopTimeUs = TimeUnit.NANOSECONDS.toMicros(System.nanoTime()); long explicitlyStopTimeUs = TimeUnit.NANOSECONDS.toMicros(System.nanoTime());
RecordingRecord finalActiveRecordingRecord = mActiveRecordingRecord; RecordingRecord finalActiveRecordingRecord = mActiveRecordingRecord;
mSequentialExecutor.execute(() -> stopInternal(finalActiveRecordingRecord, mSequentialExecutor.execute(() -> stopInternal(finalActiveRecordingRecord,
explicitlyStopTimeUs, error, errorCause)); explicitlyStopTimeUs, ERROR_NONE, null));
break; break;
case ERROR: case ERROR:
// In an error state, the recording will already be finalized. Treat as a // In an error state, the recording will already be finalized. Treat as a
@ -918,13 +901,9 @@ public final class Recorder implements VideoOutput {
} }
if (pendingRecordingToFinalize != null) { 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, finalizePendingRecording(pendingRecordingToFinalize, ERROR_NO_VALID_DATA,
new RuntimeException("Recording was stopped before any data could be " new RuntimeException("Recording was stopped before any data could be "
+ "produced.", errorCause)); + "produced."));
} }
} }
@ -954,8 +933,7 @@ public final class Recorder implements VideoOutput {
recordingToFinalize.getOutputOptions(), recordingToFinalize.getOutputOptions(),
RecordingStats.of(/*duration=*/0L, RecordingStats.of(/*duration=*/0L,
/*bytes=*/0L, /*bytes=*/0L,
AudioStats.of(AudioStats.AUDIO_STATE_DISABLED, mAudioErrorCause, AudioStats.of(AudioStats.AUDIO_STATE_DISABLED, mAudioErrorCause)),
AUDIO_AMPLITUDE_NONE)),
OutputResults.of(Uri.EMPTY), OutputResults.of(Uri.EMPTY),
error, error,
cause)); cause));
@ -986,15 +964,14 @@ public final class Recorder implements VideoOutput {
// If we're inactive and have no active surface, we'll reset the encoder directly. // 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 // Otherwise, we'll wait for the active surface's surface request listener to
// reset the encoder. // reset the encoder.
requestReset(ERROR_SOURCE_INACTIVE, null, false); requestReset(ERROR_SOURCE_INACTIVE, null);
} else { } else {
// The source becomes inactive, the incoming new surface request has to be cached // 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 // and be serviced after the Recorder is reset when receiving the previous
// surface request complete callback. // surface request complete callback.
mNeedsResetBeforeNextStart = true; mNeedsReset = true;
if (mInProgressRecording != null && !mInProgressRecording.isPersistent()) { if (mInProgressRecording != null) {
// Stop the in progress recording with "source inactive" error if it's not a // Stop any in progress recording with "source inactive" error
// persistent recording.
onInProgressRecordingInternalError(mInProgressRecording, ERROR_SOURCE_INACTIVE, onInProgressRecordingInternalError(mInProgressRecording, ERROR_SOURCE_INACTIVE,
null); null);
} }
@ -1018,8 +995,7 @@ public final class Recorder implements VideoOutput {
* the surface request complete callback first. * the surface request complete callback first.
*/ */
@ExecutedBy("mSequentialExecutor") @ExecutedBy("mSequentialExecutor")
void requestReset(@VideoRecordError int errorCode, @Nullable Throwable errorCause, void requestReset(@VideoRecordError int errorCode, @Nullable Throwable errorCause) {
boolean videoOnly) {
boolean shouldReset = false; boolean shouldReset = false;
boolean shouldStop = false; boolean shouldStop = false;
synchronized (mLock) { synchronized (mLock) {
@ -1041,22 +1017,14 @@ public final class Recorder implements VideoOutput {
case PAUSED: case PAUSED:
// Fall-through // Fall-through
case RECORDING: case RECORDING:
Preconditions.checkState(mInProgressRecording != null, "In-progress recording"
+ " shouldn't be null when in state " + mState);
if (mActiveRecordingRecord != mInProgressRecording) { if (mActiveRecordingRecord != mInProgressRecording) {
throw new AssertionError("In-progress recording does not match the active" throw new AssertionError("In-progress recording does not match the active"
+ " recording. Unable to reset encoder."); + " recording. Unable to reset encoder.");
} }
// If there's an active persistent recording, reset the Recorder directly. // If there's an active recording, stop it first then release the resources
// Otherwise, stop the recording first then release the Recorder at // at onRecordingFinalized().
// onRecordingFinalized(). shouldStop = true;
if (isPersistentRecordingInProgress()) { // Fall-through
shouldReset = true;
} else {
shouldStop = true;
setState(State.RESETTING);
}
break;
case STOPPING: case STOPPING:
// Already stopping. Set state to RESETTING so resources will be released once // Already stopping. Set state to RESETTING so resources will be released once
// onRecordingFinalized() runs. // onRecordingFinalized() runs.
@ -1071,17 +1039,14 @@ public final class Recorder implements VideoOutput {
// These calls must not be posted to the executor to ensure they are executed inline on // 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. // the sequential executor and the state changes above are correctly handled.
if (shouldReset) { if (shouldReset) {
if (videoOnly) { reset();
resetVideo();
} else {
reset();
}
} else if (shouldStop) { } else if (shouldStop) {
stopInternal(mInProgressRecording, Encoder.NO_TIMESTAMP, errorCode, errorCause); stopInternal(mInProgressRecording, Encoder.NO_TIMESTAMP, errorCode, errorCause);
} }
} }
@ExecutedBy("mSequentialExecutor") @ExecutedBy("mSequentialExecutor")
private void configureInternal(@NonNull SurfaceRequest surfaceRequest, private void configureInternal(@NonNull SurfaceRequest surfaceRequest,
@NonNull Timebase videoSourceTimebase) { @NonNull Timebase videoSourceTimebase) {
if (surfaceRequest.isServiced()) { if (surfaceRequest.isServiced()) {
@ -1089,19 +1054,16 @@ public final class Recorder implements VideoOutput {
return; return;
} }
surfaceRequest.setTransformationInfoListener(mSequentialExecutor, surfaceRequest.setTransformationInfoListener(mSequentialExecutor,
(transformationInfo) -> mSourceTransformationInfo = transformationInfo); (transformationInfo) -> mSurfaceTransformationInfo = transformationInfo);
Size surfaceSize = surfaceRequest.getResolution(); Size surfaceSize = surfaceRequest.getResolution();
// Fetch and cache nearest encoder profiles, if one exists. // Fetch and cache nearest encoder profiles, if one exists.
DynamicRange dynamicRange = surfaceRequest.getDynamicRange(); LegacyVideoCapabilities capabilities =
VideoCapabilities capabilities = getVideoCapabilities( LegacyVideoCapabilities.from(surfaceRequest.getCamera().getCameraInfo());
surfaceRequest.getCamera().getCameraInfo()); Quality highestSupportedQuality = capabilities.findHighestSupportedQualityFor(surfaceSize);
Quality highestSupportedQuality = capabilities.findHighestSupportedQualityFor(surfaceSize,
dynamicRange);
Logger.d(TAG, "Using supported quality of " + highestSupportedQuality Logger.d(TAG, "Using supported quality of " + highestSupportedQuality
+ " for surface size " + surfaceSize); + " for surface size " + surfaceSize);
if (highestSupportedQuality != Quality.NONE) { if (highestSupportedQuality != Quality.NONE) {
mResolvedEncoderProfiles = capabilities.getProfiles(highestSupportedQuality, mResolvedEncoderProfiles = capabilities.getProfiles(highestSupportedQuality);
dynamicRange);
if (mResolvedEncoderProfiles == null) { if (mResolvedEncoderProfiles == null) {
throw new AssertionError("Camera advertised available quality but did not " throw new AssertionError("Camera advertised available quality but did not "
+ "produce EncoderProfiles for advertised quality."); + "produce EncoderProfiles for advertised quality.");
@ -1114,14 +1076,9 @@ public final class Recorder implements VideoOutput {
@ExecutedBy("mSequentialExecutor") @ExecutedBy("mSequentialExecutor")
private void setupVideo(@NonNull SurfaceRequest request, @NonNull Timebase timebase) { private void setupVideo(@NonNull SurfaceRequest request, @NonNull Timebase timebase) {
safeToCloseVideoEncoder().addListener(() -> { safeToCloseVideoEncoder().addListener(() -> {
if (request.isServiced() if (request.isServiced() || mVideoEncoderSession.isConfiguredSurfaceRequest(request)) {
|| (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: " 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; return;
} }
VideoEncoderSession videoEncoderSession = VideoEncoderSession videoEncoderSession =
@ -1153,12 +1110,6 @@ public final class Recorder implements VideoOutput {
}, mSequentialExecutor); }, mSequentialExecutor);
} }
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
@ExecutedBy("mSequentialExecutor")
boolean isPersistentRecordingInProgress() {
return mInProgressRecording != null && mInProgressRecording.isPersistent();
}
@NonNull @NonNull
@ExecutedBy("mSequentialExecutor") @ExecutedBy("mSequentialExecutor")
private ListenableFuture<Void> safeToCloseVideoEncoder() { private ListenableFuture<Void> safeToCloseVideoEncoder() {
@ -1194,9 +1145,7 @@ public final class Recorder implements VideoOutput {
mVideoEncoderSessionToRelease = videoEncoderSession; mVideoEncoderSessionToRelease = videoEncoderSession;
setLatestSurface(null); setLatestSurface(null);
// Only reset video if the in-progress recording is persistent. requestReset(ERROR_SOURCE_INACTIVE, null);
requestReset(ERROR_SOURCE_INACTIVE, null,
isPersistentRecordingInProgress());
} }
@Override @Override
@ -1211,14 +1160,17 @@ public final class Recorder implements VideoOutput {
void onConfigured() { void onConfigured() {
RecordingRecord recordingToStart = null; RecordingRecord recordingToStart = null;
RecordingRecord pendingRecordingToFinalize = null; RecordingRecord pendingRecordingToFinalize = null;
boolean continuePersistentRecording = false;
@VideoRecordError int error = ERROR_NONE; @VideoRecordError int error = ERROR_NONE;
Throwable errorCause = null; Throwable errorCause = null;
boolean recordingPaused = false; boolean startRecordingPaused = false;
synchronized (mLock) { synchronized (mLock) {
switch (mState) { switch (mState) {
case IDLING: case IDLING:
// Fall-through // Fall-through
case RECORDING:
// Fall-through
case PAUSED:
// Fall-through
case RESETTING: case RESETTING:
throw new AssertionError( throw new AssertionError(
"Incorrectly invoke onConfigured() in state " + mState); "Incorrectly invoke onConfigured() in state " + mState);
@ -1228,15 +1180,6 @@ public final class Recorder implements VideoOutput {
+ "STOPPING state when it's not waiting for a new surface."); + "STOPPING state when it's not waiting for a new surface.");
} }
break; 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: case CONFIGURING:
setState(State.IDLING); setState(State.IDLING);
break; break;
@ -1245,7 +1188,7 @@ public final class Recorder implements VideoOutput {
"onConfigured() was invoked when the Recorder had encountered error"); "onConfigured() was invoked when the Recorder had encountered error");
break; break;
case PENDING_PAUSED: case PENDING_PAUSED:
recordingPaused = true; startRecordingPaused = true;
// Fall through // Fall through
case PENDING_RECORDING: case PENDING_RECORDING:
if (mActiveRecordingRecord != null) { if (mActiveRecordingRecord != null) {
@ -1266,21 +1209,9 @@ public final class Recorder implements VideoOutput {
} }
} }
if (continuePersistentRecording) { if (recordingToStart != null) {
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). // Start new active recording inline on sequential executor (but unlocked).
startRecording(recordingToStart, recordingPaused); startRecording(recordingToStart, startRecordingPaused);
} else if (pendingRecordingToFinalize != null) { } else if (pendingRecordingToFinalize != null) {
finalizePendingRecording(pendingRecordingToFinalize, error, errorCause); finalizePendingRecording(pendingRecordingToFinalize, error, errorCause);
} }
@ -1321,7 +1252,7 @@ public final class Recorder implements VideoOutput {
throws AudioSourceAccessException, InvalidConfigException { throws AudioSourceAccessException, InvalidConfigException {
MediaSpec mediaSpec = getObservableData(mMediaSpec); MediaSpec mediaSpec = getObservableData(mMediaSpec);
// Resolve the audio mime info // Resolve the audio mime info
AudioMimeInfo audioMimeInfo = resolveAudioMimeInfo(mediaSpec, mResolvedEncoderProfiles); MimeInfo audioMimeInfo = resolveAudioMimeInfo(mediaSpec, mResolvedEncoderProfiles);
Timebase audioSourceTimebase = Timebase.UPTIME; Timebase audioSourceTimebase = Timebase.UPTIME;
// Select and create the audio source // Select and create the audio source
@ -1472,10 +1403,8 @@ public final class Recorder implements VideoOutput {
return; return;
} }
SurfaceRequest.TransformationInfo transformationInfo = mSourceTransformationInfo; if (mSurfaceTransformationInfo != null) {
if (transformationInfo != null) { mediaMuxer.setOrientationHint(mSurfaceTransformationInfo.getRotationDegrees());
setInProgressTransformationInfo(transformationInfo);
mediaMuxer.setOrientationHint(transformationInfo.getRotationDegrees());
} }
Location location = recordingToStart.getOutputOptions().getLocation(); Location location = recordingToStart.getOutputOptions().getLocation();
if (location != null) { if (location != null) {
@ -1578,9 +1507,7 @@ public final class Recorder implements VideoOutput {
"The Recorder doesn't support recording with audio"); "The Recorder doesn't support recording with audio");
} }
try { try {
if (!mInProgressRecording.isPersistent() || mAudioEncoder == null) { setupAudio(recordingToStart);
setupAudio(recordingToStart);
}
setAudioState(AudioState.ENABLED); setAudioState(AudioState.ENABLED);
} catch (AudioSourceAccessException | InvalidConfigException e) { } catch (AudioSourceAccessException | InvalidConfigException e) {
Logger.e(TAG, "Unable to create audio resource with error: ", e); Logger.e(TAG, "Unable to create audio resource with error: ", e);
@ -1597,7 +1524,7 @@ public final class Recorder implements VideoOutput {
break; break;
} }
updateEncoderCallbacks(recordingToStart, false); initEncoderAndAudioSourceCallbacks(recordingToStart);
if (isAudioEnabled()) { if (isAudioEnabled()) {
mAudioSource.start(recordingToStart.isMuted()); mAudioSource.start(recordingToStart.isMuted());
mAudioEncoder.start(); mAudioEncoder.start();
@ -1610,17 +1537,7 @@ public final class Recorder implements VideoOutput {
} }
@ExecutedBy("mSequentialExecutor") @ExecutedBy("mSequentialExecutor")
private void updateEncoderCallbacks(@NonNull RecordingRecord recordingToStart, private void initEncoderAndAudioSourceCallbacks(@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( mEncodingFutures.add(CallbackToFutureAdapter.getFuture(
completer -> { completer -> {
mVideoEncoder.setEncoderCallback(new EncoderCallback() { mVideoEncoder.setEncoderCallback(new EncoderCallback() {
@ -1716,7 +1633,7 @@ public final class Recorder implements VideoOutput {
return "videoEncodingFuture"; return "videoEncodingFuture";
})); }));
if (isAudioEnabled() && !videoOnly) { if (isAudioEnabled()) {
mEncodingFutures.add(CallbackToFutureAdapter.getFuture( mEncodingFutures.add(CallbackToFutureAdapter.getFuture(
completer -> { completer -> {
Consumer<Throwable> audioErrorConsumer = throwable -> { Consumer<Throwable> audioErrorConsumer = throwable -> {
@ -1756,11 +1673,6 @@ public final class Recorder implements VideoOutput {
audioErrorConsumer.accept(throwable); audioErrorConsumer.accept(throwable);
} }
} }
@Override
public void onAmplitudeValue(double maxAmplitude) {
mAudioAmplitude = maxAmplitude;
}
}); });
mAudioEncoder.setEncoderCallback(new EncoderCallback() { mAudioEncoder.setEncoderCallback(new EncoderCallback() {
@ -1847,16 +1759,12 @@ public final class Recorder implements VideoOutput {
@Override @Override
public void onFailure(@NonNull Throwable t) { public void onFailure(@NonNull Throwable t) {
Preconditions.checkState(mInProgressRecording != null, Logger.d(TAG, "Encodings end with error: " + t);
"In-progress recording shouldn't be null"); // If the media muxer hasn't been set up, assume the encoding fails
// If a persistent recording requires reconfiguring the video encoder, // because of no valid data has been produced.
// the previous encoder future has to be canceled without finalizing the finalizeInProgressRecording(
// in-progress recording. mMediaMuxer == null ? ERROR_NO_VALID_DATA : ERROR_ENCODING_FAILED,
if (!mInProgressRecording.isPersistent()) { t);
Logger.d(TAG, "Encodings end with error: " + t);
finalizeInProgressRecording(mMediaMuxer == null ? ERROR_NO_VALID_DATA
: ERROR_ENCODING_FAILED, t);
}
} }
}, },
// Can use direct executor since completers are always completed on sequential // Can use direct executor since completers are always completed on sequential
@ -1997,20 +1905,11 @@ public final class Recorder implements VideoOutput {
if (isAudioEnabled()) { if (isAudioEnabled()) {
mAudioEncoder.start(); mAudioEncoder.start();
} }
// If a persistent recording is resumed immediately after the VideoCapture is rebound mVideoEncoder.start();
// to a camera, it's possible that the encoder hasn't been created yet. Then the
// encoder will be started once it's initialized. So only start the encoder when it's mInProgressRecording.updateVideoRecordEvent(VideoRecordEvent.resume(
// not null. mInProgressRecording.getOutputOptions(),
if (mVideoEncoder != null) { getInProgressRecordingStats()));
mVideoEncoder.start();
mInProgressRecording.updateVideoRecordEvent(VideoRecordEvent.resume(
mInProgressRecording.getOutputOptions(),
getInProgressRecordingStats()));
} else {
// Instead sending here, send the Resume event once the encoder is initialized
// and started.
mShouldSendResumeEvent = true;
}
} }
} }
@ -2104,12 +2003,13 @@ public final class Recorder implements VideoOutput {
mAudioEncoder = null; mAudioEncoder = null;
mAudioOutputConfig = null; mAudioOutputConfig = null;
} }
tryReleaseVideoEncoder();
if (mAudioSource != null) { if (mAudioSource != null) {
releaseCurrentAudioSource(); releaseCurrentAudioSource();
} }
setAudioState(AudioState.INITIALIZING); setAudioState(AudioState.INITIALIZING);
resetVideo(); onReset();
} }
@SuppressWarnings("FutureReturnValueIgnored") @SuppressWarnings("FutureReturnValueIgnored")
@ -2131,8 +2031,7 @@ public final class Recorder implements VideoOutput {
} }
@ExecutedBy("mSequentialExecutor") @ExecutedBy("mSequentialExecutor")
private void onResetVideo() { private void onReset() {
boolean shouldConfigure = true;
synchronized (mLock) { synchronized (mLock) {
switch (mState) { switch (mState) {
case PENDING_PAUSED: case PENDING_PAUSED:
@ -2145,10 +2044,6 @@ public final class Recorder implements VideoOutput {
case PAUSED: case PAUSED:
// Fall-through // Fall-through
case RECORDING: case RECORDING:
if (isPersistentRecordingInProgress()) {
shouldConfigure = false;
break;
}
// Fall-through // Fall-through
case IDLING: case IDLING:
// Fall-through // Fall-through
@ -2163,24 +2058,14 @@ public final class Recorder implements VideoOutput {
} }
} }
mNeedsResetBeforeNextStart = false; mNeedsReset = false;
// If the latest surface request hasn't been serviced, use it to re-configure the Recorder. // If the latest surface request hasn't been serviced, use it to re-configure the Recorder.
if (shouldConfigure && mLatestSurfaceRequest != null if (mLatestSurfaceRequest != null && !mLatestSurfaceRequest.isServiced()) {
&& !mLatestSurfaceRequest.isServiced()) {
configureInternal(mLatestSurfaceRequest, mVideoSourceTimebase); configureInternal(mLatestSurfaceRequest, mVideoSourceTimebase);
} }
} }
@ExecutedBy("mSequentialExecutor")
private void resetVideo() {
if (mVideoEncoder != null) {
Logger.d(TAG, "Releasing video encoder.");
tryReleaseVideoEncoder();
}
onResetVideo();
}
@ExecutedBy("mSequentialExecutor") @ExecutedBy("mSequentialExecutor")
private int internalAudioStateToAudioStatsState(@NonNull AudioState audioState) { private int internalAudioStateToAudioStatsState(@NonNull AudioState audioState) {
switch (audioState) { switch (audioState) {
@ -2283,9 +2168,7 @@ public final class Recorder implements VideoOutput {
mRecordingStopError = ERROR_UNKNOWN; mRecordingStopError = ERROR_UNKNOWN;
mRecordingStopErrorCause = null; mRecordingStopErrorCause = null;
mAudioErrorCause = null; mAudioErrorCause = null;
mAudioAmplitude = AUDIO_AMPLITUDE_NONE;
clearPendingAudioRingBuffer(); clearPendingAudioRingBuffer();
setInProgressTransformationInfo(null);
switch (mAudioState) { switch (mAudioState) {
case IDLING: case IDLING:
@ -2468,7 +2351,7 @@ public final class Recorder implements VideoOutput {
startRecordingPaused = true; startRecordingPaused = true;
// Fall-through // Fall-through
case PENDING_RECORDING: case PENDING_RECORDING:
if (mActiveRecordingRecord != null || mNeedsResetBeforeNextStart) { if (mActiveRecordingRecord != null || mNeedsReset) {
// Active recording is still finalizing or the Recorder is expected to be // Active recording is still finalizing or the Recorder is expected to be
// reset. Pending recording will be serviced in onRecordingFinalized() or // reset. Pending recording will be serviced in onRecordingFinalized() or
// in onReset(). // in onReset().
@ -2583,8 +2466,7 @@ public final class Recorder implements VideoOutput {
@NonNull @NonNull
RecordingStats getInProgressRecordingStats() { RecordingStats getInProgressRecordingStats() {
return RecordingStats.of(mRecordingDurationNs, mRecordingBytes, return RecordingStats.of(mRecordingDurationNs, mRecordingBytes,
AudioStats.of(internalAudioStateToAudioStatsState(mAudioState), mAudioErrorCause, AudioStats.of(internalAudioStateToAudioStatsState(mAudioState), mAudioErrorCause));
mAudioAmplitude));
} }
@SuppressWarnings("WeakerAccess") /* synthetic accessor */ @SuppressWarnings("WeakerAccess") /* synthetic accessor */
@ -2638,7 +2520,7 @@ public final class Recorder implements VideoOutput {
if (streamState == null) { if (streamState == null) {
streamState = internalStateToStreamState(mState); streamState = internalStateToStreamState(mState);
} }
mStreamInfo.setState(StreamInfo.of(mStreamId, streamState, mInProgressTransformationInfo)); mStreamInfo.setState(StreamInfo.of(mStreamId, streamState));
} }
@ExecutedBy("mSequentialExecutor") @ExecutedBy("mSequentialExecutor")
@ -2660,20 +2542,7 @@ public final class Recorder implements VideoOutput {
} }
Logger.d(TAG, "Transitioning streamId: " + mStreamId + " --> " + streamId); Logger.d(TAG, "Transitioning streamId: " + mStreamId + " --> " + streamId);
mStreamId = streamId; mStreamId = streamId;
mStreamInfo.setState(StreamInfo.of(streamId, internalStateToStreamState(mState), mStreamInfo.setState(StreamInfo.of(streamId, internalStateToStreamState(mState)));
mInProgressTransformationInfo));
}
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
@ExecutedBy("mSequentialExecutor")
void setInProgressTransformationInfo(
@Nullable SurfaceRequest.TransformationInfo transformationInfo) {
Logger.d(TAG, "Update stream transformation info: " + transformationInfo);
mInProgressTransformationInfo = transformationInfo;
synchronized (mLock) {
mStreamInfo.setState(StreamInfo.of(mStreamId, internalStateToStreamState(mState),
transformationInfo));
}
} }
/** /**
@ -2696,8 +2565,8 @@ public final class Recorder implements VideoOutput {
if (mNonPendingState != state) { if (mNonPendingState != state) {
mNonPendingState = state; mNonPendingState = state;
mStreamInfo.setState(StreamInfo.of(mStreamId, internalStateToStreamState(state), mStreamInfo.setState(
mInProgressTransformationInfo)); StreamInfo.of(mStreamId, internalStateToStreamState(state)));
} }
} }
@ -2745,21 +2614,6 @@ public final class Recorder implements VideoOutput {
return defaultMuxerFormat; return defaultMuxerFormat;
} }
/**
* Returns the {@link VideoCapabilities} of Recorder with respect to input camera information.
*
* <p>{@link VideoCapabilities} provides methods to query supported dynamic ranges and
* qualities. This information can be used for things like checking if HDR is supported for
* configuring VideoCapture to record HDR video.
*
* @param cameraInfo info about the camera.
* @return VideoCapabilities with respect to the input camera info.
*/
@NonNull
public static VideoCapabilities getVideoCapabilities(@NonNull CameraInfo cameraInfo) {
return RecorderVideoCapabilities.from(cameraInfo);
}
@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java @RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
@AutoValue @AutoValue
abstract static class RecordingRecord implements AutoCloseable { abstract static class RecordingRecord implements AutoCloseable {
@ -2788,7 +2642,6 @@ public final class Recorder implements VideoOutput {
pendingRecording.getListenerExecutor(), pendingRecording.getListenerExecutor(),
pendingRecording.getEventListener(), pendingRecording.getEventListener(),
pendingRecording.isAudioEnabled(), pendingRecording.isAudioEnabled(),
pendingRecording.isPersistent(),
recordingId recordingId
); );
} }
@ -2804,8 +2657,6 @@ public final class Recorder implements VideoOutput {
abstract boolean hasAudioEnabled(); abstract boolean hasAudioEnabled();
abstract boolean isPersistent();
abstract long getRecordingId(); abstract long getRecordingId();
/** /**
@ -2869,13 +2720,8 @@ public final class Recorder implements VideoOutput {
// Toggle on pending status for the video file. // Toggle on pending status for the video file.
contentValues.put(MediaStore.Video.Media.IS_PENDING, PENDING); contentValues.put(MediaStore.Video.Media.IS_PENDING, PENDING);
} }
try { outputUri = mediaStoreOutputOptions.getContentResolver().insert(
outputUri = mediaStoreOutputOptions.getContentResolver().insert( mediaStoreOutputOptions.getCollectionUri(), contentValues);
mediaStoreOutputOptions.getCollectionUri(), contentValues);
} catch (RuntimeException e) {
throw new IOException("Unable to create MediaStore entry by " + e,
e);
}
if (outputUri == null) { if (outputUri == null) {
throw new IOException("Unable to create MediaStore entry."); throw new IOException("Unable to create MediaStore entry.");
} }
@ -3097,12 +2943,7 @@ public final class Recorder implements VideoOutput {
throw new AssertionError("One-time media muxer creation has already occurred for" throw new AssertionError("One-time media muxer creation has already occurred for"
+ " recording " + this); + " recording " + this);
} }
return mediaMuxerSupplier.get(muxerOutputFormat, outputUriCreatedCallback);
try {
return mediaMuxerSupplier.get(muxerOutputFormat, outputUriCreatedCallback);
} catch (RuntimeException e) {
throw new IOException("Failed to create MediaMuxer by " + e, e);
}
} }
/** /**

View File

@ -19,7 +19,6 @@ package androidx.camera.video;
import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP; import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi; import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo; import androidx.annotation.RestrictTo;
import androidx.camera.core.impl.utils.CloseGuardHelper; import androidx.camera.core.impl.utils.CloseGuardHelper;
@ -54,15 +53,13 @@ public final class Recording implements AutoCloseable {
private final Recorder mRecorder; private final Recorder mRecorder;
private final long mRecordingId; private final long mRecordingId;
private final OutputOptions mOutputOptions; private final OutputOptions mOutputOptions;
private final boolean mIsPersistent;
private final CloseGuardHelper mCloseGuard = CloseGuardHelper.create(); private final CloseGuardHelper mCloseGuard = CloseGuardHelper.create();
Recording(@NonNull Recorder recorder, long recordingId, @NonNull OutputOptions options, Recording(@NonNull Recorder recorder, long recordingId, @NonNull OutputOptions options,
boolean isPersistent, boolean finalizedOnCreation) { boolean finalizedOnCreation) {
mRecorder = recorder; mRecorder = recorder;
mRecordingId = recordingId; mRecordingId = recordingId;
mOutputOptions = options; mOutputOptions = options;
mIsPersistent = isPersistent;
if (finalizedOnCreation) { if (finalizedOnCreation) {
mIsClosed.set(true); mIsClosed.set(true);
@ -83,7 +80,6 @@ public final class Recording implements AutoCloseable {
return new Recording(pendingRecording.getRecorder(), return new Recording(pendingRecording.getRecorder(),
recordingId, recordingId,
pendingRecording.getOutputOptions(), pendingRecording.getOutputOptions(),
pendingRecording.isPersistent(),
/*finalizedOnCreation=*/false); /*finalizedOnCreation=*/false);
} }
@ -104,7 +100,6 @@ public final class Recording implements AutoCloseable {
return new Recording(pendingRecording.getRecorder(), return new Recording(pendingRecording.getRecorder(),
recordingId, recordingId,
pendingRecording.getOutputOptions(), pendingRecording.getOutputOptions(),
pendingRecording.isPersistent(),
/*finalizedOnCreation=*/true); /*finalizedOnCreation=*/true);
} }
@ -113,20 +108,6 @@ public final class Recording implements AutoCloseable {
return mOutputOptions; 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. * Pauses the current recording if active.
* *
@ -212,7 +193,11 @@ public final class Recording implements AutoCloseable {
*/ */
@Override @Override
public void close() { public void close() {
stopWithError(VideoRecordEvent.Finalize.ERROR_NONE, /*errorCause=*/ null); mCloseGuard.close();
if (mIsClosed.getAndSet(true)) {
return;
}
mRecorder.stop(this);
} }
@Override @Override
@ -220,8 +205,7 @@ public final class Recording implements AutoCloseable {
protected void finalize() throws Throwable { protected void finalize() throws Throwable {
try { try {
mCloseGuard.warnIfOpen(); mCloseGuard.warnIfOpen();
stopWithError(VideoRecordEvent.Finalize.ERROR_RECORDING_GARBAGE_COLLECTED, stop();
new RuntimeException("Recording stopped due to being garbage collected."));
} finally { } finally {
super.finalize(); super.finalize();
} }
@ -247,14 +231,5 @@ public final class Recording implements AutoCloseable {
public boolean isClosed() { public boolean isClosed() {
return mIsClosed.get(); 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);
}
} }

View File

@ -1,7 +1,5 @@
#!/bin/sh #!/bin/sh
set -e
for i in "PendingRecording" "Recording" "Recorder"; do for i in "PendingRecording" "Recording" "Recorder"; do
diff3 -m ../Suckless$i.java base/$i.java new/$i.java > Suckless$i.java && mv Suckless$i.java .. diff3 -m ../Suckless$i.java base/$i.java new/$i.java > Suckless$i.java && mv Suckless$i.java ..
done done

View File

@ -18,7 +18,6 @@ import androidx.annotation.RequiresApi
import androidx.camera.core.AspectRatio import androidx.camera.core.AspectRatio
import androidx.camera.core.Camera import androidx.camera.core.Camera
import androidx.camera.core.CameraSelector import androidx.camera.core.CameraSelector
import androidx.camera.core.DynamicRange
import androidx.camera.core.FocusMeteringAction import androidx.camera.core.FocusMeteringAction
import androidx.camera.core.ImageCapture import androidx.camera.core.ImageCapture
import androidx.camera.core.ImageCaptureException import androidx.camera.core.ImageCaptureException
@ -415,7 +414,7 @@ class CameraActivity : BaseActivity(), SensorOrientationListener.Listener {
refreshVideoCapture() refreshVideoCapture()
camera = cameraProvider.bindToLifecycle(this, cameraSelector, cameraPreview, videoCapture) camera = cameraProvider.bindToLifecycle(this, cameraSelector, cameraPreview, videoCapture)
if (qualities == null) { if (qualities == null) {
qualities = SucklessRecorder.getVideoCapabilities(camera!!.cameraInfo).getSupportedQualities(DynamicRange.UNSPECIFIED) qualities = QualitySelector.getSupportedQualities(camera!!.cameraInfo)
} }
videoCapture videoCapture
} else { } else {

View File

@ -16,7 +16,7 @@ import sushi.hardcore.droidfs.filesystems.EncryptedVolume
import sushi.hardcore.droidfs.filesystems.GocryptfsVolume import sushi.hardcore.droidfs.filesystems.GocryptfsVolume
import sushi.hardcore.droidfs.util.IntentUtils import sushi.hardcore.droidfs.util.IntentUtils
import sushi.hardcore.droidfs.util.ObjRef import sushi.hardcore.droidfs.util.ObjRef
import sushi.hardcore.droidfs.util.UIUtils import sushi.hardcore.droidfs.util.WidgetUtil
import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder
import java.util.* import java.util.*
@ -89,8 +89,8 @@ class ChangePasswordActivity: BaseActivity() {
} }
private fun changeVolumePassword() { private fun changeVolumePassword() {
val newPassword = UIUtils.encodeEditTextContent(binding.editNewPassword) val newPassword = WidgetUtil.encodeEditTextContent(binding.editNewPassword)
val newPasswordConfirm = UIUtils.encodeEditTextContent(binding.editPasswordConfirm) val newPasswordConfirm = WidgetUtil.encodeEditTextContent(binding.editPasswordConfirm)
@SuppressLint("NewApi") @SuppressLint("NewApi")
if (!newPassword.contentEquals(newPasswordConfirm)) { if (!newPassword.contentEquals(newPasswordConfirm)) {
Toast.makeText(this, R.string.passwords_mismatch, Toast.LENGTH_SHORT).show() Toast.makeText(this, R.string.passwords_mismatch, Toast.LENGTH_SHORT).show()
@ -135,7 +135,7 @@ class ChangePasswordActivity: BaseActivity() {
null null
} }
val currentPassword = if (givenHash == null) { val currentPassword = if (givenHash == null) {
UIUtils.encodeEditTextContent(binding.editCurrentPassword) WidgetUtil.encodeEditTextContent(binding.editCurrentPassword)
} else { } else {
null null
} }
@ -160,7 +160,7 @@ class ChangePasswordActivity: BaseActivity() {
) )
} }
if (success) { if (success) {
if (volumeDatabase.isHashSaved(volume)) { if (volumeDatabase.isHashSaved(volume.name)) {
volumeDatabase.removeHash(volume) volumeDatabase.removeHash(volume)
} }
} }

View File

@ -7,7 +7,6 @@ import android.os.Handler
import android.os.ParcelFileDescriptor import android.os.ParcelFileDescriptor
import android.system.Os import android.system.Os
import android.util.Log import android.util.Log
import androidx.preference.PreferenceManager
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import sushi.hardcore.droidfs.filesystems.EncryptedVolume import sushi.hardcore.droidfs.filesystems.EncryptedVolume
@ -23,23 +22,6 @@ class EncryptedFileProvider(context: Context) {
companion object { companion object {
private const val TAG = "EncryptedFileProvider" private const val TAG = "EncryptedFileProvider"
fun getTmpFilesDir(context: Context) = File(context.cacheDir, "tmp") fun getTmpFilesDir(context: Context) = File(context.cacheDir, "tmp")
var exportMethod = ExportMethod.AUTO
}
enum class ExportMethod {
AUTO,
DISK,
MEMORY;
companion object {
fun parse(value: String) = when (value) {
"auto" -> EncryptedFileProvider.ExportMethod.AUTO
"disk" -> EncryptedFileProvider.ExportMethod.DISK
"memory" -> EncryptedFileProvider.ExportMethod.MEMORY
else -> throw IllegalArgumentException("Invalid export method: $value")
}
}
} }
private val memoryInfo = ActivityManager.MemoryInfo() private val memoryInfo = ActivityManager.MemoryInfo()
@ -51,11 +33,6 @@ class EncryptedFileProvider(context: Context) {
(context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager).getMemoryInfo( (context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager).getMemoryInfo(
memoryInfo memoryInfo
) )
PreferenceManager.getDefaultSharedPreferences(context)
.getString("export_method", null)?.let {
exportMethod = ExportMethod.parse(it)
}
} }
class ExportedDiskFile private constructor( class ExportedDiskFile private constructor(
@ -141,18 +118,16 @@ class EncryptedFileProvider(context: Context) {
path: String, path: String,
size: Long, size: Long,
): ExportedFile? { ): ExportedFile? {
val diskFile by lazy { ExportedDiskFile.create(path, tmpFilesDir, handler) } return if (size > memoryInfo.availMem * 0.8) {
val memFile by lazy { ExportedMemFile.create(path, size) } ExportedDiskFile.create(
return when (exportMethod) { path,
ExportMethod.MEMORY -> memFile tmpFilesDir,
ExportMethod.DISK -> diskFile handler,
ExportMethod.AUTO -> { )
if (isMemFileSupported && size < memoryInfo.availMem * 0.8) { } else if (isMemFileSupported) {
memFile ExportedMemFile.create(path, size) as ExportedFile
} else { } else {
diskFile null
}
}
} }
} }

View File

@ -6,7 +6,7 @@ object FileTypes {
private val FILE_EXTENSIONS = mapOf( private val FILE_EXTENSIONS = mapOf(
Pair("image", listOf("png", "jpg", "jpeg", "gif", "webp", "bmp", "heic")), Pair("image", listOf("png", "jpg", "jpeg", "gif", "webp", "bmp", "heic")),
Pair("video", listOf("mp4", "webm", "mkv", "mov")), Pair("video", listOf("mp4", "webm", "mkv", "mov")),
Pair("audio", listOf("mp3", "ogg", "m4a", "wav", "flac", "opus")), Pair("audio", listOf("mp3", "ogg", "m4a", "wav", "flac")),
Pair("pdf", listOf("pdf")), Pair("pdf", listOf("pdf")),
Pair("text", listOf( Pair("text", listOf(
"asc", "asc",

View File

@ -1,88 +0,0 @@
package sushi.hardcore.droidfs
import android.net.Uri
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import sushi.hardcore.droidfs.databinding.ActivityLogcatBinding
import java.io.BufferedReader
import java.io.BufferedWriter
import java.io.InputStreamReader
import java.io.InterruptedIOException
import java.io.OutputStreamWriter
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
class LogcatActivity: BaseActivity() {
private lateinit var binding: ActivityLogcatBinding
private var process: Process? = null
private val dateFormat by lazy {
SimpleDateFormat("yyyy-MM-dd_HH:mm:ss", Locale.getDefault())
}
private val saveAs = registerForActivityResult(ActivityResultContracts.CreateDocument("text/*")) { uri ->
uri?.let {
saveTo(it)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityLogcatBinding.inflate(layoutInflater)
setContentView(binding.root)
title = getString(R.string.logcat_title)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
lifecycleScope.launch(Dispatchers.IO) {
try {
BufferedReader(InputStreamReader(Runtime.getRuntime().exec("logcat").also {
process = it
}.inputStream)).forEachLine {
binding.content.post {
binding.content.append("$it\n")
}
}
} catch (_: InterruptedIOException) {}
}
}
override fun onDestroy() {
super.onDestroy()
process?.destroy()
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.logcat, menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
android.R.id.home -> {
finish()
true
}
R.id.save -> {
saveAs.launch("DroidFS_${dateFormat.format(Date())}.log")
true
}
else -> super.onOptionsItemSelected(item)
}
}
private fun saveTo(uri: Uri) {
lifecycleScope.launch(Dispatchers.IO) {
BufferedWriter(OutputStreamWriter(contentResolver.openOutputStream(uri))).use {
it.write(binding.content.text.toString())
}
launch(Dispatchers.Main) {
Toast.makeText(this@LogcatActivity, R.string.logcat_saved, Toast.LENGTH_SHORT).show()
}
}
}
}

View File

@ -20,7 +20,6 @@ import kotlinx.coroutines.launch
import sushi.hardcore.droidfs.Constants.DEFAULT_VOLUME_KEY import sushi.hardcore.droidfs.Constants.DEFAULT_VOLUME_KEY
import sushi.hardcore.droidfs.adapters.VolumeAdapter import sushi.hardcore.droidfs.adapters.VolumeAdapter
import sushi.hardcore.droidfs.add_volume.AddVolumeActivity import sushi.hardcore.droidfs.add_volume.AddVolumeActivity
import sushi.hardcore.droidfs.content_providers.VolumeProvider
import sushi.hardcore.droidfs.databinding.ActivityMainBinding import sushi.hardcore.droidfs.databinding.ActivityMainBinding
import sushi.hardcore.droidfs.databinding.DialogDeleteVolumeBinding import sushi.hardcore.droidfs.databinding.DialogDeleteVolumeBinding
import sushi.hardcore.droidfs.explorers.ExplorerRouter import sushi.hardcore.droidfs.explorers.ExplorerRouter
@ -28,7 +27,6 @@ import sushi.hardcore.droidfs.file_operations.FileOperationService
import sushi.hardcore.droidfs.file_operations.TaskResult import sushi.hardcore.droidfs.file_operations.TaskResult
import sushi.hardcore.droidfs.util.IntentUtils import sushi.hardcore.droidfs.util.IntentUtils
import sushi.hardcore.droidfs.util.PathUtils import sushi.hardcore.droidfs.util.PathUtils
import sushi.hardcore.droidfs.util.UIUtils
import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder
import sushi.hardcore.droidfs.widgets.EditTextDialog import sushi.hardcore.droidfs.widgets.EditTextDialog
import java.io.File import java.io.File
@ -197,7 +195,7 @@ class MainActivity : BaseActivity(), VolumeAdapter.Listener {
private fun removeVolume(volume: VolumeData) { private fun removeVolume(volume: VolumeData) {
volumeManager.getVolumeId(volume)?.let { volumeManager.closeVolume(it) } volumeManager.getVolumeId(volume)?.let { volumeManager.closeVolume(it) }
volumeDatabase.removeVolume(volume) volumeDatabase.removeVolume(volume.name)
} }
private fun removeVolumes(volumes: List<VolumeData>, i: Int = 0, doDeleteVolumeContent: Boolean? = null) { private fun removeVolumes(volumes: List<VolumeData>, i: Int = 0, doDeleteVolumeContent: Boolean? = null) {
@ -326,14 +324,7 @@ class MainActivity : BaseActivity(), VolumeAdapter.Listener {
DocumentFile.fromFile(File(volume.name)), DocumentFile.fromFile(File(volume.name)),
DocumentFile.fromFile(hiddenVolumeFile.parentFile!!), DocumentFile.fromFile(hiddenVolumeFile.parentFile!!),
) { ) {
VolumeData( VolumeData(volume.shortName, true, volume.type, volume.encryptedHash, volume.iv)
VolumeData.newUuid(),
volume.shortName,
true,
volume.type,
volume.encryptedHash,
volume.iv
)
} }
} }
} }
@ -355,11 +346,7 @@ class MainActivity : BaseActivity(), VolumeAdapter.Listener {
override fun onCreateOptionsMenu(menu: Menu): Boolean { override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.main_activity, menu) menuInflater.inflate(R.menu.main_activity, menu)
val settingsVisible = !explorerRouter.pickMode && !explorerRouter.dropMode menu.findItem(R.id.settings).isVisible = !explorerRouter.pickMode && !explorerRouter.dropMode
menu.findItem(R.id.settings).isVisible = settingsVisible
if (settingsVisible) {
UIUtils.getMenuIconNeutralTint(this, menu).applyTo(R.id.settings, R.drawable.icon_settings)
}
val isSelecting = volumeAdapter.selectedItems.isNotEmpty() val isSelecting = volumeAdapter.selectedItems.isNotEmpty()
menu.findItem(R.id.select_all).isVisible = isSelecting menu.findItem(R.id.select_all).isVisible = isSelecting
menu.findItem(R.id.lock).isVisible = isSelecting && volumeAdapter.selectedItems.any { menu.findItem(R.id.lock).isVisible = isSelecting && volumeAdapter.selectedItems.any {
@ -387,7 +374,7 @@ class MainActivity : BaseActivity(), VolumeAdapter.Listener {
) )
} }
} }
menu.findItem(R.id.rename).isVisible = onlyOneAndWriteable && !volumeManager.isOpen(volumeAdapter.volumes[volumeAdapter.selectedItems.first()]) menu.findItem(R.id.rename).isVisible = onlyOneAndWriteable
supportActionBar?.setDisplayHomeAsUpEnabled(isSelecting || explorerRouter.pickMode || explorerRouter.dropMode) supportActionBar?.setDisplayHomeAsUpEnabled(isSelecting || explorerRouter.pickMode || explorerRouter.dropMode)
return true return true
} }
@ -411,7 +398,6 @@ class MainActivity : BaseActivity(), VolumeAdapter.Listener {
val path = PathUtils.getFullPathFromTreeUri(dstRootDirectory.uri, this) val path = PathUtils.getFullPathFromTreeUri(dstRootDirectory.uri, this)
if (path == null) null if (path == null) null
else VolumeData( else VolumeData(
VolumeData.newUuid(),
PathUtils.pathJoin(path, name), PathUtils.pathJoin(path, name),
false, false,
volume.type, volume.type,
@ -480,8 +466,7 @@ class MainActivity : BaseActivity(), VolumeAdapter.Listener {
DocumentFile.fromFile(srcPath).renameTo(newName) DocumentFile.fromFile(srcPath).renameTo(newName)
} }
if (success) { if (success) {
volumeDatabase.renameVolume(volume, newDBName) volumeDatabase.renameVolume(volume.name, newDBName)
VolumeProvider.notifyRootsChanged(this)
unselect(position) unselect(position)
if (volume.name == volumeOpener.defaultVolumeName) { if (volume.name == volumeOpener.defaultVolumeName) {
with (sharedPrefs.edit()) { with (sharedPrefs.edit()) {

View File

@ -1,6 +1,5 @@
package sushi.hardcore.droidfs package sushi.hardcore.droidfs
import android.app.ActivityOptions
import android.content.Intent import android.content.Intent
import android.content.SharedPreferences import android.content.SharedPreferences
import android.os.Build import android.os.Build
@ -91,15 +90,9 @@ class SettingsActivity : BaseActivity() {
private fun refreshTheme() { private fun refreshTheme() {
with(requireActivity()) { with(requireActivity()) {
startActivity( startActivity(Intent(this, SettingsActivity::class.java))
Intent(this, SettingsActivity::class.java),
ActivityOptions.makeCustomAnimation(
this,
android.R.anim.fade_in,
android.R.anim.fade_out
).toBundle()
)
finish() finish()
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out)
} }
} }
@ -127,10 +120,6 @@ class SettingsActivity : BaseActivity() {
false false
} }
} }
findPreference<Preference>("logcat")?.setOnPreferenceClickListener { _ ->
startActivity(Intent(requireContext(), LogcatActivity::class.java))
true
}
} }
} }
@ -190,9 +179,19 @@ class SettingsActivity : BaseActivity() {
true true
} }
switchExpose.setOnPreferenceChangeListener { _, checked -> switchExpose.setOnPreferenceChangeListener { _, checked ->
VolumeProvider.usfExpose = checked as Boolean if (checked as Boolean) {
if (!Compat.isMemFileSupported()) {
CustomAlertDialogBuilder(requireContext(), (requireActivity() as BaseActivity).theme)
.setTitle(R.string.error)
.setMessage("Your current kernel does not support memfd_create(). This feature requires a minimum kernel version of ${Compat.MEMFD_CREATE_MINIMUM_KERNEL_VERSION}.")
.setPositiveButton(R.string.ok, null)
.show()
return@setOnPreferenceChangeListener false
}
}
VolumeProvider.usfExpose = checked
updateView(usfExpose = checked) updateView(usfExpose = checked)
VolumeProvider.notifyRootsChanged(requireContext()) VolumeProvider.notifyRootChanged(requireContext())
true true
} }
switchSafWrite.setOnPreferenceChangeListener { _, checked -> switchSafWrite.setOnPreferenceChangeListener { _, checked ->
@ -200,19 +199,6 @@ class SettingsActivity : BaseActivity() {
TemporaryFileProvider.usfSafWrite = checked TemporaryFileProvider.usfSafWrite = checked
true true
} }
findPreference<ListPreference>("export_method")!!.setOnPreferenceChangeListener { _, newValue ->
if (newValue as String == "memory" && !Compat.isMemFileSupported()) {
CustomAlertDialogBuilder(requireContext(), (requireActivity() as BaseActivity).theme)
.setTitle(R.string.error)
.setMessage(getString(R.string.memfd_create_unsupported, Compat.MEMFD_CREATE_MINIMUM_KERNEL_VERSION))
.setPositiveButton(R.string.ok, null)
.show()
return@setOnPreferenceChangeListener false
}
EncryptedFileProvider.exportMethod = EncryptedFileProvider.ExportMethod.parse(newValue)
true
}
} }
} }
} }

View File

@ -8,19 +8,10 @@ import sushi.hardcore.droidfs.filesystems.GocryptfsVolume
import sushi.hardcore.droidfs.util.PathUtils import sushi.hardcore.droidfs.util.PathUtils
import java.io.File import java.io.File
import java.io.FileInputStream import java.io.FileInputStream
import java.util.UUID
class VolumeData( class VolumeData(val name: String, val isHidden: Boolean = false, val type: Byte, var encryptedHash: ByteArray? = null, var iv: ByteArray? = null): Parcelable {
val uuid: String,
val name: String,
val isHidden: Boolean = false,
val type: Byte,
var encryptedHash: ByteArray? = null,
var iv: ByteArray? = null
) : Parcelable {
constructor(parcel: Parcel) : this( constructor(parcel: Parcel) : this(
parcel.readString()!!,
parcel.readString()!!, parcel.readString()!!,
parcel.readByte() != 0.toByte(), parcel.readByte() != 0.toByte(),
parcel.readByte(), parcel.readByte(),
@ -32,7 +23,12 @@ class VolumeData(
File(name).name File(name).name
} }
fun getFullPath(filesDir: String) = getFullPath(name, isHidden, filesDir) fun getFullPath(filesDir: String): String {
return if (isHidden)
getHiddenVolumeFullPath(filesDir, name)
else
name
}
fun canRead(filesDir: String): Boolean { fun canRead(filesDir: String): Boolean {
val volumePath = getFullPath(filesDir) val volumePath = getFullPath(filesDir)
@ -66,7 +62,6 @@ class VolumeData(
override fun writeToParcel(dest: Parcel, flags: Int) { override fun writeToParcel(dest: Parcel, flags: Int) {
with (dest) { with (dest) {
writeString(uuid)
writeString(name) writeString(name)
writeByte(if (isHidden) 1 else 0) writeByte(if (isHidden) 1 else 0)
writeByte(type) writeByte(type)
@ -79,10 +74,12 @@ class VolumeData(
if (other !is VolumeData) { if (other !is VolumeData) {
return false return false
} }
return other.uuid == uuid return other.name == name && other.isHidden == isHidden
} }
override fun hashCode() = uuid.hashCode() override fun hashCode(): Int {
return name.hashCode()+isHidden.hashCode()
}
companion object { companion object {
const val VOLUMES_DIRECTORY = "volumes" const val VOLUMES_DIRECTORY = "volumes"
@ -93,17 +90,8 @@ class VolumeData(
override fun newArray(size: Int) = arrayOfNulls<VolumeData>(size) override fun newArray(size: Int) = arrayOfNulls<VolumeData>(size)
} }
fun newUuid(): String = UUID.randomUUID().toString()
fun getHiddenVolumeFullPath(filesDir: String, name: String): String { fun getHiddenVolumeFullPath(filesDir: String, name: String): String {
return PathUtils.pathJoin(filesDir, VOLUMES_DIRECTORY, name) return PathUtils.pathJoin(filesDir, VOLUMES_DIRECTORY, name)
} }
fun getFullPath(name: String, isHidden: Boolean, filesDir: String): String {
return if (isHidden)
getHiddenVolumeFullPath(filesDir, name)
else
name
}
} }
} }

View File

@ -10,69 +10,46 @@ import sushi.hardcore.droidfs.filesystems.EncryptedVolume
import sushi.hardcore.droidfs.util.PathUtils import sushi.hardcore.droidfs.util.PathUtils
import java.io.File import java.io.File
class VolumeDatabase(private val context: Context): SQLiteOpenHelper(context, Constants.VOLUME_DATABASE_NAME, null, 6) { class VolumeDatabase(private val context: Context): SQLiteOpenHelper(context, Constants.VOLUME_DATABASE_NAME, null, 5) {
companion object { companion object {
private const val TAG = "VolumeDatabase" const val TAG = "VolumeDatabase"
private const val TABLE_NAME = "Volumes" const val TABLE_NAME = "Volumes"
private const val COLUMN_UUID = "uuid" const val COLUMN_NAME = "name"
private const val COLUMN_NAME = "name" const val COLUMN_HIDDEN = "hidden"
private const val COLUMN_HIDDEN = "hidden" const val COLUMN_TYPE = "type"
private const val COLUMN_TYPE = "type" const val COLUMN_HASH = "hash"
private const val COLUMN_HASH = "hash" const val COLUMN_IV = "iv"
private const val COLUMN_IV = "iv"
}
private fun createTable(db: SQLiteDatabase) = private fun contentValuesFromVolume(volume: VolumeData): ContentValues {
db.execSQL( val contentValues = ContentValues()
"CREATE TABLE IF NOT EXISTS $TABLE_NAME (" + contentValues.put(COLUMN_NAME, volume.name)
"$COLUMN_UUID TEXT PRIMARY KEY," + contentValues.put(COLUMN_HIDDEN, volume.isHidden)
"$COLUMN_NAME TEXT," + contentValues.put(COLUMN_TYPE, byteArrayOf(volume.type))
"$COLUMN_HIDDEN SHORT," + contentValues.put(COLUMN_HASH, volume.encryptedHash)
"$COLUMN_TYPE BLOB," + contentValues.put(COLUMN_IV, volume.iv)
"$COLUMN_HASH BLOB," + return contentValues
"$COLUMN_IV BLOB" +
");"
)
override fun onCreate(db: SQLiteDatabase) {
createTable(db)
File(context.filesDir, VolumeData.VOLUMES_DIRECTORY).mkdir()
}
override fun onOpen(db: SQLiteDatabase) {
//check if database has been corrupted by v2.1.1
val cursor = db.rawQuery("SELECT * FROM $TABLE_NAME WHERE $COLUMN_TYPE IS NULL;", null)
if (cursor.count > 0) {
Log.w(TAG, "Found ${cursor.count} corrupted volumes")
while (cursor.moveToNext()) {
// fix columns left shift
val uuid = cursor.getString(cursor.getColumnIndexOrThrow(COLUMN_UUID)+5)
val name = cursor.getString(cursor.getColumnIndexOrThrow(COLUMN_NAME)-1)
val isHidden = cursor.getShort(cursor.getColumnIndexOrThrow(COLUMN_HIDDEN)-1) == 1.toShort()
val type = cursor.getBlob(cursor.getColumnIndexOrThrow(COLUMN_TYPE)-1)[0]
val hash = cursor.getBlob(cursor.getColumnIndexOrThrow(COLUMN_HASH)-1)
val iv = cursor.getBlob(cursor.getColumnIndexOrThrow(COLUMN_IV)-1)
if (db.delete(TABLE_NAME, "$COLUMN_IV=?", arrayOf(uuid)) < 1) {
Log.e(TAG, "Failed to remove volume $name")
}
if (db.insert(TABLE_NAME, null, ContentValues().apply {
put(COLUMN_UUID, uuid)
put(COLUMN_NAME, name)
put(COLUMN_HIDDEN, isHidden)
put(COLUMN_TYPE, byteArrayOf(type))
put(COLUMN_HASH, hash)
put(COLUMN_IV, iv)
}) < 0) {
Log.e(TAG, "Failed to insert volume $name")
}
}
} }
cursor.close() }
override fun onCreate(db: SQLiteDatabase) {
db.execSQL(
"CREATE TABLE IF NOT EXISTS $TABLE_NAME (" +
"$COLUMN_NAME TEXT PRIMARY KEY," +
"$COLUMN_HIDDEN SHORT," +
"$COLUMN_TYPE BLOB," +
"$COLUMN_HASH BLOB," +
"$COLUMN_IV BLOB" +
");"
)
File(context.filesDir, VolumeData.VOLUMES_DIRECTORY).mkdir()
} }
private fun getNewVolumePath(volumeName: String): File { private fun getNewVolumePath(volumeName: String): File {
return File( return File(
VolumeData.getFullPath(volumeName, true, context.filesDir.path) VolumeData(
volumeName,
true,
EncryptedVolume.GOCRYPTFS_VOLUME_TYPE
).getFullPath(context.filesDir.path)
).canonicalFile ).canonicalFile
} }
@ -124,37 +101,10 @@ class VolumeDatabase(private val context: Context): SQLiteOpenHelper(context, Co
} }
} }
} }
if (oldVersion < 6) {
val cursor = db.rawQuery("SELECT $COLUMN_NAME FROM $TABLE_NAME;", null)
val volumeNames = arrayOfNulls<String>(cursor.count)
var i = 0
while (cursor.moveToNext()) {
volumeNames[i++] = cursor.getString(0)
}
cursor.close()
if (volumeNames.isEmpty()) {
db.execSQL("DROP TABLE $TABLE_NAME;")
createTable(db)
} else {
db.execSQL("ALTER TABLE $TABLE_NAME RENAME TO OLD;")
createTable(db)
val uuidsValues = volumeNames.indices.joinToString(", ") { "('${VolumeData.newUuid()}', ?)" }
// add uuids to old data
db.execSQL(
"INSERT INTO $TABLE_NAME " +
"WITH uuids($COLUMN_UUID, $COLUMN_NAME) AS (VALUES $uuidsValues) " +
"SELECT $COLUMN_UUID, OLD.$COLUMN_NAME, $COLUMN_HIDDEN, $COLUMN_TYPE, $COLUMN_HASH, $COLUMN_IV " +
"FROM OLD JOIN uuids ON OLD.name = uuids.name;",
volumeNames
)
db.execSQL("DROP TABLE OLD;")
}
}
} }
private fun extractVolumeData(cursor: Cursor): VolumeData { private fun extractVolumeData(cursor: Cursor): VolumeData {
return VolumeData( return VolumeData(
cursor.getString(cursor.getColumnIndexOrThrow(COLUMN_UUID)),
cursor.getString(cursor.getColumnIndexOrThrow(COLUMN_NAME)), cursor.getString(cursor.getColumnIndexOrThrow(COLUMN_NAME)),
cursor.getShort(cursor.getColumnIndexOrThrow(COLUMN_HIDDEN)) == 1.toShort(), cursor.getShort(cursor.getColumnIndexOrThrow(COLUMN_HIDDEN)) == 1.toShort(),
cursor.getBlob(cursor.getColumnIndexOrThrow(COLUMN_TYPE))[0], cursor.getBlob(cursor.getColumnIndexOrThrow(COLUMN_TYPE))[0],
@ -192,14 +142,7 @@ class VolumeDatabase(private val context: Context): SQLiteOpenHelper(context, Co
fun saveVolume(volume: VolumeData): Boolean { fun saveVolume(volume: VolumeData): Boolean {
if (!isVolumeSaved(volume.name, volume.isHidden)) { if (!isVolumeSaved(volume.name, volume.isHidden)) {
return (writableDatabase.insert(TABLE_NAME, null, ContentValues().apply { return (writableDatabase.insert(TABLE_NAME, null, contentValuesFromVolume(volume)) >= 0.toLong())
put(COLUMN_UUID, volume.uuid)
put(COLUMN_NAME, volume.name)
put(COLUMN_HIDDEN, volume.isHidden)
put(COLUMN_TYPE, byteArrayOf(volume.type))
put(COLUMN_HASH, volume.encryptedHash)
put(COLUMN_IV, volume.iv)
}) >= 0.toLong())
} }
return false return false
} }
@ -214,8 +157,8 @@ class VolumeDatabase(private val context: Context): SQLiteOpenHelper(context, Co
return list return list
} }
fun isHashSaved(volume: VolumeData): Boolean { fun isHashSaved(volumeName: String): Boolean {
val cursor = readableDatabase.rawQuery("SELECT $COLUMN_HASH FROM $TABLE_NAME WHERE $COLUMN_UUID=?", arrayOf(volume.uuid)) val cursor = readableDatabase.query(TABLE_NAME, arrayOf(COLUMN_NAME, COLUMN_HASH), "$COLUMN_NAME=?", arrayOf(volumeName), null, null, null)
var isHashSaved = false var isHashSaved = false
if (cursor.moveToNext()) { if (cursor.moveToNext()) {
if (cursor.getBlob(cursor.getColumnIndexOrThrow(COLUMN_HASH)) != null) { if (cursor.getBlob(cursor.getColumnIndexOrThrow(COLUMN_HASH)) != null) {
@ -227,33 +170,32 @@ class VolumeDatabase(private val context: Context): SQLiteOpenHelper(context, Co
} }
fun addHash(volume: VolumeData): Boolean { fun addHash(volume: VolumeData): Boolean {
return writableDatabase.update(TABLE_NAME, ContentValues().apply { return writableDatabase.update(TABLE_NAME, contentValuesFromVolume(volume), "$COLUMN_NAME=?", arrayOf(volume.name)) > 0
put(COLUMN_HASH, volume.encryptedHash)
put(COLUMN_IV, volume.iv)
}, "$COLUMN_UUID=?", arrayOf(volume.uuid)) > 0
} }
fun removeHash(volume: VolumeData): Boolean { fun removeHash(volume: VolumeData): Boolean {
return writableDatabase.update( return writableDatabase.update(
TABLE_NAME, TABLE_NAME, contentValuesFromVolume(
ContentValues().apply { VolumeData(
put(COLUMN_HASH, null as ByteArray?) volume.name,
put(COLUMN_IV, null as ByteArray?) volume.isHidden,
}, "$COLUMN_UUID=?", arrayOf(volume.uuid) volume.type,
) > 0 null,
null
)
), "$COLUMN_NAME=?", arrayOf(volume.name)) > 0
} }
fun renameVolume(volume: VolumeData, newName: String): Boolean { fun renameVolume(oldName: String, newName: String): Boolean {
return writableDatabase.update( return writableDatabase.update(TABLE_NAME,
TABLE_NAME,
ContentValues().apply { ContentValues().apply {
put(COLUMN_NAME, newName) put(COLUMN_NAME, newName)
}, },
"$COLUMN_UUID=?", arrayOf(volume.uuid) "$COLUMN_NAME=?",arrayOf(oldName)
) > 0 ) > 0
} }
fun removeVolume(volume: VolumeData): Boolean { fun removeVolume(volumeName: String): Boolean {
return writableDatabase.delete(TABLE_NAME, "$COLUMN_UUID=?", arrayOf(volume.uuid)) > 0 return writableDatabase.delete(TABLE_NAME, "$COLUMN_NAME=?", arrayOf(volumeName)) > 0
} }
} }

View File

@ -1,6 +1,7 @@
package sushi.hardcore.droidfs package sushi.hardcore.droidfs
import android.content.Context import android.content.Context
import android.provider.DocumentsContract
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
@ -17,7 +18,7 @@ class VolumeManager(private val context: Context) {
fun insert(volume: EncryptedVolume, data: VolumeData): Int { fun insert(volume: EncryptedVolume, data: VolumeData): Int {
volumes[id] = volume volumes[id] = volume
volumesData[data] = id volumesData[data] = id
VolumeProvider.notifyRootsChanged(context) VolumeProvider.notifyRootChanged(context)
return id++ return id++
} }
@ -48,7 +49,7 @@ class VolumeManager(private val context: Context) {
volumesData.filter { it.value == id }.forEach { volumesData.filter { it.value == id }.forEach {
volumesData.remove(it.key) volumesData.remove(it.key)
} }
VolumeProvider.notifyRootsChanged(context) VolumeProvider.notifyRootChanged(context)
} }
} }
@ -59,6 +60,6 @@ class VolumeManager(private val context: Context) {
} }
volumes.clear() volumes.clear()
volumesData.clear() volumesData.clear()
VolumeProvider.notifyRootsChanged(context) VolumeProvider.notifyRootChanged(context)
} }
} }

View File

@ -13,7 +13,7 @@ import sushi.hardcore.droidfs.Constants.DEFAULT_VOLUME_KEY
import sushi.hardcore.droidfs.databinding.DialogOpenVolumeBinding import sushi.hardcore.droidfs.databinding.DialogOpenVolumeBinding
import sushi.hardcore.droidfs.filesystems.EncryptedVolume import sushi.hardcore.droidfs.filesystems.EncryptedVolume
import sushi.hardcore.droidfs.util.ObjRef import sushi.hardcore.droidfs.util.ObjRef
import sushi.hardcore.droidfs.util.UIUtils import sushi.hardcore.droidfs.util.WidgetUtil
import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder
import java.util.* import java.util.*
@ -123,7 +123,7 @@ class VolumeOpener(
apply() apply()
} }
} }
val password = UIUtils.encodeEditTextContent(dialogBinding!!.editPassword) val password = WidgetUtil.encodeEditTextContent(dialogBinding!!.editPassword)
val savePasswordHash = dialogBinding!!.checkboxSavePassword.isChecked val savePasswordHash = dialogBinding!!.checkboxSavePassword.isChecked
dialogBinding = null dialogBinding = null
// openVolumeWithPassword is responsible for wiping the password // openVolumeWithPassword is responsible for wiping the password

View File

@ -20,7 +20,7 @@ import sushi.hardcore.droidfs.filesystems.EncryptedVolume
import sushi.hardcore.droidfs.filesystems.GocryptfsVolume import sushi.hardcore.droidfs.filesystems.GocryptfsVolume
import sushi.hardcore.droidfs.util.Compat import sushi.hardcore.droidfs.util.Compat
import sushi.hardcore.droidfs.util.ObjRef import sushi.hardcore.droidfs.util.ObjRef
import sushi.hardcore.droidfs.util.UIUtils import sushi.hardcore.droidfs.util.WidgetUtil
import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder
import java.io.File import java.io.File
import java.util.* import java.util.*
@ -146,8 +146,8 @@ class CreateVolumeFragment: Fragment() {
} }
private fun createVolume() { private fun createVolume() {
val password = UIUtils.encodeEditTextContent(binding.editPassword) val password = WidgetUtil.encodeEditTextContent(binding.editPassword)
val passwordConfirm = UIUtils.encodeEditTextContent(binding.editPasswordConfirm) val passwordConfirm = WidgetUtil.encodeEditTextContent(binding.editPasswordConfirm)
if (!password.contentEquals(passwordConfirm)) { if (!password.contentEquals(passwordConfirm)) {
Toast.makeText(requireContext(), R.string.passwords_mismatch, Toast.LENGTH_SHORT).show() Toast.makeText(requireContext(), R.string.passwords_mismatch, Toast.LENGTH_SHORT).show()
Arrays.fill(password, 0) Arrays.fill(password, 0)
@ -211,11 +211,11 @@ class CreateVolumeFragment: Fragment() {
.show() .show()
} else { } else {
val volumeName = if (isHiddenVolume) File(volumePath).name else volumePath val volumeName = if (isHiddenVolume) File(volumePath).name else volumePath
val volume = VolumeData(VolumeData.newUuid(), volumeName, isHiddenVolume, result) val volume = VolumeData(volumeName, isHiddenVolume, result)
var isVolumeSaved = false var isVolumeSaved = false
volumeDatabase.apply { volumeDatabase.apply {
if (isVolumeSaved(volumeName, isHiddenVolume)) // cleaning old saved path if (isVolumeSaved(volumeName, isHiddenVolume)) // cleaning old saved path
removeVolume(volume) removeVolume(volumeName)
if (rememberVolume) { if (rememberVolume) {
isVolumeSaved = saveVolume(volume) isVolumeSaved = saveVolume(volume)
} }

View File

@ -350,7 +350,7 @@ class SelectPathFragment: Fragment() {
} }
private fun addVolume(volumeName: String, isHidden: Boolean, volumeType: Byte) { private fun addVolume(volumeName: String, isHidden: Boolean, volumeType: Byte) {
val volumeData = VolumeData(VolumeData.newUuid(), volumeName, isHidden, volumeType) val volumeData = VolumeData(volumeName, isHidden, volumeType)
if (binding.switchRemember.isChecked) { if (binding.switchRemember.isChecked) {
volumeDatabase.saveVolume(volumeData) volumeDatabase.saveVolume(volumeData)
} }

View File

@ -2,7 +2,6 @@ package sushi.hardcore.droidfs.content_providers
import android.content.ContentProvider import android.content.ContentProvider
import android.content.ContentValues import android.content.ContentValues
import android.content.Intent
import android.database.Cursor import android.database.Cursor
import android.database.MatrixCursor import android.database.MatrixCursor
import android.net.Uri import android.net.Uri
@ -136,7 +135,6 @@ class TemporaryFileProvider : ContentProvider() {
// this must not be cancelled // this must not be cancelled
fun wipe() = GlobalScope.launch(Dispatchers.IO) { fun wipe() = GlobalScope.launch(Dispatchers.IO) {
context!!.revokeUriPermission(BASE_URI, Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
synchronized(this@TemporaryFileProvider) { synchronized(this@TemporaryFileProvider) {
for (i in files.values) { for (i in files.values) {
i.file.free() i.file.free()

View File

@ -1,6 +1,7 @@
package sushi.hardcore.droidfs.content_providers package sushi.hardcore.droidfs.content_providers
import android.content.Context import android.content.Context
import android.content.Intent
import android.database.Cursor import android.database.Cursor
import android.database.MatrixCursor import android.database.MatrixCursor
import android.os.CancellationSignal import android.os.CancellationSignal
@ -43,7 +44,7 @@ class VolumeProvider: DocumentsProvider() {
var usfExpose = false var usfExpose = false
var usfSafWrite = false var usfSafWrite = false
fun notifyRootsChanged(context: Context) { fun notifyRootChanged(context: Context) {
context.contentResolver.notifyChange(DocumentsContract.buildRootsUri(AUTHORITY), null) context.contentResolver.notifyChange(DocumentsContract.buildRootsUri(AUTHORITY), null)
} }
} }
@ -65,6 +66,7 @@ class VolumeProvider: DocumentsProvider() {
override fun queryRoots(projection: Array<out String>?): Cursor { override fun queryRoots(projection: Array<out String>?): Cursor {
val cursor = MatrixCursor(projection ?: DEFAULT_ROOT_PROJECTION) val cursor = MatrixCursor(projection ?: DEFAULT_ROOT_PROJECTION)
if (!usfExpose) return cursor if (!usfExpose) return cursor
val previousVolumeIds = volumes.keys.toSet()
volumes.clear() volumes.clear()
for (volume in volumeManager.listVolumes()) { for (volume in volumeManager.listVolumes()) {
var flags = DocumentsContract.Root.FLAG_LOCAL_ONLY or DocumentsContract.Root.FLAG_SUPPORTS_IS_CHILD var flags = DocumentsContract.Root.FLAG_LOCAL_ONLY or DocumentsContract.Root.FLAG_SUPPORTS_IS_CHILD
@ -76,9 +78,18 @@ class VolumeProvider: DocumentsProvider() {
add(DocumentsContract.Root.COLUMN_FLAGS, flags) add(DocumentsContract.Root.COLUMN_FLAGS, flags)
add(DocumentsContract.Root.COLUMN_ICON, R.drawable.icon_document_provider) add(DocumentsContract.Root.COLUMN_ICON, R.drawable.icon_document_provider)
add(DocumentsContract.Root.COLUMN_TITLE, volume.second.name) add(DocumentsContract.Root.COLUMN_TITLE, volume.second.name)
add(DocumentsContract.Root.COLUMN_DOCUMENT_ID, volume.second.uuid) add(DocumentsContract.Root.COLUMN_DOCUMENT_ID, volume.first)
}
volumes[volume.first.toString()] = volume
}
for (i in previousVolumeIds) {
if (!volumes.containsKey(i)) {
val uri = DocumentsContract.buildRootUri(AUTHORITY, i)
context?.revokeUriPermission(
uri,
Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
)
} }
volumes[volume.second.uuid] = volume
} }
return cursor return cursor
} }
@ -177,32 +188,6 @@ class VolumeProvider: DocumentsProvider() {
return cursor return cursor
} }
class LazyExportedFile(
private val encryptedFileProvider: EncryptedFileProvider,
private val encryptedVolume: EncryptedVolume,
path: String,
) : EncryptedFileProvider.ExportedFile(path) {
private val exportedFile: EncryptedFileProvider.ExportedFile by lazy {
val size = encryptedVolume.getAttr(path)?.size ?: run {
Log.e(TAG, "stat() failed")
throw RuntimeException("stat() failed")
}
val exportedFile = encryptedFileProvider.createFile(path, size) ?: run {
Log.e(TAG, "Can't create exported file")
throw RuntimeException("Can't create exported file")
}
if (!encryptedFileProvider.exportFile(exportedFile, encryptedVolume)) {
Log.e(TAG, "File export failed")
throw RuntimeException("File export failed")
}
exportedFile
}
override fun open(mode: Int, furtive: Boolean) = exportedFile.open(mode, furtive)
override fun free() = exportedFile.free()
}
override fun openDocument( override fun openDocument(
documentId: String, documentId: String,
mode: String, mode: String,
@ -210,11 +195,20 @@ class VolumeProvider: DocumentsProvider() {
): ParcelFileDescriptor? { ): ParcelFileDescriptor? {
if (!usfExpose) return null if (!usfExpose) return null
val document = parseDocumentId(documentId) ?: return null val document = parseDocumentId(documentId) ?: return null
val size = document.encryptedVolume.getAttr(document.path)?.size ?: run {
val lazyExportedFile = LazyExportedFile(encryptedFileProvider, document.encryptedVolume, document.path) Log.e(TAG, "stat() failed")
return null
}
val exportedFile = encryptedFileProvider.createFile(document.path, size) ?: run {
Log.e(TAG, "Can't create exported file")
return null
}
if (!encryptedFileProvider.exportFile(exportedFile, document.encryptedVolume)) {
Log.e(TAG, "File export failed")
return null
}
val result = encryptedFileProvider.openFile( val result = encryptedFileProvider.openFile(
lazyExportedFile, exportedFile,
mode, mode,
document.encryptedVolume, document.encryptedVolume,
volumeManager.getCoroutineScope(document.volumeId), volumeManager.getCoroutineScope(document.volumeId),
@ -236,21 +230,14 @@ class VolumeProvider: DocumentsProvider() {
): String? { ): String? {
if (!usfExpose || !usfSafWrite) return null if (!usfExpose || !usfSafWrite) return null
val document = parseDocumentId(parentDocumentId) ?: return null val document = parseDocumentId(parentDocumentId) ?: return null
val path = PathUtils.pathJoin(document.path, displayName) val newFile = PathUtils.pathJoin(document.path, displayName)
var success = false val f = document.encryptedVolume.openFileWriteMode(newFile)
if (mimeType == DocumentsContract.Document.MIME_TYPE_DIR) { return if (f == -1L) {
success = document.encryptedVolume.mkdir(path) Log.e(TAG, "Failed to create file: $document")
} else {
val f = document.encryptedVolume.openFileWriteMode(path)
if (f != -1L) {
document.encryptedVolume.closeFile(f)
success = true
}
}
return if (success) {
document.rootId+path
} else {
null null
} else {
document.encryptedVolume.closeFile(f)
document.rootId+"/"+newFile
} }
} }

View File

@ -11,12 +11,10 @@ import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.widget.ImageButton import android.widget.ImageButton
import android.widget.ProgressBar
import android.widget.TextView import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.activity.addCallback import androidx.activity.addCallback
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
@ -25,13 +23,10 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.MainScope import kotlinx.coroutines.MainScope
import kotlinx.coroutines.async
import kotlinx.coroutines.cancel import kotlinx.coroutines.cancel
import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.yield import kotlinx.coroutines.withContext
import sushi.hardcore.droidfs.BaseActivity import sushi.hardcore.droidfs.BaseActivity
import sushi.hardcore.droidfs.Constants import sushi.hardcore.droidfs.Constants
import sushi.hardcore.droidfs.EncryptedFileProvider import sushi.hardcore.droidfs.EncryptedFileProvider
@ -54,7 +49,6 @@ import sushi.hardcore.droidfs.file_viewers.VideoPlayer
import sushi.hardcore.droidfs.filesystems.EncryptedVolume import sushi.hardcore.droidfs.filesystems.EncryptedVolume
import sushi.hardcore.droidfs.filesystems.Stat import sushi.hardcore.droidfs.filesystems.Stat
import sushi.hardcore.droidfs.util.PathUtils import sushi.hardcore.droidfs.util.PathUtils
import sushi.hardcore.droidfs.util.UIUtils
import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder
import sushi.hardcore.droidfs.widgets.EditTextDialog import sushi.hardcore.droidfs.widgets.EditTextDialog
@ -75,7 +69,6 @@ open class BaseExplorerActivity : BaseActivity(), ExplorerElementAdapter.Listene
} }
protected lateinit var fileOperationService: FileOperationService protected lateinit var fileOperationService: FileOperationService
protected val activityScope = MainScope() protected val activityScope = MainScope()
private var directoryLoadingTask: Job? = null
protected lateinit var explorerElements: MutableList<ExplorerElement> protected lateinit var explorerElements: MutableList<ExplorerElement>
protected lateinit var explorerAdapter: ExplorerElementAdapter protected lateinit var explorerAdapter: ExplorerElementAdapter
protected lateinit var app: VolumeManagerApp protected lateinit var app: VolumeManagerApp
@ -86,7 +79,6 @@ open class BaseExplorerActivity : BaseActivity(), ExplorerElementAdapter.Listene
private lateinit var titleText: TextView private lateinit var titleText: TextView
private lateinit var recycler_view_explorer: RecyclerView private lateinit var recycler_view_explorer: RecyclerView
private lateinit var refresher: SwipeRefreshLayout private lateinit var refresher: SwipeRefreshLayout
private lateinit var loader: ProgressBar
private lateinit var textDirEmpty: TextView private lateinit var textDirEmpty: TextView
private lateinit var currentPathText: TextView private lateinit var currentPathText: TextView
private lateinit var numberOfFilesText: TextView private lateinit var numberOfFilesText: TextView
@ -109,7 +101,6 @@ open class BaseExplorerActivity : BaseActivity(), ExplorerElementAdapter.Listene
init() init()
recycler_view_explorer = findViewById(R.id.recycler_view_explorer) recycler_view_explorer = findViewById(R.id.recycler_view_explorer)
refresher = findViewById(R.id.refresher) refresher = findViewById(R.id.refresher)
loader = findViewById(R.id.loader)
textDirEmpty = findViewById(R.id.text_dir_empty) textDirEmpty = findViewById(R.id.text_dir_empty)
currentPathText = findViewById(R.id.current_path_text) currentPathText = findViewById(R.id.current_path_text)
numberOfFilesText = findViewById(R.id.number_of_files_text) numberOfFilesText = findViewById(R.id.number_of_files_text)
@ -268,27 +259,6 @@ open class BaseExplorerActivity : BaseActivity(), ExplorerElementAdapter.Listene
.show() .show()
} }
protected fun createNewFile(callback: (Long) -> Unit) {
EditTextDialog(this, R.string.enter_file_name) {
if (it.isEmpty()) {
Toast.makeText(this, R.string.error_filename_empty, Toast.LENGTH_SHORT).show()
createNewFile(callback)
} else {
val filePath = PathUtils.pathJoin(currentDirectoryPath, it)
val handleID = encryptedVolume.openFileWriteMode(filePath)
if (handleID == -1L) {
CustomAlertDialogBuilder(this, theme)
.setTitle(R.string.error)
.setMessage(R.string.file_creation_failed)
.setPositiveButton(R.string.ok, null)
.show()
} else {
callback(handleID)
}
}
}.show()
}
private fun setVolumeNameTitle() { private fun setVolumeNameTitle() {
titleText.text = getString(R.string.volume, volumeName) titleText.text = getString(R.string.volume, volumeName)
} }
@ -342,15 +312,17 @@ open class BaseExplorerActivity : BaseActivity(), ExplorerElementAdapter.Listene
} }
private fun displayExplorerElements() { private fun displayExplorerElements() {
ExplorerElement.sortBy(sortOrderValues[currentSortOrderIndex], foldersFirst, explorerElements) synchronized(this) {
ExplorerElement.sortBy(sortOrderValues[currentSortOrderIndex], foldersFirst, explorerElements)
}
unselectAll(false) unselectAll(false)
loader.isVisible = false
recycler_view_explorer.isVisible = true
explorerAdapter.explorerElements = explorerElements explorerAdapter.explorerElements = explorerElements
val sharedPrefsEditor = sharedPrefs.edit()
sharedPrefsEditor.putString(Constants.SORT_ORDER_KEY, sortOrderValues[currentSortOrderIndex])
sharedPrefsEditor.apply()
} }
private suspend fun recursiveSetSize(directory: ExplorerElement) { private fun recursiveSetSize(directory: ExplorerElement) {
yield()
for (child in encryptedVolume.readDir(directory.fullPath) ?: return) { for (child in encryptedVolume.readDir(directory.fullPath) ?: return) {
if (child.isDirectory) { if (child.isDirectory) {
recursiveSetSize(child) recursiveSetSize(child)
@ -374,16 +346,15 @@ open class BaseExplorerActivity : BaseActivity(), ExplorerElementAdapter.Listene
} }
} }
protected fun setCurrentPath(path: String, onDisplayed: (() -> Unit)? = null) = lifecycleScope.launch { protected fun setCurrentPath(path: String, onDisplayed: (() -> Unit)? = null) {
directoryLoadingTask?.cancelAndJoin() synchronized(this) {
recycler_view_explorer.isVisible = false explorerElements = encryptedVolume.readDir(path) ?: return
loader.isVisible = true if (path != "/") {
explorerElements = encryptedVolume.readDir(path) ?: return@launch explorerElements.add(
if (path != "/") { 0,
explorerElements.add( ExplorerElement("..", Stat.parentFolderStat(), parentPath = currentDirectoryPath)
0, )
ExplorerElement("..", Stat.parentFolderStat(), parentPath = currentDirectoryPath) }
)
} }
textDirEmpty.visibility = if (explorerElements.size == 0) View.VISIBLE else View.GONE textDirEmpty.visibility = if (explorerElements.size == 0) View.VISIBLE else View.GONE
currentDirectoryPath = path currentDirectoryPath = path
@ -391,19 +362,22 @@ open class BaseExplorerActivity : BaseActivity(), ExplorerElementAdapter.Listene
displayNumberOfElements(numberOfFilesText, R.string.one_file, R.string.multiple_files, explorerElements.count { it.isRegularFile }) displayNumberOfElements(numberOfFilesText, R.string.one_file, R.string.multiple_files, explorerElements.count { it.isRegularFile })
displayNumberOfElements(numberOfFoldersText, R.string.one_folder, R.string.multiple_folders, explorerElements.count { it.isDirectory }) displayNumberOfElements(numberOfFoldersText, R.string.one_folder, R.string.multiple_folders, explorerElements.count { it.isDirectory })
if (mapFolders) { if (mapFolders) {
var totalSize: Long = 0 lifecycleScope.launch {
directoryLoadingTask = launch(Dispatchers.IO) { var totalSize: Long = 0
for (element in explorerElements) { withContext(Dispatchers.IO) {
if (element.isDirectory) { synchronized(this@BaseExplorerActivity) {
recursiveSetSize(element) for (element in explorerElements) {
if (element.isDirectory) {
recursiveSetSize(element)
}
totalSize += element.stat.size
}
} }
totalSize += element.stat.size
} }
displayExplorerElements()
totalSizeText.text = getString(R.string.total_size, PathUtils.formatSize(totalSize))
onDisplayed?.invoke()
} }
directoryLoadingTask!!.join()
displayExplorerElements()
totalSizeText.text = getString(R.string.total_size, PathUtils.formatSize(totalSize))
onDisplayed?.invoke()
} else { } else {
displayExplorerElements() displayExplorerElements()
totalSizeText.text = getString( totalSizeText.text = getString(
@ -586,6 +560,14 @@ open class BaseExplorerActivity : BaseActivity(), ExplorerElementAdapter.Listene
} }
} }
private fun setMenuIconTint(menu: Menu, iconColor: Int, menuItemId: Int, drawableId: Int) {
menu.findItem(menuItemId)?.let {
it.icon = ContextCompat.getDrawable(this, drawableId)?.apply {
setTint(iconColor)
}
}
}
override fun onCreateOptionsMenu(menu: Menu): Boolean { override fun onCreateOptionsMenu(menu: Menu): Boolean {
menu.findItem(R.id.rename).isVisible = false menu.findItem(R.id.rename).isVisible = false
menu.findItem(R.id.open_as)?.isVisible = false menu.findItem(R.id.open_as)?.isVisible = false
@ -593,10 +575,9 @@ open class BaseExplorerActivity : BaseActivity(), ExplorerElementAdapter.Listene
menu.findItem(R.id.external_open)?.isVisible = false menu.findItem(R.id.external_open)?.isVisible = false
} }
val noItemSelected = explorerAdapter.selectedItems.isEmpty() val noItemSelected = explorerAdapter.selectedItems.isEmpty()
with(UIUtils.getMenuIconNeutralTint(this, menu)) { val iconColor = ContextCompat.getColor(this, R.color.neutralIconTint)
applyTo(R.id.sort, R.drawable.icon_sort) setMenuIconTint(menu, iconColor, R.id.sort, R.drawable.icon_sort)
applyTo(R.id.share, R.drawable.icon_share) setMenuIconTint(menu, iconColor, R.id.share, R.drawable.icon_share)
}
menu.findItem(R.id.sort).isVisible = noItemSelected menu.findItem(R.id.sort).isVisible = noItemSelected
menu.findItem(R.id.lock).isVisible = noItemSelected menu.findItem(R.id.lock).isVisible = noItemSelected
menu.findItem(R.id.close).isVisible = noItemSelected menu.findItem(R.id.close).isVisible = noItemSelected
@ -626,13 +607,7 @@ open class BaseExplorerActivity : BaseActivity(), ExplorerElementAdapter.Listene
.setTitle(R.string.sort_order) .setTitle(R.string.sort_order)
.setSingleChoiceItems(sortOrderEntries, currentSortOrderIndex) { dialog, which -> .setSingleChoiceItems(sortOrderEntries, currentSortOrderIndex) { dialog, which ->
currentSortOrderIndex = which currentSortOrderIndex = which
// displayExplorerElements must not be called if directoryLoadingTask is active displayExplorerElements()
if (directoryLoadingTask?.isActive != true) {
displayExplorerElements()
}
val sharedPrefsEditor = sharedPrefs.edit()
sharedPrefsEditor.putString(Constants.SORT_ORDER_KEY, sortOrderValues[currentSortOrderIndex])
sharedPrefsEditor.apply()
dialog.dismiss() dialog.dismiss()
} }
.setNegativeButton(R.string.cancel, null) .setNegativeButton(R.string.cancel, null)

View File

@ -68,11 +68,7 @@ class ExplorerActivity : BaseExplorerActivity() {
private val pickFiles = registerForActivityResult(ActivityResultContracts.OpenMultipleDocuments()) { uris -> private val pickFiles = registerForActivityResult(ActivityResultContracts.OpenMultipleDocuments()) { uris ->
if (uris != null) { if (uris != null) {
for (uri in uris) { for (uri in uris) {
try { contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
} catch (e: SecurityException) {
e.printStackTrace()
}
} }
importFilesFromUris(uris) { importFilesFromUris(uris) {
onImportComplete(uris) onImportComplete(uris)
@ -193,11 +189,9 @@ class ExplorerActivity : BaseExplorerActivity() {
pickImportDirectory.launch(null) pickImportDirectory.launch(null)
} }
"createFile" -> { "createFile" -> {
createNewFile { EditTextDialog(this, R.string.enter_file_name) {
encryptedVolume.closeFile(it) createNewFile(it)
setCurrentPath(currentDirectoryPath) }.show()
invalidateOptionsMenu()
}
} }
"createFolder" -> { "createFolder" -> {
openDialogCreateFolder() openDialogCreateFolder()
@ -225,6 +219,26 @@ class ExplorerActivity : BaseExplorerActivity() {
cancelItemAction() cancelItemAction()
} }
private fun createNewFile(fileName: String){
if (fileName.isEmpty()) {
Toast.makeText(this, R.string.error_filename_empty, Toast.LENGTH_SHORT).show()
} else {
val filePath = PathUtils.pathJoin(currentDirectoryPath, fileName)
val handleID = encryptedVolume.openFileWriteMode(filePath)
if (handleID == -1L) {
CustomAlertDialogBuilder(this, theme)
.setTitle(R.string.error)
.setMessage(R.string.file_creation_failed)
.setPositiveButton(R.string.ok, null)
.show()
} else {
encryptedVolume.closeFile(handleID)
setCurrentPath(currentDirectoryPath)
invalidateOptionsMenu()
}
}
}
override fun onCreateOptionsMenu(menu: Menu): Boolean { override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.explorer, menu) menuInflater.inflate(R.menu.explorer, menu)
val result = super.onCreateOptionsMenu(menu) val result = super.onCreateOptionsMenu(menu)

View File

@ -9,8 +9,6 @@ import com.google.android.material.floatingactionbutton.FloatingActionButton
import sushi.hardcore.droidfs.R import sushi.hardcore.droidfs.R
import sushi.hardcore.droidfs.util.IntentUtils import sushi.hardcore.droidfs.util.IntentUtils
import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder
import java.nio.CharBuffer
import java.nio.charset.StandardCharsets
class ExplorerActivityDrop : BaseExplorerActivity() { class ExplorerActivityDrop : BaseExplorerActivity() {
@ -32,15 +30,15 @@ class ExplorerActivityDrop : BaseExplorerActivity() {
return when (item.itemId) { return when (item.itemId) {
R.id.validate -> { R.id.validate -> {
val extras = intent.extras val extras = intent.extras
val success = if (extras != null && extras.containsKey(Intent.EXTRA_STREAM)) { val errorMsg: String? = if (extras != null && extras.containsKey(Intent.EXTRA_STREAM)) {
when (intent.action) { when (intent.action) {
Intent.ACTION_SEND -> { Intent.ACTION_SEND -> {
val uri = IntentUtils.getParcelableExtra<Uri>(intent, Intent.EXTRA_STREAM) val uri = IntentUtils.getParcelableExtra<Uri>(intent, Intent.EXTRA_STREAM)
if (uri == null) { if (uri == null) {
false getString(R.string.share_intent_parsing_failed)
} else { } else {
importFilesFromUris(listOf(uri), ::onImported) importFilesFromUris(listOf(uri), ::onImported)
true null
} }
} }
Intent.ACTION_SEND_MULTIPLE -> { Intent.ACTION_SEND_MULTIPLE -> {
@ -52,34 +50,20 @@ class ExplorerActivityDrop : BaseExplorerActivity() {
} }
if (uris != null) { if (uris != null) {
importFilesFromUris(uris, ::onImported) importFilesFromUris(uris, ::onImported)
true null
} else { } else {
false getString(R.string.share_intent_parsing_failed)
} }
} }
else -> false else -> getString(R.string.share_intent_parsing_failed)
} }
} else if ((intent.clipData?.itemCount ?: 0) > 0) {
val byteBuffer = StandardCharsets.UTF_8.encode(CharBuffer.wrap(intent.clipData!!.getItemAt(0).text))
val byteArray = ByteArray(byteBuffer.remaining())
byteBuffer.get(byteArray)
val size = byteArray.size.toLong()
createNewFile {
var offset = 0L
while (offset < size) {
offset += encryptedVolume.write(it, offset, byteArray, offset, size-offset)
}
encryptedVolume.closeFile(it)
onImported()
}
true
} else { } else {
false getString(R.string.share_intent_parsing_failed)
} }
if (!success) { errorMsg?.let {
CustomAlertDialogBuilder(this, theme) CustomAlertDialogBuilder(this, theme)
.setTitle(R.string.error) .setTitle(R.string.error)
.setMessage(R.string.share_intent_parsing_failed) .setMessage(it)
.setPositiveButton(R.string.ok, null) .setPositiveButton(R.string.ok, null)
.show() .show()
} }

View File

@ -1,11 +1,8 @@
package sushi.hardcore.droidfs.file_viewers package sushi.hardcore.droidfs.file_viewers
import androidx.annotation.OptIn import com.google.android.exoplayer2.ExoPlayer
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.ExoPlayer
import sushi.hardcore.droidfs.databinding.ActivityAudioPlayerBinding import sushi.hardcore.droidfs.databinding.ActivityAudioPlayerBinding
@OptIn(UnstableApi::class)
class AudioPlayer: MediaPlayer(){ class AudioPlayer: MediaPlayer(){
private lateinit var binding: ActivityAudioPlayerBinding private lateinit var binding: ActivityAudioPlayerBinding

View File

@ -1,19 +1,15 @@
package sushi.hardcore.droidfs.file_viewers package sushi.hardcore.droidfs.file_viewers
import android.net.Uri import android.net.Uri
import androidx.media3.common.C import com.google.android.exoplayer2.C
import androidx.annotation.OptIn import com.google.android.exoplayer2.upstream.DataSource
import androidx.media3.common.util.UnstableApi import com.google.android.exoplayer2.upstream.DataSpec
import androidx.media3.datasource.DataSource import com.google.android.exoplayer2.upstream.TransferListener
import androidx.media3.datasource.DataSpec
import androidx.media3.datasource.TransferListener
import sushi.hardcore.droidfs.Constants import sushi.hardcore.droidfs.Constants
import sushi.hardcore.droidfs.filesystems.EncryptedVolume import sushi.hardcore.droidfs.filesystems.EncryptedVolume
import kotlin.math.min import kotlin.math.min
@OptIn(UnstableApi::class) class EncryptedVolumeDataSource(private val encryptedVolume: EncryptedVolume, private val filePath: String): DataSource {
class EncryptedVolumeDataSource(private val encryptedVolume: EncryptedVolume, private val filePath: String):
DataSource {
private var fileHandle = -1L private var fileHandle = -1L
private var fileOffset: Long = 0 private var fileOffset: Long = 0
private var bytesRemaining: Long = -1 private var bytesRemaining: Long = -1

View File

@ -1,22 +1,16 @@
package sushi.hardcore.droidfs.file_viewers package sushi.hardcore.droidfs.file_viewers
import android.view.WindowManager import android.view.WindowManager
import androidx.annotation.OptIn import com.google.android.exoplayer2.*
import androidx.media3.common.MediaItem import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory
import androidx.media3.common.PlaybackException import com.google.android.exoplayer2.source.MediaSource
import androidx.media3.common.Player import com.google.android.exoplayer2.source.ProgressiveMediaSource
import androidx.media3.common.VideoSize import com.google.android.exoplayer2.video.VideoSize
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.source.MediaSource
import androidx.media3.exoplayer.source.ProgressiveMediaSource
import androidx.media3.extractor.DefaultExtractorsFactory
import sushi.hardcore.droidfs.Constants import sushi.hardcore.droidfs.Constants
import sushi.hardcore.droidfs.R import sushi.hardcore.droidfs.R
import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder
import java.io.File import java.io.File
@OptIn(UnstableApi::class)
abstract class MediaPlayer: FileViewerActivity() { abstract class MediaPlayer: FileViewerActivity() {
private lateinit var player: ExoPlayer private lateinit var player: ExoPlayer

View File

@ -3,8 +3,8 @@ package sushi.hardcore.droidfs.file_viewers
import android.content.pm.ActivityInfo import android.content.pm.ActivityInfo
import android.content.res.Configuration import android.content.res.Configuration
import android.view.View import android.view.View
import androidx.media3.exoplayer.ExoPlayer import com.google.android.exoplayer2.ExoPlayer
import androidx.media3.ui.PlayerView import com.google.android.exoplayer2.ui.StyledPlayerView
import sushi.hardcore.droidfs.databinding.ActivityVideoPlayerBinding import sushi.hardcore.droidfs.databinding.ActivityVideoPlayerBinding
class VideoPlayer: MediaPlayer() { class VideoPlayer: MediaPlayer() {
@ -19,7 +19,7 @@ class VideoPlayer: MediaPlayer() {
setContentView(binding.root) setContentView(binding.root)
applyNavigationBarMargin(binding.root) applyNavigationBarMargin(binding.root)
binding.videoPlayer.doubleTapOverlay = binding.doubleTapOverlay binding.videoPlayer.doubleTapOverlay = binding.doubleTapOverlay
binding.videoPlayer.setControllerVisibilityListener(PlayerView.ControllerVisibilityListener { visibility -> binding.videoPlayer.setControllerVisibilityListener(StyledPlayerView.ControllerVisibilityListener { visibility ->
binding.topBar.visibility = visibility binding.topBar.visibility = visibility
if (visibility == View.VISIBLE) { if (visibility == View.VISIBLE) {
showPartialSystemUi() showPartialSystemUi()

View File

@ -3,7 +3,6 @@ package sushi.hardcore.droidfs.util
import android.content.ActivityNotFoundException import android.content.ActivityNotFoundException
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import android.os.Build
import android.os.Environment import android.os.Environment
import android.os.storage.StorageManager import android.os.storage.StorageManager
import android.provider.DocumentsContract import android.provider.DocumentsContract
@ -112,27 +111,24 @@ object PathUtils {
} }
} }
Log.d(PATH_RESOLVER_TAG, "getExternalFilesDirs failed") Log.d(PATH_RESOLVER_TAG, "getExternalFilesDirs failed")
// Don't risk to be killed by SELinux on newer Android versions try {
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) { val process = ProcessBuilder("mount").redirectErrorStream(true).start().apply { waitFor() }
try { process.inputStream.readBytes().decodeToString().split("\n").forEach { line ->
val process = ProcessBuilder("mount").redirectErrorStream(true).start().apply { waitFor() } if (line.startsWith("/dev/block/vold")) {
process.inputStream.readBytes().decodeToString().split("\n").forEach { line -> Log.d(PATH_RESOLVER_TAG, "mount: $line")
if (line.startsWith("/dev/block/vold")) { val fields = line.split(" ")
Log.d(PATH_RESOLVER_TAG, "mount: $line") if (fields.size >= 3) {
val fields = line.split(" ") val path = fields[2]
if (fields.size >= 3) { if (File(path).name == name) {
val path = fields[2] return path
if (File(path).name == name) {
return path
}
} }
} }
} }
} catch (e: Exception) {
e.printStackTrace()
} }
Log.d(PATH_RESOLVER_TAG, "mount processing failed") } catch (e: Exception) {
e.printStackTrace()
} }
Log.d(PATH_RESOLVER_TAG, "mount processing failed")
return null return null
} }

View File

@ -1,42 +0,0 @@
package sushi.hardcore.droidfs.util
import android.content.Context
import android.view.Menu
import android.widget.EditText
import androidx.core.content.ContextCompat
import sushi.hardcore.droidfs.R
import java.nio.CharBuffer
import java.nio.charset.StandardCharsets
import java.util.*
object UIUtils {
fun encodeEditTextContent(editText: EditText): ByteArray {
val charArray = CharArray(editText.text.length)
editText.text.getChars(0, editText.text.length, charArray, 0)
val byteBuffer = StandardCharsets.UTF_8.encode(CharBuffer.wrap(charArray))
Arrays.fill(charArray, Char.MIN_VALUE)
val byteArray = ByteArray(byteBuffer.remaining())
byteBuffer.get(byteArray)
Wiper.wipe(byteBuffer)
return byteArray
}
class MenuIconColor(
private val context: Context,
private val menu: Menu,
private val color: Int
) {
fun applyTo(menuItemId: Int, drawableId: Int) {
menu.findItem(menuItemId)?.let {
it.icon = ContextCompat.getDrawable(context, drawableId)?.apply {
setTint(color)
}
}
}
}
fun getMenuIconNeutralTint(context: Context, menu: Menu) = MenuIconColor(
context, menu,
ContextCompat.getColor(context, R.color.neutralIconTint),
)
}

View File

@ -3,7 +3,7 @@ package sushi.hardcore.droidfs.util
import java.lang.Integer.max import java.lang.Integer.max
class Version(inputVersion: String) : Comparable<Version> { class Version(inputVersion: String) : Comparable<Version> {
private val version: String val version: String
init { init {
val regex = "[0-9]+(\\.[0-9]+)*".toRegex() val regex = "[0-9]+(\\.[0-9]+)*".toRegex()
@ -24,6 +24,4 @@ class Version(inputVersion: String) : Comparable<Version> {
} }
0 0
} }
override fun toString() = version
} }

View File

@ -0,0 +1,19 @@
package sushi.hardcore.droidfs.util
import android.widget.EditText
import java.nio.CharBuffer
import java.nio.charset.StandardCharsets
import java.util.*
object WidgetUtil {
fun encodeEditTextContent(editText: EditText): ByteArray {
val charArray = CharArray(editText.text.length)
editText.text.getChars(0, editText.text.length, charArray, 0)
val byteBuffer = StandardCharsets.UTF_8.encode(CharBuffer.wrap(charArray))
Arrays.fill(charArray, Char.MIN_VALUE)
val byteArray = ByteArray(byteBuffer.remaining())
byteBuffer.get(byteArray)
Wiper.wipe(byteBuffer)
return byteArray
}
}

View File

@ -185,14 +185,14 @@ internal class CircleClipTapView(context: Context, attrs: AttributeSet): View(co
updatePathShape() updatePathShape()
} }
override fun onDraw(canvas: Canvas) { override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas) super.onDraw(canvas)
// Background // Background
canvas.clipPath(shapePath) canvas?.clipPath(shapePath)
canvas.drawPath(shapePath, backgroundPaint) canvas?.drawPath(shapePath, backgroundPaint)
// Circle // Circle
canvas.drawCircle(cX, cY, currentRadius, circlePaint) canvas?.drawCircle(cX, cY, currentRadius, circlePaint)
} }
} }

View File

@ -2,19 +2,29 @@ package sushi.hardcore.droidfs.widgets
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.content.res.Configuration
import android.media.session.PlaybackState import android.media.session.PlaybackState
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.util.AttributeSet import android.util.AttributeSet
import android.view.GestureDetector import android.view.GestureDetector
import android.view.MotionEvent import android.view.MotionEvent
import androidx.media3.ui.PlayerView import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.FrameLayout
import android.widget.LinearLayout
import androidx.core.view.GestureDetectorCompat
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
import com.google.android.exoplayer2.ui.StyledPlayerView
import sushi.hardcore.droidfs.R
class DoubleTapPlayerView @JvmOverloads constructor( class DoubleTapPlayerView @JvmOverloads constructor(
context: Context, context: Context,
attrs: AttributeSet? = null, attrs: AttributeSet? = null,
defStyleAttr: Int = 0 defStyleAttr: Int = 0
) : PlayerView(context, attrs, defStyleAttr) { ) : StyledPlayerView(context, attrs, defStyleAttr) {
companion object { companion object {
const val SEEK_SECONDS = 10 const val SEEK_SECONDS = 10
@ -65,7 +75,22 @@ class DoubleTapPlayerView @JvmOverloads constructor(
handler.postDelayed(stopDoubleTap, 700) handler.postDelayed(stopDoubleTap, 700)
} }
} }
private val gestureDetector = GestureDetector(context, gestureListener) private val gestureDetector = GestureDetectorCompat(context, gestureListener)
private val density by lazy {
context.resources.displayMetrics.density
}
private val originalExoIconPaddingBottom by lazy {
resources.getDimension(R.dimen.exo_icon_padding_bottom)
}
private val originalExoIconSize by lazy {
resources.getDimension(R.dimen.exo_icon_size)
}
init {
if (resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) {
handleOrientationChange(Configuration.ORIENTATION_LANDSCAPE)
}
}
@SuppressLint("ClickableViewAccessibility") @SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(event: MotionEvent): Boolean { override fun onTouchEvent(event: MotionEvent): Boolean {
@ -110,4 +135,35 @@ class DoubleTapPlayerView @JvmOverloads constructor(
} }
} }
} }
private fun updateButtonSize(orientation: Int) {
val size = (if (orientation == Configuration.ORIENTATION_LANDSCAPE) 45*density else originalExoIconSize).toInt()
listOf(R.id.exo_prev, R.id.exo_rew_with_amount, R.id.exo_play_pause, R.id.exo_ffwd_with_amount, R.id.exo_next).forEach {
findViewById<View>(it).updateLayoutParams {
width = size
height = size
}
}
// fix text vertical alignment inside icons
val paddingBottom = (if (orientation == Configuration.ORIENTATION_LANDSCAPE) 15*density else originalExoIconPaddingBottom).toInt()
listOf(R.id.exo_rew_with_amount, R.id.exo_ffwd_with_amount).forEach {
findViewById<Button>(it).updatePadding(bottom = paddingBottom)
}
}
private fun handleOrientationChange(orientation: Int) {
val centerControls = findViewById<LinearLayout>(R.id.exo_center_controls)
(centerControls.parent as ViewGroup).removeView(centerControls)
findViewById<FrameLayout>(if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
R.id.center_controls_bar
} else {
R.id.center_controls_external
}).addView(centerControls)
updateButtonSize(orientation)
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
handleOrientationChange(newConfig.orientation)
}
} }

View File

@ -40,7 +40,7 @@ struct Muxer {
jmethodID seek_method_id; jmethodID seek_method_id;
}; };
int write_packet(void* opaque, const uint8_t* buff, int buff_size) { int write_packet(void* opaque, uint8_t* buff, int buff_size) {
struct Muxer* muxer = opaque; struct Muxer* muxer = opaque;
JNIEnv *env; JNIEnv *env;
(*muxer->jvm)->GetEnv(muxer->jvm, (void **) &env, JNI_VERSION_1_6); (*muxer->jvm)->GetEnv(muxer->jvm, (void **) &env, JNI_VERSION_1_6);
@ -108,8 +108,8 @@ Java_sushi_hardcore_droidfs_video_1recording_FFmpegMuxer_addVideoTrack(JNIEnv *e
stream->codecpar->height = height; stream->codecpar->height = height;
stream->codecpar->format = AV_PIX_FMT_YUVJ420P; stream->codecpar->format = AV_PIX_FMT_YUVJ420P;
stream->time_base = (AVRational) {1, frame_rate}; stream->time_base = (AVRational) {1, frame_rate};
AVPacketSideData *side_data_packet = av_packet_side_data_new(&stream->codecpar->coded_side_data, &stream->codecpar->nb_coded_side_data, AV_PKT_DATA_DISPLAYMATRIX, sizeof(int32_t) * 9, 0); uint8_t* matrix = av_stream_new_side_data(stream, AV_PKT_DATA_DISPLAYMATRIX, sizeof(int32_t) * 9);
av_display_rotation_set((int32_t *) side_data_packet->data, orientation_hint); av_display_rotation_set((int32_t *) matrix, orientation_hint);
return stream->index; return stream->index;
} }

View File

@ -1,5 +0,0 @@
<vector android:height="24dp"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="?attr/colorAccent" android:pathData="M5,16c0,3.87 3.13,7 7,7s7,-3.13 7,-7v-4L5,12v4zM16.12,4.37l2.1,-2.1 -0.82,-0.83 -2.3,2.31C14.16,3.28 13.12,3 12,3s-2.16,0.28 -3.09,0.75L6.6,1.44l-0.82,0.83 2.1,2.1C6.14,5.64 5,7.68 5,10v1h14v-1c0,-2.32 -1.14,-4.36 -2.88,-5.63zM9,9c-0.55,0 -1,-0.45 -1,-1s0.45,-1 1,-1 1,0.45 1,1 -0.45,1 -1,1zM15,9c-0.55,0 -1,-0.45 -1,-1s0.45,-1 1,-1 1,0.45 1,1 -0.45,1 -1,1z"/>
</vector>

View File

@ -1,5 +1,5 @@
<vector android:height="24dp" <vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24" android:viewportWidth="24" android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"> android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="?attr/colorAccent" android:pathData="M19.14,12.94c0.04,-0.3 0.06,-0.61 0.06,-0.94c0,-0.32 -0.02,-0.64 -0.07,-0.94l2.03,-1.58c0.18,-0.14 0.23,-0.41 0.12,-0.61l-1.92,-3.32c-0.12,-0.22 -0.37,-0.29 -0.59,-0.22l-2.39,0.96c-0.5,-0.38 -1.03,-0.7 -1.62,-0.94L14.4,2.81c-0.04,-0.24 -0.24,-0.41 -0.48,-0.41h-3.84c-0.24,0 -0.43,0.17 -0.47,0.41L9.25,5.35C8.66,5.59 8.12,5.92 7.63,6.29L5.24,5.33c-0.22,-0.08 -0.47,0 -0.59,0.22L2.74,8.87C2.62,9.08 2.66,9.34 2.86,9.48l2.03,1.58C4.84,11.36 4.8,11.69 4.8,12s0.02,0.64 0.07,0.94l-2.03,1.58c-0.18,0.14 -0.23,0.41 -0.12,0.61l1.92,3.32c0.12,0.22 0.37,0.29 0.59,0.22l2.39,-0.96c0.5,0.38 1.03,0.7 1.62,0.94l0.36,2.54c0.05,0.24 0.24,0.41 0.48,0.41h3.84c0.24,0 0.44,-0.17 0.47,-0.41l0.36,-2.54c0.59,-0.24 1.13,-0.56 1.62,-0.94l2.39,0.96c0.22,0.08 0.47,0 0.59,-0.22l1.92,-3.32c0.12,-0.22 0.07,-0.47 -0.12,-0.61L19.14,12.94zM12,15.6c-1.98,0 -3.6,-1.62 -3.6,-3.6s1.62,-3.6 3.6,-3.6s3.6,1.62 3.6,3.6S13.98,15.6 12,15.6z"/> <path android:fillColor="@android:color/white" android:pathData="M19.14,12.94c0.04,-0.3 0.06,-0.61 0.06,-0.94c0,-0.32 -0.02,-0.64 -0.07,-0.94l2.03,-1.58c0.18,-0.14 0.23,-0.41 0.12,-0.61l-1.92,-3.32c-0.12,-0.22 -0.37,-0.29 -0.59,-0.22l-2.39,0.96c-0.5,-0.38 -1.03,-0.7 -1.62,-0.94L14.4,2.81c-0.04,-0.24 -0.24,-0.41 -0.48,-0.41h-3.84c-0.24,0 -0.43,0.17 -0.47,0.41L9.25,5.35C8.66,5.59 8.12,5.92 7.63,6.29L5.24,5.33c-0.22,-0.08 -0.47,0 -0.59,0.22L2.74,8.87C2.62,9.08 2.66,9.34 2.86,9.48l2.03,1.58C4.84,11.36 4.8,11.69 4.8,12s0.02,0.64 0.07,0.94l-2.03,1.58c-0.18,0.14 -0.23,0.41 -0.12,0.61l1.92,3.32c0.12,0.22 0.37,0.29 0.59,0.22l2.39,-0.96c0.5,0.38 1.03,0.7 1.62,0.94l0.36,2.54c0.05,0.24 0.24,0.41 0.48,0.41h3.84c0.24,0 0.44,-0.17 0.47,-0.41l0.36,-2.54c0.59,-0.24 1.13,-0.56 1.62,-0.94l2.39,0.96c0.22,0.08 0.47,0 0.59,-0.22l1.92,-3.32c0.12,-0.22 0.07,-0.47 -0.12,-0.61L19.14,12.94zM12,15.6c-1.98,0 -3.6,-1.62 -3.6,-3.6s1.62,-3.6 3.6,-3.6s3.6,1.62 3.6,3.6S13.98,15.6 12,15.6z"/>
</vector> </vector>

View File

@ -15,11 +15,11 @@
android:textSize="@dimen/title_text_size" android:textSize="@dimen/title_text_size"
android:padding="10dp"/> android:padding="10dp"/>
<androidx.media3.ui.PlayerControlView <com.google.android.exoplayer2.ui.StyledPlayerControlView
android:id="@+id/audio_controller" android:id="@+id/audio_controller"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:controller_layout_id="@layout/audio_exo_player_control_view" app:controller_layout_id="@layout/audio_exo_styled_player_control_view"
app:show_timeout="0" app:show_timeout="0"
app:show_shuffle_button="true" app:show_shuffle_button="true"
app:repeat_toggle_modes="all|one"/> app:repeat_toggle_modes="all|one"/>

View File

@ -1,20 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<HorizontalScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/content"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textIsSelectable="true"
android:typeface="monospace" />
</HorizontalScrollView>
</ScrollView>

View File

@ -26,20 +26,20 @@
android:layout_gravity="center_horizontal" android:layout_gravity="center_horizontal"
android:background="@android:color/transparent" android:background="@android:color/transparent"
android:gravity="center" android:gravity="center"
android:padding="@dimen/exo_styled_controls_padding"
android:clipToPadding="false"
android:layoutDirection="ltr" android:layoutDirection="ltr"
android:layout_marginBottom="-40dp"> android:padding="@dimen/exo_styled_controls_padding"
android:layout_marginBottom="-40dp"
android:clipToPadding="false">
<ImageButton android:id="@id/exo_prev" <ImageButton android:id="@id/exo_prev"
style="@style/ExoStyledControls.Button.Center.Previous"/> style="@style/ExoStyledControls.Button.Center.Previous"/>
<include layout="@layout/exo_player_control_rewind_button" /> <include layout="@layout/exo_styled_player_control_rewind_button" />
<ImageButton android:id="@id/exo_play_pause" <ImageButton android:id="@id/exo_play_pause"
style="@style/ExoStyledControls.Button.Center.PlayPause"/> style="@style/ExoStyledControls.Button.Center.PlayPause"/>
<include layout="@layout/exo_player_control_ffwd_button" /> <include layout="@layout/exo_styled_player_control_ffwd_button" />
<ImageButton android:id="@id/exo_next" <ImageButton android:id="@id/exo_next"
style="@style/ExoStyledControls.Button.Center.Next"/> style="@style/ExoStyledControls.Button.Center.Next"/>

View File

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android">
<LinearLayout
android:id="@id/exo_center_controls"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:background="@android:color/transparent"
android:gravity="center"
android:layoutDirection="ltr"
android:padding="@dimen/exo_styled_controls_padding"
android:clipToPadding="false">
<ImageButton android:id="@id/exo_prev"
style="@style/ExoStyledControls.Button.Center.Previous"/>
<include layout="@layout/exo_styled_player_control_rewind_button" />
<ImageButton android:id="@id/exo_play_pause"
style="@style/ExoStyledControls.Button.Center.PlayPause"/>
<include layout="@layout/exo_styled_player_control_ffwd_button" />
<ImageButton android:id="@id/exo_next"
style="@style/ExoStyledControls.Button.Center.Next"/>
</LinearLayout>
</merge>

View File

@ -0,0 +1,148 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2020 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.
-->
<merge xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 0dp dimensions are used to prevent this view from influencing the size of
the parent view if it uses "wrap_content". It is expanded to occupy the
entirety of the parent in code, after the parent's size has been
determined. See: https://github.com/google/ExoPlayer/issues/8726.
-->
<View android:id="@id/exo_controls_background"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="@color/exo_black_opacity_60"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:orientation="vertical">
<FrameLayout
android:id="@+id/center_controls_external"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|center_horizontal">
<include layout="@layout/exo_center_controls"/>
</FrameLayout>
<FrameLayout android:id="@id/exo_bottom_bar"
android:layout_width="match_parent"
android:layout_height="@dimen/exo_styled_bottom_bar_height"
android:background="@color/exo_bottom_bar_background"
android:layoutDirection="ltr">
<LinearLayout android:id="@id/exo_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="@dimen/exo_styled_bottom_bar_time_padding"
android:paddingEnd="@dimen/exo_styled_bottom_bar_time_padding"
android:paddingLeft="@dimen/exo_styled_bottom_bar_time_padding"
android:paddingRight="@dimen/exo_styled_bottom_bar_time_padding"
android:layout_gravity="center_vertical|start"
android:layoutDirection="ltr">
<TextView android:id="@id/exo_position"
style="@style/ExoStyledControls.TimeText.Position"/>
<TextView
style="@style/ExoStyledControls.TimeText.Separator"/>
<TextView android:id="@id/exo_duration"
style="@style/ExoStyledControls.TimeText.Duration"/>
</LinearLayout>
<FrameLayout
android:id="@+id/center_controls_bar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"/>
<LinearLayout android:id="@id/exo_basic_controls"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical|end"
android:layoutDirection="ltr">
<ImageButton android:id="@id/exo_vr"
style="@style/ExoStyledControls.Button.Bottom.VR"/>
<ImageButton android:id="@id/exo_shuffle"
style="@style/ExoStyledControls.Button.Bottom.Shuffle"/>
<ImageButton android:id="@id/exo_repeat_toggle"
style="@style/ExoStyledControls.Button.Bottom.RepeatToggle"/>
<ImageButton android:id="@id/exo_subtitle"
style="@style/ExoStyledControls.Button.Bottom.CC"/>
<ImageButton android:id="@id/exo_settings"
style="@style/ExoStyledControls.Button.Bottom.Settings"/>
<ImageButton android:id="@id/exo_fullscreen"
style="@style/ExoStyledControls.Button.Bottom.FullScreen"/>
<ImageButton android:id="@id/exo_overflow_show"
style="@style/ExoStyledControls.Button.Bottom.OverflowShow"/>
</LinearLayout>
<HorizontalScrollView android:id="@id/exo_extra_controls_scroll_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical|end"
android:visibility="invisible">
<LinearLayout android:id="@id/exo_extra_controls"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layoutDirection="ltr">
<ImageButton android:id="@id/exo_overflow_hide"
style="@style/ExoStyledControls.Button.Bottom.OverflowHide"/>
</LinearLayout>
</HorizontalScrollView>
</FrameLayout>
</LinearLayout>
<View android:id="@id/exo_progress_placeholder"
android:layout_width="match_parent"
android:layout_height="@dimen/exo_styled_progress_layout_height"
android:layout_gravity="bottom"
android:layout_marginBottom="@dimen/exo_styled_progress_margin_bottom"/>
<LinearLayout android:id="@id/exo_minimal_controls"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_marginBottom="@dimen/exo_styled_minimal_controls_margin_bottom"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layoutDirection="ltr">
<ImageButton android:id="@id/exo_minimal_fullscreen"
style="@style/ExoStyledControls.Button.Bottom.FullScreen"/>
</LinearLayout>
</merge>

View File

@ -10,12 +10,6 @@
android:text="@string/dir_empty" android:text="@string/dir_empty"
android:visibility="gone"/> android:visibility="gone"/>
<ProgressBar
android:id="@+id/loader"
android:layout_centerInParent="true"
android:layout_width="40dp"
android:layout_height="40dp"/>
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout <androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/refresher" android:id="@+id/refresher"
android:layout_width="match_parent" android:layout_width="match_parent"

View File

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/save"
app:showAsAction="ifRoom"
android:icon="@drawable/icon_save"
android:title="@string/save" />
</menu>

View File

@ -252,20 +252,4 @@
<string name="black_theme">لون أسود داكن</string> <string name="black_theme">لون أسود داكن</string>
<string name="password_fallback">العودة إلى كلمة المرور</string> <string name="password_fallback">العودة إلى كلمة المرور</string>
<string name="password_fallback_summary">طلب كلمة المرور في حال فشل المصادقة ببصمة الأصبع</string> <string name="password_fallback_summary">طلب كلمة المرور في حال فشل المصادقة ببصمة الأصبع</string>
<string name="unknown_error_code">خطأ غير معروف: %d</string>
<string name="config_load_error">لا يمكن تحميل ملف الإعدادات. تأكد من صحّة مسار المجلد الآمن.</string>
<string name="wrong_password">لقد تعذر فك تشفر ملف الإعدادات. رجاءً قم بالتحقق من كلمة المرور.</string>
<string name="filesystem_id_changed">لقد إختلف معرف نظام الملفات الموجود في ملف الإعدادات عن آخر مرة فتحنا فيها هذا المجلد. قد يعني هذا أن أحد ما قد قام باستبدال ملفاتك بملفات أخرى بهدف إختراقك.</string>
<string name="inaccessible_base_dir">إن المجلد المشفر غير موجود أو لا يمكن الوصول إليه.</string>
<string name="task_failed">لقد فشلت عملية: %s</string>
<string name="usf_expose">كشف المجلدات المفتوحة</string>
<string name="usf_expose_summary">السماح للتطبيقات الأخرى بالوصول إلى المجلدات المشفرة عن طريق نظام "توفير المستندات" الخاص بنظام الأندرويد</string>
<string name="usf_saf_write">منح صلاحيات الكتابة</string>
<string name="usf_saf_write_summary">منح صلاحيات الكتابة عند فتح الملفات مع التطبيقات الأخرى</string>
<string name="saf">"إطار الوصول للقرص"</string>
<string name="tmp_export_failed">لقد فشل تصدير: %s</string>
<string name="export_failed_create">تعذر إنشاء الملف المستخرج</string>
<string name="export_failed_export">فشل إستخراج الملف</string>
<string name="export_mem">يتم الإستخراج إلى الذاكرة المؤقتة…</string>
<string name="export_disk">يتم الإستخراج إلى القرص…</string>
</resources> </resources>

View File

@ -1,20 +0,0 @@
<resources>
<string-array name="sort_orders_entries">
<item>Name</item>
<item>Größe</item>
<item>Datum</item>
<item>Name (absteigend)</item>
<item>Größe (absteigend)</item>
<item>Datum (absteigend)</item>
</string-array>
<string-array name="color_names">
<item>Grün</item>
<item>Rot</item>
<item>Blau</item>
<item>Gelb</item>
<item>Orange</item>
<item>Lila</item>
<item>Rosa</item>
</string-array>
</resources>

View File

@ -1,276 +0,0 @@
<resources>
<string name="app_name">DroidFS</string>
<string name="create_volume">Laufwerk erstellen</string>
<string name="open">Öffnen</string>
<string name="create">Erstellen</string>
<string name="change_password">Passwort ändern</string>
<string name="password">Passwort</string>
<string name="import_files">Dateien importieren/verschlüsseln</string>
<string name="import_folder">Ordner importieren/verschlüsseln</string>
<string name="discovering_files">Dateien aufspüren…</string>
<string name="mkdir">Ordner erstellen</string>
<string name="dir_empty">Verzeichnis leeren</string>
<string name="warning">Warnung!</string>
<string name="ask_lock_volume">Sind Sie sicher, dass Sie dieses Volume sperren wollen?</string>
<string name="ok">OK</string>
<string name="cancel">Abbrechen</string>
<string name="enter_folder_name">Ordnername:</string>
<string name="error">Fehler</string>
<string name="error_filename_empty">Bitte geben Sie einen Namen ein</string>
<string name="error_mkdir">Ordnererstellung fehlgeschlagen.</string>
<string name="success_import">Import erfolgreich.</string>
<string name="success_import_msg">Die ausgewählten Dateien wurden erfolgreich importiert.</string>
<string name="import_failed">Import von %s fehlgeschlagen.</string>
<string name="export_failed">Export von %s fehlgeschlagen.</string>
<string name="success_export">Export erfolgreich.</string>
<string name="remove_failed">Löschung von %s fehlgeschlagen.</string>
<string name="passwords_mismatch">Passwörter stimmen nicht überein</string>
<string name="dir_not_empty">Das ausgewählte Verzeichnis ist nicht leer</string>
<string name="create_volume_failed">Die Erstellung des Volumes ist fehlgeschlagen.</string>
<string name="open_volume_failed">Öffnen fehlgeschlagen</string>
<string name="share_chooser">Datei freigeben</string>
<string name="storage_perm_denied">Speichererlaubnis verweigert</string>
<string name="storage_perm_denied_msg">DroidFS kann ohne Speicherberechtigung nicht funktionieren</string>
<string name="get_size_failed">Fehlgeschlagen beim Abrufen der Dateigröße.</string>
<string name="parent_folder">Übergeordneter Ordner</string>
<string name="enter_volume_path">Bitte geben Sie den Volume-Pfad ein</string>
<string name="enter_volume_name">Bitte geben Sie den Datenträgernamen ein</string>
<string name="external_open">Öffnen mit externer Anwendung</string>
<string name="single_delete_confirm">Sind Sie sicher, dass Sie %s löschen wollen?</string>
<string name="multiple_delete_confirm">Sind Sie sicher, dass Sie diese %s Elemente löschen wollen?</string>
<string name="location">Standort: %s</string>
<string name="total_size">Gesamtgröße: %s</string>
<string name="import_from_other_volume">Importieren von einem anderen Datenträger</string>
<string name="read_file_failed">Diese Datei konnte nicht geöffnet werden.</string>
<string name="volume">Volume: %s</string>
<string name="yes">Ja</string>
<string name="no">Nein</string>
<string name="ask_for_wipe">Möchten Sie die Originaldateien löschen?</string>
<string name="wipe_failed">Löschvorgang fehlgeschlagen: %s</string>
<string name="wipe_successful">Dateien erfolgreich gelöscht.</string>
<string name="rename">Umbenennen</string>
<string name="rename_title">Neuer Name:</string>
<string name="rename_failed">Fehlgeschlagene Umbenennung %s</string>
<string name="sort_order">Sortierreihenfolge:</string>
<string name="change_password_failed">Vorgang fehlgeschlagen. Bitte überprüfen Sie Ihr altes Passwort!</string>
<string name="share_menu_label">Verschlüsseln mit DroidFS</string>
<string name="share_intent_parsing_failed">Fehlgeschlagene Bearbeitung der Freigabeanfrage.</string>
<string name="listdir_null_error_msg">Zugriff auf dieses Verzeichnis nicht möglich</string>
<string name="fingerprint_save_checkbox_text">Passwort-Hash mit Fingerabdruck speichern</string>
<string name="fingerprint_instruction">Bitte berühren Sie den Fingerabdrucksensor</string>
<string name="illegal_block_size_exception">IllegalBlockSizeException</string>
<string name="illegal_block_size_exception_msg">Dies kann passieren, wenn Sie einen neuen Fingerabdruck hinzugefügt haben. Das Zurücksetzen des Hash-Speichers kann dieses Problem lösen.</string>
<string name="reset_hash_storage">Reset hash storage</string>
<string name="MAC_verification_failed">Signatur/MAC-Verifizierung fehlgeschlagen. Entweder Android KeyStore oder der gespeicherte Hash wurde verändert. Das Zurücksetzen des Hash-Speichers kann dieses Problem lösen.</string>
<string name="hash_storage_reset">Hash-Speicher erfolgreich zurückgesetzt</string>
<string name="encrypt_action_description">Kennwort-Hash verschlüsseln und speichern.</string>
<string name="decrypt_action_description">Passwort-Hash entschlüsseln.</string>
<string name="title_activity_settings">DroidFS Einstellungen</string>
<string name="explorer">Explorer</string>
<string name="settings_title_sort_order">Standard-Sortierreihenfolge</string>
<string name="usf_decrypt">Exportieren/Entschlüsseln von Dateien zulassen</string>
<string name="usf_share">Freigeben von Dateien über das Android-Freigabemenü erlauben</string>
<string name="usf_open">Erlaubt das Öffnen von Dateien mit anderen Anwendungen</string>
<string name="usf_screenshot">Screenshots zulassen</string>
<string name="usf_fingerprint">Kennwort-Hash mit Fingerabdruck speichern können</string>
<string name="usf_volume_management">Volumenverwaltung</string>
<string name="usf_keep_open">Volumen offen halten, wenn die App in den Hintergrund geht</string>
<string name="unsafe_features">Unsichere Funktionen</string>
<string name="manage_unsafe_features">Sichere Funktionen verwalten</string>
<string name="manage_unsafe_features_summary">Aktivieren/Deaktivieren unsicherer Funktionen</string>
<string name="usf_home_warning_msg">DroidFS versucht, so sicher wie möglich zu sein. Sicherheit ist jedoch oft mit mangelndem Komfort verbunden. Aus diesem Grund bietet DroidFS zusätzliche unsichere Funktionen, die Sie je nach Bedarf aktivieren/deaktivieren können.\n\nWarnung: Diese Funktionen können UNSICHER sein. Verwenden Sie sie nicht, wenn Sie nicht genau wissen, was Sie tun. Es wird dringend empfohlen, die Dokumentation zu lesen, bevor Sie sie aktivieren.</string>
<string name="see_unsafe_features">Siehe unsichere Funktionen</string>
<string name="open_as">Öffnen als</string>
<string name="image">Bild</string>
<string name="video">Video</string>
<string name="audio">Audio</string>
<string name="playing_failed">Die Datei konnte nicht abgespielt werden: %s</string>
<string name="text">Text</string>
<string name="save_failed">Speichern fehlgeschlagen</string>
<string name="file_saved">Datei gespeichert.</string>
<string name="ask_save">Die Datei enthält ungespeicherte Änderungen. Möchten Sie diese vor dem Beenden speichern?</string>
<string name="save">Speichern</string>
<string name="discard">Verwerfen</string>
<string name="word_wrap">Wortumbruch</string>
<string name="outofmemoryerror_msg">OutOfMemoryError: Diese Datei ist zu groß, um in den Speicher geladen zu werden.</string>
<string name="new_file">Neue Datei erstellen</string>
<string name="enter_file_name">Dateiname:</string>
<string name="file_creation_failed">Die Datei konnte nicht erstellt werden.</string>
<string name="loading">Laden…</string>
<string name="loading_msg_create">Erzeuge Datenträger…</string>
<string name="loading_msg_change_password">Kennwort ändern…</string>
<string name="loading_msg_open">Datenträger öffnen…</string>
<string name="loading_msg_export">Dateien exportieren…</string>
<string name="query_cursor_null_error_msg">Zugriff auf diese Datei nicht möglich</string>
<string name="about">Über uns</string>
<string name="github">GitHub</string>
<string name="github_summary">Das DroidFS-Repository auf GitHub. Quellcode, Dokumentation, Bugtracker…</string>
<string name="gitea">Gitea</string>
<string name="gitea_summary">Das DroidFS-Repository auf der Chapril Gitea-Instanz. Im Gegensatz zu GitHub ist Gitea vollständig freie Software und wird selbst gehostet. Quellcode, Dokumentation, Bugtracker…</string>
<string name="share">Share</string>
<string name="decrypt_files">Exportieren/Entschlüsseln</string>
<string name="copy_failed">Kopie von %s fehlgeschlagen.</string>
<string name="copy_success">Kopie erfolgreich erstellt.</string>
<string name="add">Hinzufügen</string>
<string name="camera">Kamera</string>
<string name="picture_save_success">Bild gespeichert in %s</string>
<string name="picture_save_failed">Das Bild konnte nicht gespeichert werden.</string>
<string name="video_save_success">Video gespeichert unter %s</string>
<string name="file_overwrite_question">%s existiert bereits, wollen Sie es überschreiben?</string>
<string name="dir_overwrite_question">%s existiert bereits, soll der Inhalt zusammengeführt werden?</string>
<string name="enter_new_name">Neuen Namen eingeben</string>
<string name="copy_menu_title">Kopieren</string>
<string name="move_failed">Verschiebung von %s fehlgeschlagen.</string>
<string name="move_success">Verschiebung erfolgreich.</string>
<string name="enter_timer_duration">Eingabe der Timer-Dauer (in s)</string>
<string name="path_error">Der ausgewählte Pfad konnte nicht abgerufen werden.</string>
<string name="create_cant_write_error_msg">DroidFS hat keinen Schreibzugriff auf diesen Pfad. Bitte versuchen Sie einen anderen Ort.</string>
<string name="add_cant_write_warning">DroidFS hat keinen Schreibzugriff auf diesen Pfad. Fügen Sie ein Volume mit nur Lesezugriff hinzu.</string>
<string name="sdcard_error_header">DroidFS kann nur auf entfernbare SD-Karten schreiben unter:</string>
<string name="sdcard_error_add_footer">Datenträger mit Nur-Lese-Zugriff hinzufügen.</string>
<string name="sdcard_error_create_footer">Bitte verwenden Sie ein Unterverzeichnis dieses Pfades oder den internen Speicher.</string>
<string name="slideshow_stopped">Slideshow gestoppt</string>
<string name="slideshow_started">Diashow gestartet</string>
<string name="ask_save_img_rotated">Das Bild wurde gedreht. Möchten Sie diese Änderungen speichern und das ursprüngliche Bild überschreiben?</string>
<string name="image_saved_successfully">Bildänderungen erfolgreich gespeichert.</string>
<string name="bitmap_compress_failed">Komprimierung der Bitmap fehlgeschlagen.</string>
<string name="file_write_failed">Fehler beim Schreiben der Datei.</string>
<string name="error_not_a_volume">Verschlüsselter Datenträger nicht erkannt. Bitte überprüfen Sie den gewählten Pfad.</string>
<string name="version">Version</string>
<string name="error_cipher_null">Fehler: Chiffre ist null</string>
<string name="key_permanently_invalidated_exception">KeyPermanentlyInvalidatedException</string>
<string name="key_permanently_invalidated_exception_msg">Es sieht so aus, als hätten Sie einen neuen Fingerabdruck hinzugefügt. Der Hash der gespeicherten Passwörter ist unbrauchbar geworden.</string>
<string name="usf_read_doc">Sie sollten es sorgfältig lesen, bevor Sie eine dieser Optionen aktivieren.</string>
<string name="usf_doc">Dokumentation der unsicheren Funktionen</string>
<string name="error_retrieving_filename">Dateiname für URI nicht abrufbar: %s</string>
<string name="hidden_volume">Versteckter Datenträger</string>
<string name="error_slash_in_name">Datenträgername darf keine Schrägstriche enthalten</string>
<string name="hidden_volume_warning">Versteckte Volumes werden im internen Speicher der App gespeichert. Andere Apps können diese Volumes ohne Root-Zugriff nicht sehen. Wenn du jedoch DroidFS deinstallierst oder die Daten der App löschst, sind alle versteckten Volumes VERLOREN. Stellen Sie sicher, dass Sie Backups machen!</string>
<string name="camera_perm_needed">Die Kameraerlaubnis wird benötigt, um Fotos zu machen.</string>
<string name="choose_resolution">Wählen Sie eine Auflösung</string>
<string name="file_operations">Dateioperationen</string>
<string name="file_op_copy_msg">Kopieren von Dateien…</string>
<string name="file_op_import_msg">Dateien importieren…</string>
<string name="file_op_export_msg">Dateien exportieren…</string>
<string name="file_op_move_msg">Dateien verschieben…</string>
<string name="file_op_wiping_msg">Dateien löschen…</string>
<string name="folders_first">Ordner zuerst</string>
<string name="folders_first_summary">Ordner am Anfang der Liste anzeigen</string>
<string name="auto_fit_title">Bildschirm des Videoplayers wird automatisch gedreht</string>
<string name="auto_fit_summary">Bildschirm automatisch drehen, um sich den Videoabmessungen anzupassen</string>
<string name="open_tree_failed">Kein Datei-Explorer gefunden. Bitte installieren Sie einen und versuchen Sie es erneut.</string>
<string name="close_volume">Laufwerk schließen</string>
<string name="sort_by">Sortieren nach</string>
<string name="cut">Ausschneiden</string>
<string name="map_folders">Ordner zuordnen</string>
<string name="map_folders_summary">Ordner rekursiv zuordnen, um ihre Größe zu berechnen (Sie sollten dies beim Öffnen großer Volumes deaktivieren)</string>
<string name="camera_optimization">Kameraoptimierung</string>
<string name="maximize_quality">Qualität maximieren</string>
<string name="minimize_latency">Latenzzeit minimieren</string>
<string name="auto">Auto</string>
<string name="encryption_cipher_label">Verschlüsselungs-Chiffre:</string>
<string name="theme">Theme</string>
<string name="thumbnails">Vorschaubilder</string>
<string name="thumbnails_summary">Miniaturansichten von Bildern und Videos anzeigen</string>
<string name="seek_seconds_forward">+%d Sekunden</string>
<string name="seek_seconds_backward">-%d Sekunden</string>
<string name="add_volume">Datenträger hinzufügen</string>
<string name="pick_directory">Verzeichnis auswählen</string>
<string name="volume_alread_saved">Volume bereits gespeichert</string>
<string name="open_dialog_title">Öffnen von %s:</string>
<string name="remove">Entfernen</string>
<string name="settings">Einstellungen</string>
<string name="select_all">Alle auswählen</string>
<string name="remove_fingerprint">Fingerabdruck entfernen</string>
<string name="unrecoverable_key_exception_msg">%s. Kann den Verschlüsselungsschlüssel nicht laden.</string>
<string name="unrecoverable_key_exception">UnrecoverableKeyException</string>
<string name="delete_hidden_volume_question">%s ist versteckt, wollen Sie nur den Pfad des Volumes vergessen oder auch seinen gesamten INHALT LÖSCHEN?</string>
<string name="forget_only">Nur vergessen</string>
<string name="delete_volume">Datenträger löschen</string>
<string name="hidden_volume_description">Das Volume im internen DroidFS-Speicher speichern</string>
<string name="error_is_file">Fehler: Datei existiert</string>
<string name="volume_path_label">Wählen Sie den Pfad zum Volume:</string>
<string name="volume_name_label">Geben Sie den Namen des Datenträgers ein:</string>
<string name="volume_path_hint">Datenträgerpfad</string>
<string name="volume_name_hint">Datenträgername</string>
<string name="password_label">Geben Sie das Volume-Passwort ein:</string>
<string name="password_confirmation_label">Wiederholen Sie das Passwort:</string>
<string name="password_confirmation_hint">Passwort (Bestätigung)</string>
<string name="password_hash_saved">Passwort-Hash gespeichert</string>
<string name="no_volumes_text">Kein Volumen gespeichert, fügen Sie eines hinzu, indem Sie auf die Schaltfläche + klicken</string>
<string name="fingerprint_error_msg">Fingerabdruck-Authentifizierung kann nicht verwendet werden: %s.</string>
<string name="keyguard_not_secure">Keyguard nicht sicher</string>
<string name="no_hardware">Keine geeignete Hardware gefunden</string>
<string name="hardware_unavailable">Hardware nicht verfügbar</string>
<string name="no_fingerprint">kein registrierter Fingerabdruck</string>
<string name="unknown_error">unbekannter Fehler</string>
<string name="biometric_error">Biometrischer Fehler: %s</string>
<string name="apply_to_all">Diese Auswahl auf alle versteckten Datenträger anwenden</string>
<string name="select_volume">Datenträger auswählen</string>
<string name="current_password_label">Geben Sie das aktuelle Volume-Passwort ein:</string>
<string name="current_password_hint">Aktuelles Passwort</string>
<string name="new_password_label">Geben Sie das neue Volume-Passwort ein:</string>
<string name="new_password_hint">Neues Passwort</string>
<string name="new_password_confirmation_label">Wiederholen Sie das neue Passwort:</string>
<string name="error_marshmallow_required">Diese Funktion ist nur unter Android 6.0 (Marshmallow) oder höher verfügbar.</string>
<string name="copy_hidden_volume">Kopieren in gemeinsamen Speicher</string>
<string name="copy_external_volume">Eine versteckte Kopie erstellen</string>
<string name="copy_volume_notification">Datenträger kopieren…</string>
<string name="hidden_volume_already_exists">Ein versteckter Datenträger mit dem gleichen Namen existiert bereits.</string>
<string name="pdf_document">PDF-Dokument</string>
<string name="thumbnail_max_size">Maximale Größe für Miniaturansichten</string>
<string name="thumbnail_max_size_summary">Maximale Dateigröße, für die eine Vorschau geladen werden soll. Aktueller Wert: %s</string>
<string name="size_hint">Größe (in KB)</string>
<string name="invalid_number">Ungültige Zahl</string>
<string name="new_volume_name">Neuer Datenträgername:</string>
<string name="volume_rename_failed">Datenträger konnte nicht umbenannt werden</string>
<string name="switch_display_layout">Anzeigelayout umschalten</string>
<string name="one_file">1 Datei</string>
<string name="multiple_files">%d Dateien</string>
<string name="one_folder">1 Ordner</string>
<string name="multiple_folders">%d Ordner</string>
<string name="default_open">Öffnen Sie diesen Datenträger beim Start der Anwendung</string>
<string name="remove_default_open">Standardmäßig nicht öffnen</string>
<string name="elements_selected">%d/%d ausgewählt</string>
<string name="pin_passwords_title">Numerische Tastaturbelegung</string>
<string name="pin_passwords_summary">Numerische Tastaturbelegung bei der Eingabe von Volume-Passwörtern verwenden</string>
<string name="volume_type_label">Lautstärketyp:</string>
<string name="gocryptfs">Gocryptfs</string>
<string name="cryfs">CryFS</string>
<string name="gocryptfs_disabled">Die Unterstützung von Gocryptfs wurde deaktiviert</string>
<string name="cryfs_disabled">CryFS-Unterstützung wurde deaktiviert</string>
<string name="file_op_delete_msg">Löschen von Dateien…</string>
<string name="volume_type">(%s)</string>
<string name="volume_type_read_only">(%s, schreibgeschützt)</string>
<string name="volume_type_inaccessible">(%s, unzugänglich)</string>
<string name="io_error">I/O Fehler.</string>
<string name="use_fingerprint">Fingerabdruck anstelle des aktuellen Passworts verwenden</string>
<string name="remember_volume">Volumen merken</string>
<string name="open_volume">Laufwerk öffnen</string>
<string name="choose_existing_volume">Bitte wählen Sie einen vorhandenen Datenträger</string>
<string name="volume_unlocked">Volumen entsperrt</string>
<string name="lock_volume">Datenträger sperren</string>
<string name="lock">Sperren</string>
<string name="ux">UX</string>
<string name="theme_color">Farbschema </string>
<string name="theme_color_summary">Farbschema der Anwendung ändern</string>
<string name="black_theme">Dark Mode</string>
<string name="password_fallback">Passwort anfordern</string>
<string name="password_fallback_summary">Aufforderung zur Eingabe des Passworts, wenn die Authentifizierung per Fingerabdruck abgebrochen wird</string>
<string name="unknown_error_code">Unbekannter Fehlercode: %d</string>
<string name="config_load_error">Die Konfigurationsdatei kann nicht geladen werden. Stellen Sie sicher, dass der Datenträger zugänglich ist.</string>
<string name="wrong_password">Die Konfigurationsdatei kann nicht entschlüsselt werden. Bitte überprüfen Sie Ihr Passwort.</string>
<string name="filesystem_id_changed">Die Dateisystem-ID in der Konfigurationsdatei ist eine andere als beim letzten Öffnen dieses Datenträgers. Dies könnte bedeuten, dass ein Angreifer das Dateisystem durch ein anderes ersetzt hat.</string>
<string name="inaccessible_base_dir">Das Volume existiert nicht oder ist unzugänglich.</string>
<string name="task_failed">Die Aufgabe ist fehlgeschlagen: %s</string>
<string name="usf_expose">Geöffnete Datenträger anzeigen</string>
<string name="usf_expose_summary">Anderen Anwendungen das Durchsuchen offener Datenträger als Dokumentanbieter erlauben</string>
<string name="usf_saf_write">Schreibzugriff gewähren</string>
<string name="usf_saf_write_summary">Schreibzugriff beim Öffnen von Dateien mit anderen Anwendungen gewähren</string>
<string name="saf">Storage Access Framework</string>
<string name="tmp_export_failed">Export fehlgeschlagen: %s</string>
<string name="export_failed_create">kann exportierte Datei nicht erstellen</string>
<string name="export_failed_export">Fehler beim Exportieren der Datei</string>
<string name="export_mem">Exportieren in den Arbeitsspeicher…</string>
<string name="export_disk">Exportieren auf den Datenträger…</string>
</resources>

View File

@ -242,7 +242,6 @@
<string name="file_op_delete_msg">Eliminando archivos…</string> <string name="file_op_delete_msg">Eliminando archivos…</string>
<string name="volume_type">(%s)</string> <string name="volume_type">(%s)</string>
<string name="volume_type_read_only">(%s, Sólo lectura)</string> <string name="volume_type_read_only">(%s, Sólo lectura)</string>
<string name="volume_type_inaccessible">(%s, inaccesible)</string>
<string name="io_error">I/O Error.</string> <string name="io_error">I/O Error.</string>
<string name="use_fingerprint">Utilizar huella dactilar en lugar de la contraseña actual</string> <string name="use_fingerprint">Utilizar huella dactilar en lugar de la contraseña actual</string>
<string name="remember_volume">Recordar volumen</string> <string name="remember_volume">Recordar volumen</string>
@ -256,27 +255,5 @@
<string name="theme_color_summary">Cambiar el color del tema de la aplicación</string> <string name="theme_color_summary">Cambiar el color del tema de la aplicación</string>
<string name="black_theme">Tema en negro</string> <string name="black_theme">Tema en negro</string>
<string name="password_fallback">Reintroducir contraseña</string> <string name="password_fallback">Reintroducir contraseña</string>
<string name="password_fallback_summary">Solicitar contraseña cuando se cancela la autenticación por huella dactilar</string> <string name="password_fallback_summary">Solicitar la contraseña cuando se cancela la autenticación por huella dactilar</string>
<string name="unknown_error_code">Código de error desconocido: %d</string>
<string name="config_load_error">No se puede cargar el archivo de configuración. Asegúrese de que el volumen es accesible.</string>
<string name="wrong_password">No se puede descifrar el archivo de configuración. Por favor, comprueba tu contraseña.</string>
<string name="filesystem_id_changed">El id del sistema de ficheros en el fichero de configuración es diferente a la última vez que se abrió este volumen. Esto podría significar que un atacante sustituyó el sistema de archivos por otro diferente.</string>
<string name="inaccessible_base_dir">El volumen no existe o es inaccesible.</string>
<string name="task_failed">La tarea falló: %s</string>
<string name="usf_expose">Exponer volúmenes abiertos</string>
<string name="usf_expose_summary">Permitir que otras aplicaciones exploren volúmenes abiertos como fuentes de documentos</string>
<string name="usf_saf_write">Conceder acceso de escritura</string>
<string name="usf_saf_write_summary">Conceder acceso de escritura al abrir archivos con otras aplicaciones</string>
<string name="saf">Marco de acceso al almacenamiento</string>
<string name="tmp_export_failed">Falló la exportación: %s</string>
<string name="export_failed_create">no se pudo crear el archivo exportado</string>
<string name="export_failed_export">Error al exportar el archivo</string>
<string name="export_mem">Exportando a memoria…</string>
<string name="export_disk">Exportar a disco…</string>
<string name="memfd_create_unsupported">El kernel actual no soporta memfd_create(). Esta característica requiere una versión mínima del kernel de %s.</string>
<string name="export_method">Método de exportación</string>
<string name="export_method_summary">Método de exportación de archivos. Se utiliza para compartir, abrir externamente y acceder a archivos expuestos.</string>
<string name="debug">Depurar</string>
<string name="logcat_title">Logcat de DroidFS</string>
<string name="logcat_saved">Logcat guardado</string>
</resources> </resources>

View File

@ -17,10 +17,4 @@
<item>Фиолетовый</item> <item>Фиолетовый</item>
<item>Розовый</item> <item>Розовый</item>
</string-array> </string-array>
<string-array name="export_methods">
<item>Автовыбор (зависит от доступной памяти)</item>
<item>Временный файл в хранилище (надёжно, но могут остаться следы)</item>
<item>Файл в памяти (безопаснее, но не всегда возможно)</item>
</string-array>
</resources> </resources>

View File

@ -231,8 +231,7 @@
<string name="gocryptfs_disabled">Поддержка Gocryptfs отключена</string> <string name="gocryptfs_disabled">Поддержка Gocryptfs отключена</string>
<string name="cryfs_disabled">Поддержка CryFS отключена</string> <string name="cryfs_disabled">Поддержка CryFS отключена</string>
<string name="file_op_delete_msg">Удаление файлов…</string> <string name="file_op_delete_msg">Удаление файлов…</string>
<string name="volume_type_read_only">(%s, только чтение)</string> <string name="volume_type_read_only">(%s, только для чтения)</string>
<string name="volume_type_inaccessible">(%s, недоступен)</string>
<string name="io_error">Ошибка ввода/вывода.</string> <string name="io_error">Ошибка ввода/вывода.</string>
<string name="use_fingerprint">Использовать отпечатка пальца вместо пароля</string> <string name="use_fingerprint">Использовать отпечатка пальца вместо пароля</string>
<string name="remember_volume">Запомнить том</string> <string name="remember_volume">Запомнить том</string>
@ -247,25 +246,4 @@
<string name="black_theme">Чёрная тема</string> <string name="black_theme">Чёрная тема</string>
<string name="password_fallback">Возврат к паролю</string> <string name="password_fallback">Возврат к паролю</string>
<string name="password_fallback_summary">Запрашивать пароль при отмене аутентификации по отпечатку пальца</string> <string name="password_fallback_summary">Запрашивать пароль при отмене аутентификации по отпечатку пальца</string>
<string name="unknown_error_code">Неизвестный код ошибки: %d</string>
<string name="config_load_error">Невозможно загрузить файл конфигурации. Убедитесь, что том доступен.</string>
<string name="wrong_password">Невозможно расшифровать файл конфигурации. Проверьте пароль.</string>
<string name="filesystem_id_changed">Идентификатор файловой системы в файле конфигурации отличается сохранённого при последнем использовании тома. Это может означать, что злоумышленник подменил файловую систему.</string>
<string name="inaccessible_base_dir">Том не существует или недоступен.</string>
<string name="task_failed">Задача не выполнена: %s</string>
<string name="usf_expose">Показывать открытые тома</string>
<string name="usf_expose_summary">Разрешить другим приложениям просматривать открытые тома через поставщиков документов</string>
<string name="usf_saf_write">Разрешить запись</string>
<string name="usf_saf_write_summary">Разрешить другим приложениям изменять открытые файлы</string>
<string name="tmp_export_failed">Экспорт не выполнен: %s</string>
<string name="export_failed_create">невозможно создать экспортируемый файл</string>
<string name="export_failed_export">невозможно экспортировать файл</string>
<string name="export_mem">Экспорт в память…</string>
<string name="export_disk">Экспорт в хранилище…</string>
<string name="memfd_create_unsupported">Текущее ядро не поддерживает memfd_create(). Для работы данной функции требуется версия ядра не ниже %s.</string>
<string name="export_method">Метод экспорта</string>
<string name="export_method_summary">Метод экспорта файлов. Используется для обмена, открытия во внешнем приложении и доступа к открытым файлам.</string>
<string name="debug">Отладка</string>
<string name="logcat_title">Журнал logcat DroidFS</string>
<string name="logcat_saved">Журнал logcat сохранён</string>
</resources> </resources>

View File

@ -1,72 +0,0 @@
<resources>
<string-array name="gocryptfs_encryption_ciphers">
<item>AES-GCM</item>
<item>XChaCha20-Poly1305</item>
<item>@string/auto</item>
</string-array>
<string-array name="cryfs_encryption_ciphers">
<item>xchacha20-poly1305</item>
<item>aes-256-gcm</item>
<item>aes-128-gcm</item>
<item>twofish-256-gcm</item>
<item>twofish-128-gcm</item>
<item>serpent-256-gcm</item>
<item>serpent-128-gcm</item>
<item>cast-256-gcm</item>
<item>mars-448-gcm</item>
<item>mars-256-gcm</item>
<item>mars-128-gcm</item>
</string-array>
<string-array name="sort_orders_entries">
<item>Ad</item>
<item>Boyut</item>
<item>Tarih</item>
<item>Ad (azalan)</item>
<item>Boyut (azalan)</item>
<item>Tarih (azalan)</item>
</string-array>
<string-array name="color_names">
<item>Yeşil</item>
<item>Kırmızı</item>
<item>Mavi</item>
<item>Sarı</item>
<item>Turuncu</item>
<item>Mor</item>
<item>Pembe</item>
</string-array>
<string-array name="export_methods">
<item>Otomatik (kullanılabilir belleğe bağlı olarak)</item>
<item>Diske geçici dışa aktarma (güvenilir ancak iz bırakabilir)</item>
<item>Bellek dosyası (daha güvenlidir ancak her zaman işe yaramaz)</item>
</string-array>
<!-- don't translate the following otherwise the app will crash -->
<string-array name="sort_orders_values">
<item>name</item>
<item>size</item>
<item>date</item>
<item>name_desc</item>
<item>size_desc</item>
<item>date_desc</item>
</string-array>
<string-array name="color_values">
<item>green</item>
<item>red</item>
<item>blue</item>
<item>yellow</item>
<item>orange</item>
<item>purple</item>
<item>pink</item>
</string-array>
<string-array name="export_methods_values">
<item>auto</item>
<item>disk</item>
<item>memory</item>
</string-array>
</resources>

View File

@ -1,282 +0,0 @@
<resources>
<string name="app_name">DroidFS</string>
<string name="create_volume">Birim oluştur</string>
<string name="open"></string>
<string name="create">Oluştur</string>
<string name="change_password">Şifreyi değiştir</string>
<string name="password">Şifre</string>
<string name="import_files">Dosyaları içe aktar/Şifrele</string>
<string name="import_folder">Klasörü İçe Aktar/Şifrele</string>
<string name="discovering_files">Dosyalar keşfediliyor…</string>
<string name="mkdir">Klasör oluştur</string>
<string name="dir_empty">Klasör boş</string>
<string name="warning">Dikkat !</string>
<string name="ask_lock_volume">Bu birimi kilitlemek istediğinizden emin misiniz?</string>
<string name="ok">Tamam</string>
<string name="cancel">İptal</string>
<string name="enter_folder_name">Klasör adı:</string>
<string name="error">Hata</string>
<string name="error_filename_empty">Lütfen bir isim girin</string>
<string name="error_mkdir">Klasör oluşturulamadı.</string>
<string name="success_import">Başarılı bir şekilde içe aktarıldı !</string>
<string name="success_import_msg">Seçili dosyalar başarılı bir şekilde içe aktarıldı.</string>
<string name="import_failed">İçe aktarılamadı: %s</string>
<string name="export_failed">Dışa aktarılamadı: %s</string>
<string name="success_export">Başarılı bir şekilde dışa aktarıldı !</string>
<string name="remove_failed">Silinemedi: %s</string>
<string name="passwords_mismatch">Şifreler uyuşmuyor</string>
<string name="dir_not_empty">Seçili klasör boş değil</string>
<string name="create_volume_failed">Birim oluşturualamadı.</string>
<string name="open_volume_failed">ılamadı</string>
<string name="share_chooser">Dosyayı paylaş</string>
<string name="storage_perm_denied">Depolama izni reddedildi</string>
<string name="storage_perm_denied_msg">DroidFS, depolama izinleri olmadan çalışamaz.</string>
<string name="get_size_failed">Dosya boyutu alınamadı.</string>
<string name="parent_folder">Ana klasör</string>
<string name="enter_volume_path">Lütfen birim yolunu girin</string>
<string name="enter_volume_name">Lütfen birim adını girin</string>
<string name="external_open">Harici uygulamayla aç</string>
<string name="single_delete_confirm">Silmek istediğimizden emin misiniz: %s</string>
<string name="multiple_delete_confirm">Bunları silmek istediğinizden emin misiniz: %s</string>
<string name="location">Konum: %s</string>
<string name="total_size">Toplam boyut: %s</string>
<string name="import_from_other_volume">Başka bir birimden içe aktar</string>
<string name="read_file_failed">Bu dosya açılamadı.</string>
<string name="volume">Birim: %s</string>
<string name="yes">Evet</string>
<string name="no">Hayır</string>
<string name="ask_for_wipe">Orijinal dosyaları silmek istiyor musunuz ?</string>
<string name="wipe_failed">Silinemedi: %s</string>
<string name="wipe_successful">Dosyalar başarılı bir şekilde silindi !</string>
<string name="rename">Yeniden adlandır</string>
<string name="rename_title">Yeni ad:</string>
<string name="rename_failed">Yeniden adlandırılamadı: %s</string>
<string name="sort_order">Sıralama biçimi:</string>
<string name="change_password_failed">İşlem başarısız oldu. Lütfen eski şifrenizi kontrol edin.</string>
<string name="share_menu_label">DroidFS ile şifrele</string>
<string name="share_intent_parsing_failed">Paylaşım isteği işlenemedi.</string>
<string name="listdir_null_error_msg">Bu dizine erişilemiyor</string>
<string name="fingerprint_save_checkbox_text">Parmak izini kullanarak şifre hash değerini kaydedin</string>
<string name="fingerprint_instruction">Lütfen parmak izi sensörüne dokunun</string>
<string name="illegal_block_size_exception">IllegalBlockSizeException</string>
<string name="illegal_block_size_exception_msg">Yeni bir parmak izi eklediyseniz bu durum meydana gelebilir. Hash depolamasının sıfırlanması bu sorunu çözebilir.</string>
<string name="reset_hash_storage">Hash depolamasını sıfırla</string>
<string name="MAC_verification_failed">İmza/MAC doğrulaması başarısız oldu. Android KeyStore veya kaydedilen hash değeri değiştirildi. Hash depolamasını sıfırlamak bu sorunu çözebilir.</string>
<string name="hash_storage_reset">Hash depolaması başarıyla sıfırlandı</string>
<string name="encrypt_action_description">Şifre hash değerini şifreleme ve kaydetme.</string>
<string name="decrypt_action_description">Şifre hash değeri şifreleniyor.</string>
<string name="title_activity_settings">DroidFS ayarları</string>
<string name="explorer">Tarayıcı</string>
<string name="settings_title_sort_order">Varsayılan sıralama düzeni</string>
<string name="usf_decrypt">Dosyaların dışa aktarılmasına/şifresinin çözülmesine izin ver</string>
<string name="usf_share">Android paylaşım menüsü aracılığıyla dosya paylaşımına izin ver</string>
<string name="usf_open">Dosyaların diğer uygulamalarla açılmasına izin ver</string>
<string name="usf_screenshot">Ekran görüntüsü almaya izin ver</string>
<string name="usf_fingerprint">Parmak izi kullanılarak şifre hash değerinin kaydedilmesine izin ver</string>
<string name="usf_volume_management">Birim yönetimi</string>
<string name="usf_keep_open">Uygulama arka plana geçtiğinde birimi açık tutun</string>
<string name="unsafe_features">Güvenli olmayan özellikler</string>
<string name="manage_unsafe_features">Güvenli olmayan özellikleri yönetin</string>
<string name="manage_unsafe_features_summary">Güvenli olmayan özellikleri etkinleştirme/devre dışı bırakma</string>
<string name="usf_home_warning_msg">DroidFS mümkün olduğunca güvenli olmaya çalışır. Ancak güvenlik çoğu zaman konfor eksikliğini de beraberinde getirir. Bu nedenle DroidFS, ihtiyaçlarınıza göre etkinleştirebileceğiniz/devre dışı bırakabileceğiniz güvenli olmayan ek özellikler sunar.\n\nDikkat: bu özellikler GÜVENLİ OLMAYABİLİR. Ne yaptığınızı tam olarak bilmiyorsanız bunları kullanmayın. Bunları etkinleştirmeden önce belgeleri okumanız önemle tavsiye edilir.</string>
<string name="see_unsafe_features">Güvenli olmayan özellikleri görün</string>
<string name="open_as">Farklı</string>
<string name="image">Resim</string>
<string name="video">Video</string>
<string name="audio">Ses</string>
<string name="playing_failed">Bu dosya oynatılamadı: %s</string>
<string name="text">Metin</string>
<string name="save_failed">Kaydedilemedi</string>
<string name="file_saved">Dosya kaydedildi !</string>
<string name="ask_save">Dosya kaydedilmemiş değişiklikler içeriyor. Çıkmadan önce bunları kaydetmek istiyor musunuz ?</string>
<string name="save">Kaydet</string>
<string name="discard">Gözardı et</string>
<string name="word_wrap">Kelime kaydırma</string>
<string name="outofmemoryerror_msg">OutOfMemoryError: Bu dosya belleğe yüklenemeyecek kadar büyük.</string>
<string name="new_file">Yeni dosya oluştur</string>
<string name="enter_file_name">Dosya adı:</string>
<string name="file_creation_failed">Dosya oluşturulamadı.</string>
<string name="loading">Yükleniyor…</string>
<string name="loading_msg_create">Birim oluşturuluyor…</string>
<string name="loading_msg_change_password">Şifre değiştiriliyor…</string>
<string name="loading_msg_open">Birim açılıyor…</string>
<string name="loading_msg_export">Dosyalar dışa aktarılıyor…</string>
<string name="query_cursor_null_error_msg">Bu dosyaya erişilemiyor</string>
<string name="about">Hakkında</string>
<string name="github">GitHub</string>
<string name="github_summary">DroidFS Github deposu. Kaynak kodu, dokümantasyon, hata izleyici…</string>
<string name="gitea">Gitea</string>
<string name="gitea_summary">Chapril Gitea bulut sunucusundaki DroidFS deposu GitHub\'tan farklı olarak Gitea tamamen ücretsiz bir yazılımdır ve kendi kendine barındırılır. Kaynak kodu, belgeler, hata izleyici…</string>
<string name="share">Paylaş</string>
<string name="decrypt_files">Dışa aktar/Şifre çöz</string>
<string name="copy_failed">Kopyalanamadı: %s</string>
<string name="copy_success">Başarıyla kopyalandı !</string>
<string name="add">Ekle</string>
<string name="camera">Kamera</string>
<string name="picture_save_success">Resim şuraya kaydedildi: %s</string>
<string name="picture_save_failed">Bu resim kaydedilemedi.</string>
<string name="video_save_success">Resim şuraya kaydedildi: %s</string>
<string name="file_overwrite_question">%s zaten mevcut, üzerine yazmak istiyor musunuz ?</string>
<string name="dir_overwrite_question">%s zaten mevcut, içeriğini birleştirmek istiyor musunuz ?</string>
<string name="enter_new_name">Yeni ad girin</string>
<string name="copy_menu_title">Kopyala</string>
<string name="move_failed">Taşınamadı: %s</string>
<string name="move_success">Başarıyla taşındı !</string>
<string name="enter_timer_duration">Zamanlayıcı süresini girin (saniye olarak)</string>
<string name="path_error">Seçilen yol alınamadı.</string>
<string name="create_cant_write_error_msg">DroidFS\'nin bu yola yazma erişimi yok. Lütfen başka bir konum deneyin.</string>
<string name="add_cant_write_warning">DroidFS\'nin bu yola yazma erişimi yok. Salt okunur erişimle birim ekleniyor.</string>
<string name="sdcard_error_header">DroidFS yalnızca aşağıdaki durumlarda çıkarılabilir SD kartlara yazabilir:</string>
<string name="sdcard_error_add_footer">Salt okunur erişimle birim ekleme.</string>
<string name="sdcard_error_create_footer">Lütfen bu yolun bir alt dizinini veya dahili depolamayı kullanın.</string>
<string name="slideshow_stopped">Slayt gösterisi durduruldu</string>
<string name="slideshow_started">Slayt gösterisi başlatıldı</string>
<string name="ask_save_img_rotated">Resim döndürüldü. Bu değişiklikleri kaydedip orijinal resmin üzerine yazmak istiyor musunuz ?</string>
<string name="image_saved_successfully">Resim değişiklikleri başarıyla kaydedildi.</string>
<string name="bitmap_compress_failed">Bitmap sıkıştırılamadı.</string>
<string name="file_write_failed">Dosya yazılamadı.</string>
<string name="error_not_a_volume">Şifrelenmiş birim tanınmadı. Lütfen seçilen yolu kontrol edin.</string>
<string name="version">Versiyon</string>
<string name="error_cipher_null">Hata şifre boş</string>
<string name="key_permanently_invalidated_exception">KeyPermanentlyInvalidatedException</string>
<string name="key_permanently_invalidated_exception_msg">Yeni bir parmak izi eklemişsiniz gibi görünüyor. Kaydedilen şifrelerin hash değeri kullanılamaz hale geldi.</string>
<string name="usf_read_doc">Bu seçeneklerden herhangi birini etkinleştirmeden önce dikkatlice okumalısınız.</string>
<string name="usf_doc">Güvenli olmayan özellikler dokümantasyonu</string>
<string name="error_retrieving_filename">Şu URI için dosya adı alınamıyor: %s</string>
<string name="hidden_volume">Gizli birim</string>
<string name="error_slash_in_name">Birim adı eğik çizgi sembolü içeremez</string>
<string name="hidden_volume_warning">Gizli birimler uygulamanın dahili deposunda saklanır. Diğer uygulamalar bu birimleri root erişimi olmadan göremez. Ancak DroidFS\'yi kaldırırsanız veya uygulamanın verilerini temizlerseniz tüm gizli birimleriniz KAYBOLACAKTIR. Yedekleme yaptığınızdan emin olun !</string>
<string name="camera_perm_needed">Fotoğraf çekebilmek için kamera izni gerekiyor.</string>
<string name="choose_resolution">Bir çözünürlük seçin</string>
<string name="file_operations">Dosya işlemleri</string>
<string name="file_op_copy_msg">Dosyalar kopyalanıyor…</string>
<string name="file_op_import_msg">Dosyalar içe aktarılıyor...</string>
<string name="file_op_export_msg">Dosyalar dışa aktarılıyor...</string>
<string name="file_op_move_msg">Dosyalar taşınıyor…</string>
<string name="file_op_wiping_msg">Dosyalar siliniyor…</string>
<string name="folders_first">Önce klasörler</string>
<string name="folders_first_summary">Klasörleri listenin başında göster</string>
<string name="auto_fit_title">Video oynatıcı ekranı otomatik döndürme</string>
<string name="auto_fit_summary">Video boyutlarına uyacak şekilde ekranı otomatik olarak döndürün</string>
<string name="open_tree_failed">Dosya tarayıcısı bulunamadı. Lütfen bir tane yükleyin ve tekrar deneyin.</string>
<string name="close_volume">Birimi kapat</string>
<string name="sort_by">Sırala</string>
<string name="cut">Kes</string>
<string name="map_folders">Klasörleri eşleme</string>
<string name="map_folders_summary">Boyutlarını hesaplamak için klasörleri yinelemeli olarak eşleyin (büyük birimleri açarken bunu devre dışı bırakmalısınız)</string>
<string name="camera_optimization">Kamera optimizasyonu</string>
<string name="maximize_quality">Kaliteyi maksimuma çıkarın</string>
<string name="minimize_latency">Gecikmeyi minimuma indirin</string>
<string name="auto">Otomatik</string>
<string name="encryption_cipher_label">Şifreleme şifresi:</string>
<string name="theme">Tema</string>
<string name="thumbnails">Küçük resimler</string>
<string name="thumbnails_summary">Resimlerin ve videoların küçük resimlerini göster</string>
<string name="seek_seconds_forward">+%d saniye</string>
<string name="seek_seconds_backward">-%d saniye</string>
<string name="add_volume">Birim ekle</string>
<string name="pick_directory">Klasör seç</string>
<string name="volume_alread_saved">Birim zaten kayıtlı</string>
<string name="open_dialog_title">ılıyor %s:</string>
<string name="remove">Kaldır</string>
<string name="settings">Ayarlar</string>
<string name="select_all">Tümünü seç</string>
<string name="remove_fingerprint">Parmak izini kaldır</string>
<string name="unrecoverable_key_exception_msg">%s. Şifreleme anahtarı yüklenemedi.</string>
<string name="unrecoverable_key_exception">UnrecoverableKeyException</string>
<string name="delete_hidden_volume_question">%s gizli, sadece birimin yolunu unutmak mı yoksa tüm İÇERİĞİNİ SİLMEK mi istiyorsunuz?</string>
<string name="forget_only">Sadece unut</string>
<string name="delete_volume">Birimi sil</string>
<string name="hidden_volume_description">Birimi DroidFS dahili deposunda saklayın</string>
<string name="error_is_file">Hata: dosya zaten mevcut</string>
<string name="volume_path_label">Birimin yolunu seçin:</string>
<string name="volume_name_label">Birimin adını girin:</string>
<string name="volume_path_hint">Birim yolu</string>
<string name="volume_name_hint">Birim adı</string>
<string name="password_label">Birim şifresini girin:</string>
<string name="password_confirmation_label">Şifreyi tekrarlayın:</string>
<string name="password_confirmation_hint">Şifre (doğrulama)</string>
<string name="password_hash_saved">şifre hash değeri kaydedildi</string>
<string name="no_volumes_text">Kaydedilmiş birim yok, + düğmesini tıklayarak biraz ekleyin</string>
<string name="fingerprint_error_msg">Parmak izi kimlik doğrulaması kullanılamaz: %s.</string>
<string name="keyguard_not_secure">tuş kilidi güvenli değil</string>
<string name="no_hardware">uygun donanım bulunamadı</string>
<string name="hardware_unavailable">donanım mevcut değil</string>
<string name="no_fingerprint">kayıtlı parmak izi yok</string>
<string name="unknown_error">bilinmeyen hata</string>
<string name="biometric_error">Biyometri hatası: %s</string>
<string name="apply_to_all">Bu seçimi tüm gizli birimlere uygula</string>
<string name="select_volume">Birimi seç</string>
<string name="current_password_label">Mevcut birim parolasını girin:</string>
<string name="current_password_hint">Mevcut şifre</string>
<string name="new_password_label">Yeni birim şifresini girin:</string>
<string name="new_password_hint">Yeni şifre</string>
<string name="new_password_confirmation_label">Yeni şifreyi tekrarlayın:</string>
<string name="error_marshmallow_required">Bu özellik yalnızca Android 6.0 (Marshmallow) veya üzeri sürümlerde mevcuttur.</string>
<string name="copy_hidden_volume">Paylaşılan depolamaya kopyala</string>
<string name="copy_external_volume">Gizli bir kopya oluştur</string>
<string name="copy_volume_notification">Birim kopyalanıyor…</string>
<string name="hidden_volume_already_exists">Aynı ada sahip bir gizli birim zaten mevcut.</string>
<string name="pdf_document">PDF dökümanı</string>
<string name="thumbnail_max_size">Küçük resimler için maksimum boyut</string>
<string name="thumbnail_max_size_summary">Küçük resmin yüklenebileceği maksimum dosya boyutu. Mevcut değer: %s</string>
<string name="size_hint">Boyut (KB olarak)</string>
<string name="invalid_number">Geçersiz numara</string>
<string name="new_volume_name">Yeni birim adı:</string>
<string name="volume_rename_failed">Birim yeniden adlandırılamadı</string>
<string name="switch_display_layout">Ekran düzenini değiştir</string>
<string name="one_file">1 dosya</string>
<string name="multiple_files">%d dosya</string>
<string name="one_folder">1 klasör</string>
<string name="multiple_folders">%d klasör</string>
<string name="default_open">Uygulamayı başlattığınızda bu birimi açın</string>
<string name="remove_default_open">Varsayılan olarak açma</string>
<string name="elements_selected">%d/%d seçildi</string>
<string name="pin_passwords_title">Sayısal tuş takımı düzeni</string>
<string name="pin_passwords_summary">Birim parolalarını girerken sayısal tuş takımı düzeni kullanın</string>
<string name="volume_type_label">Birim türü:</string>
<string name="gocryptfs">Gocryptfs</string>
<string name="cryfs">CryFS</string>
<string name="gocryptfs_disabled">Gocryptfs desteği devre dışı bırakıldı</string>
<string name="cryfs_disabled">CryFS desteği devre dışı bırakıldı</string>
<string name="file_op_delete_msg">Dosyalar silindi…</string>
<string name="volume_type">(%s)</string>
<string name="volume_type_read_only">(%s, salt-okunur)</string>
<string name="volume_type_inaccessible">(%s, erişilemez)</string>
<string name="io_error">I/O hatası.</string>
<string name="use_fingerprint">Mevcut şifre yerine parmak izini kullan</string>
<string name="remember_volume">Birimi hatırla</string>
<string name="open_volume">Birimi aç</string>
<string name="choose_existing_volume">Lütfen mevcut bir birimi seçin</string>
<string name="volume_unlocked">Birimin kilidi açıldı</string>
<string name="lock_volume">Birimi kilitle</string>
<string name="lock">Kilitle</string>
<string name="ux">UX</string>
<string name="theme_color">Tema rengi</string>
<string name="theme_color_summary">Uygulama teması rengini değiştirme</string>
<string name="black_theme">Siyah tema</string>
<string name="password_fallback">Şifreye geri dönme</string>
<string name="password_fallback_summary">Parmak iziyle kimlik doğrulama iptal edildiğinde şifre sor</string>
<string name="unknown_error_code">Bilinmeyen hata kodu: %d</string>
<string name="config_load_error">Yapılandırma dosyası yüklenemiyor. Birimin erişilebilir olduğundan emin olun.</string>
<string name="wrong_password">Yapılandırma dosyasının şifresi çözülemiyor. Lütfen şifrenizi kontrol edin.</string>
<string name="filesystem_id_changed">Yapılandırma dosyasındaki dosya sistemi kimliği, bu birimi son açtığımız zamandan farklı. Bu, saldırganın dosya sistemini farklı bir sistemle değiştirdiği anlamına gelebilir.</string>
<string name="inaccessible_base_dir">Birim mevcut değil veya erişilemiyor.</string>
<string name="task_failed">Görev başarısız oldu: %s</string>
<string name="usf_expose">ık birimleri ortaya çıkarın</string>
<string name="usf_expose_summary">Diğer uygulamaların belge sağlayıcıları olarak açık birimlere göz atmasına izin ver</string>
<string name="usf_saf_write">Yazma erişimi ver</string>
<string name="usf_saf_write_summary">Dosyaları diğer uygulamalarla açarken yazma erişimi verin</string>
<string name="saf">Depolama Erişim Sistemi</string>
<string name="tmp_export_failed">Dışa aktarma başarısız oldu: %s</string>
<string name="export_failed_create">dışa aktarılan dosya oluşturulamıyor</string>
<string name="export_failed_export">dosya dışa aktarılamadı</string>
<string name="export_mem">Belleğe aktarılıyor…</string>
<string name="export_disk">Diske dışarı aktarılıyor…</string>
<string name="memfd_create_unsupported">Mevcut çekirdeğiniz memfd_create() özelliğini desteklemiyor. Bu özellik minimum %s çekirdek sürümünü gerektirir.</string>
<string name="export_method">Dışa aktarma yöntemi</string>
<string name="export_method_summary">Dosya dışa aktarma yöntemi. Açıkta kalan dosyaları paylaşmak, harici olarak açmak ve bunlara erişmek için kullanılır.</string>
<string name="debug">Debug</string>
<string name="logcat_title">DroidFS Logcat</string>
<string name="logcat_saved">Logcat kaydedildi</string>
</resources>

View File

@ -1,20 +0,0 @@
<resources>
<string-array name="sort_orders_entries">
<item>名称</item>
<item>大小</item>
<item>日期</item>
<item>名称 (降序)</item>
<item>大小 (降序)</item>
<item>日期 (降序)</item>
</string-array>
<string-array name="color_names">
<item>绿</item>
<item></item>
<item></item>
<item></item>
<item></item>
<item></item>
<item></item>
</string-array>
</resources>

View File

@ -1,277 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">DroidFS</string>
<string name="create_volume">创建加密卷</string>
<string name="open">打开</string>
<string name="create">创建</string>
<string name="change_password">修改密码</string>
<string name="password">密码</string>
<string name="import_files">导入文件</string>
<string name="import_folder">导入文件夹</string>
<string name="discovering_files">正在清点文件…</string>
<string name="mkdir">新建文件夹</string>
<string name="dir_empty">文件夹空</string>
<string name="warning">警告!</string>
<string name="ask_lock_volume">你想要锁定该卷吗?</string>
<string name="ok">锁了</string>
<string name="cancel">取消</string>
<string name="enter_folder_name">文件夹名称</string>
<string name="error">出问题了</string>
<string name="error_filename_empty">输入名称</string>
<string name="error_mkdir">文件夹创建失败</string>
<string name="success_import">导入成功</string>
<string name="success_import_msg">选中文件成功导入</string>
<string name="import_failed">导入 %s 时失败</string>
<string name="export_failed">导出 %s 时失败</string>
<string name="success_export">导出成功</string>
<string name="remove_failed">删除 %s 失败</string>
<string name="passwords_mismatch">密码不匹配</string>
<string name="dir_not_empty">选中文件夹非空</string>
<string name="create_volume_failed">创建卷失败</string>
<string name="open_volume_failed">打开失败</string>
<string name="share_chooser">分享文件</string>
<string name="storage_perm_denied">存储空间权限被拒绝</string>
<string name="storage_perm_denied_msg">没有存储权限时DroidFS无法工作</string>
<string name="get_size_failed">无法获取文件的大小</string>
<string name="parent_folder">上一级文件夹</string>
<string name="enter_volume_path">输入卷的路径</string>
<string name="enter_volume_name">输入卷的名称</string>
<string name="external_open">使用外部软件打开</string>
<string name="single_delete_confirm">确认删除%s?</string>
<string name="multiple_delete_confirm">确认删除多个文件:%s</string>
<string name="location">位置: %s</string>
<string name="total_size">总大小: %s</string>
<string name="import_from_other_volume">从别的卷导入</string>
<string name="read_file_failed">打开文件失败</string>
<string name="volume">卷: %s</string>
<string name="yes">确认</string>
<string name="no">取消</string>
<string name="ask_for_wipe">确认擦除原始文件?</string>
<string name="wipe_failed">擦除失败: %s</string>
<string name="wipe_successful">擦除成功!</string>
<string name="rename">重命名</string>
<string name="rename_title">新名称:</string>
<string name="rename_failed">无法重命名: %s</string>
<string name="sort_order">排序方法:</string>
<string name="change_password_failed">操作失败,请检查你的旧密码</string>
<string name="share_menu_label">使用DroidFS加密</string>
<string name="share_intent_parsing_failed">处理分享请求失败</string>
<string name="listdir_null_error_msg">无法访问这个文件夹</string>
<string name="fingerprint_save_checkbox_text">使用指纹存储</string>
<string name="fingerprint_instruction">请使用指纹传感器</string>
<string name="illegal_block_size_exception">块大小非法(illegalBlockSizeException)</string>
<string name="illegal_block_size_exception_msg">在添加指纹后会出现此问题,重置保存的哈希值可解决此问题,随后你需要重新关联哈希与指纹</string>
<string name="reset_hash_storage">重置保存的哈希</string>
<string name="MAC_verification_failed">签名/MAC验证失败。安卓密钥存储或者哈希存储已经受到修改。重置保存的哈希值可解决此问题随后你需要重新关联哈希与指纹</string>
<string name="hash_storage_reset">哈希存储已经成功重置</string>
<string name="encrypt_action_description">正在加密并保存密码哈希</string>
<string name="decrypt_action_description">正在解密密码哈希</string>
<string name="title_activity_settings">DroidFS设置</string>
<string name="explorer">浏览</string>
<string name="settings_title_sort_order">默认排序方法</string>
<string name="usf_decrypt">允许导出文件</string>
<string name="usf_share">允许通过系统分享菜单分享文件</string>
<string name="usf_open">允许其他应用打开文件</string>
<string name="usf_screenshot">允许截屏</string>
<string name="usf_fingerprint">允许通过指纹保存密码哈希</string>
<string name="usf_volume_management">加密卷管理</string>
<string name="usf_keep_open">当应用切入后台时已打开的卷不再自动锁上</string>
<string name="unsafe_features">以下功能会降低安全性</string>
<string name="manage_unsafe_features">管理非安全功能</string>
<string name="manage_unsafe_features_summary">打开/关闭非安全功能</string>
<string name="usf_home_warning_msg">DroidFS会尽可能保证安全。但高度安全往往伴随着不便。这也是DroidFS允许你按照习惯打开/关闭非安全功能的原因。\n\n警告这些功能会 降 低 安 全 性。在不清楚风险的情况下尽量不要使用。高度建议在启用这些功能之前阅读相关文档</string>
<string name="see_unsafe_features">查看非安全功能</string>
<string name="open_as">打开为</string>
<string name="image">图像</string>
<string name="video">视频</string>
<string name="audio">音频</string>
<string name="playing_failed">无法播放这个文件: %s</string>
<string name="text">文本</string>
<string name="save_failed">保存失败</string>
<string name="file_saved">文件已保存!</string>
<string name="ask_save">有些更改未保存,在关闭文件之前要看看吗</string>
<string name="save">保存</string>
<string name="discard">丢弃</string>
<string name="word_wrap">自动换行</string>
<string name="outofmemoryerror_msg">内存耗尽: 文件过大而无法读入内存</string>
<string name="new_file">新建文件</string>
<string name="enter_file_name">文件名</string>
<string name="file_creation_failed">无法创建文件</string>
<string name="loading">加载中…</string>
<string name="loading_msg_create">正在创建卷…</string>
<string name="loading_msg_change_password">正在更改密码…</string>
<string name="loading_msg_open">正在开启卷…</string>
<string name="loading_msg_export">导出文件…</string>
<string name="query_cursor_null_error_msg">无法访问文件</string>
<string name="about">关于</string>
<string name="github">Github</string>
<string name="github_summary">DroidFS在Github上的仓库。存有源码文档以及BUG追踪等</string>
<string name="gitea">Gitea</string>
<string name="gitea_summary">DroidFS在Chapril Gitea站上的仓库。与Github不同, Gitea是完全的自主搭建的自由软件.存有源码文档以及BUG追踪等</string>
<string name="share">分享</string>
<string name="decrypt_files">导出</string>
<string name="copy_failed">复制%s失败</string>
<string name="copy_success">复制成功</string>
<string name="add">添加</string>
<string name="camera">相机</string>
<string name="picture_save_success">图片已经保存至%s</string>
<string name="picture_save_failed">保存图片失败</string>
<string name="video_save_success">视频已经保存至%s</string>
<string name="file_overwrite_question">%s已经存在覆盖吗?</string>
<string name="dir_overwrite_question">%s已经存在要合并吗?</string>
<string name="enter_new_name">输入新名称</string>
<string name="copy_menu_title">复制</string>
<string name="move_failed">移动%s失败</string>
<string name="move_success">移动成功</string>
<string name="enter_timer_duration">输入持续时间(单位: 秒)</string>
<string name="path_error">检索所选路径失败</string>
<string name="create_cant_write_error_msg">DroidFS对于该路径并无写入权限换一个吧</string>
<string name="add_cant_write_warning">DroidFS对于该路径并无写入权限将会以只读方式添加卷</string>
<string name="sdcard_error_header">DroidFS仅对可移动存储的如下路径有写入权限:</string>
<string name="sdcard_error_add_footer">以只读方式添加卷</string>
<string name="sdcard_error_create_footer">请在该路径下使用或者使用内置存储</string>
<string name="slideshow_stopped">停止幻灯片放映</string>
<string name="slideshow_started">开始幻灯片放映</string>
<string name="ask_save_img_rotated">图片已经被旋转。要存储并覆盖原图吗</string>
<string name="image_saved_successfully">图片改动已经成功保存</string>
<string name="bitmap_compress_failed">位图压缩失败</string>
<string name="file_write_failed">写入文件失败</string>
<string name="error_not_a_volume">加密卷未能被识别,请检查路径</string>
<string name="version">版本</string>
<string name="error_cipher_null">错误: 密文为空</string>
<string name="key_permanently_invalidated_exception">KeyPermanentlyInvalidatedException</string>
<string name="key_permanently_invalidated_exception_msg">看起来你添加了新的指纹。原有的密码哈希将失效</string>
<string name="usf_read_doc">在启用这些功能之前请仔细阅读</string>
<string name="usf_doc">非安全功能的说明文档</string>
<string name="error_retrieving_filename">通过URI检索文件失败: %s</string>
<string name="hidden_volume">隐藏卷</string>
<string name="error_slash_in_name">卷名不应当包含斜杠</string>
<string name="hidden_volume_warning">隐藏卷存放在DroidFS的私有文件夹内其他应用在没有Root权限的情况下无法读取隐藏卷。如果你卸载DroidFS或者在应用管理器中清除了DroidFS的数据隐藏卷内的文件会全部消失。请提前做好备份</string>
<string name="camera_perm_needed">照相需要授予相机权限</string>
<string name="choose_resolution">选择分辨率</string>
<string name="file_operations">文件操作</string>
<string name="file_op_copy_msg">正在复制文件…</string>
<string name="file_op_import_msg">正在导入…</string>
<string name="file_op_export_msg">正在导出…</string>
<string name="file_op_move_msg">正在移动文件…</string>
<string name="file_op_wiping_msg">正在擦除文件…</string>
<string name="folders_first">文件夹优先</string>
<string name="folders_first_summary">在列表顶部显示文件夹</string>
<string name="auto_fit_title">视频播放器屏幕自动旋转</string>
<string name="auto_fit_summary">自动旋转屏幕以适应屏幕尺寸</string>
<string name="open_tree_failed">未发现文件浏览器。请安装后重试</string>
<string name="close_volume">关闭卷</string>
<string name="sort_by">分类方式</string>
<string name="cut">剪切</string>
<string name="map_folders">显示文件夹大小</string>
<string name="map_folders_summary">通过递归映射来计算文件夹的大小(如果文件夹很大启用该功能将导致APP卡顿)</string>
<string name="camera_optimization">相机优化</string>
<string name="maximize_quality">最大质量</string>
<string name="minimize_latency">最低延迟</string>
<string name="auto">自动</string>
<string name="encryption_cipher_label">加密密文</string>
<string name="theme">主题</string>
<string name="thumbnails">缩略图</string>
<string name="thumbnails_summary">展示图片与视频的缩略图</string>
<string name="seek_seconds_forward">+%d 秒</string>
<string name="seek_seconds_backward">-%d 秒</string>
<string name="add_volume">添加卷</string>
<string name="pick_directory">选择文件夹</string>
<string name="volume_alread_saved">卷已经保存</string>
<string name="open_dialog_title">正在打开%s:</string>
<string name="remove">移除</string>
<string name="settings">设置</string>
<string name="select_all">全选</string>
<string name="remove_fingerprint">移除指纹验证</string>
<string name="unrecoverable_key_exception_msg">%s. 无法加载加密密钥</string>
<string name="unrecoverable_key_exception">UnrecoverableKeyException</string>
<string name="delete_hidden_volume_question">%s 是隐藏的你希望DroidFS暂时不在UI上显示该卷的入口还是希望DroidFS删除卷内的文件?</string>
<string name="forget_only">仅删除UI上的入口</string>
<string name="delete_volume">删除卷</string>
<string name="hidden_volume_description">在DroidFS的私有文件夹中存放该卷</string>
<string name="error_is_file">出错: 文件已存在</string>
<string name="volume_path_label">选择加密卷的路径:</string>
<string name="volume_name_label">输入卷的名称:</string>
<string name="volume_path_hint">卷的路径</string>
<string name="volume_name_hint">卷的名称</string>
<string name="password_label">输入卷的密码:</string>
<string name="password_confirmation_label">再次输入密码:</string>
<string name="password_confirmation_hint">密码(确认)</string>
<string name="password_hash_saved">密码的哈希值已保存</string>
<string name="no_volumes_text">没有保存的卷,通过点击\"+\"添加</string>
<string name="fingerprint_error_msg">指纹验证无法使用: %s</string>
<string name="keyguard_not_secure">Keyguard不安全</string>
<string name="no_hardware">未发现适用硬件</string>
<string name="hardware_unavailable">硬件不可用</string>
<string name="no_fingerprint">无已经录入的指纹</string>
<string name="unknown_error">未知错误</string>
<string name="biometric_error">生物识别错误: %s</string>
<string name="apply_to_all">将该设置应用到所有隐藏卷</string>
<string name="select_volume">选择卷</string>
<string name="current_password_label">输入该卷的密码</string>
<string name="current_password_hint">当前密码</string>
<string name="new_password_label">输入新密码:</string>
<string name="new_password_hint">新密码</string>
<string name="new_password_confirmation_label">确认新密码:</string>
<string name="error_marshmallow_required">该功能仅在安卓6.0及以上可用</string>
<string name="copy_hidden_volume">复制到共享存储</string>
<string name="copy_external_volume">创建隐藏副本</string>
<string name="copy_volume_notification">正在复制卷…</string>
<string name="hidden_volume_already_exists">一个有相同名字的隐藏卷已经存在</string>
<string name="pdf_document">PDF文档</string>
<string name="thumbnail_max_size">缩略图最大体积</string>
<string name="thumbnail_max_size_summary">允许的缩略图文件最大体积. 当前值:</string>
<string name="size_hint">大小(KB):</string>
<string name="invalid_number">无效数字</string>
<string name="new_volume_name">新卷名:</string>
<string name="volume_rename_failed">卷重命名失败</string>
<string name="switch_display_layout">切换显示排版</string>
<string name="one_file">单文件</string>
<string name="multiple_files">%d个文件</string>
<string name="one_folder">文件夹</string>
<string name="multiple_folders">%d个文件夹</string>
<string name="default_open">启动DroidFS时自动打开卷</string>
<string name="remove_default_open">不要默认打开</string>
<string name="elements_selected">已选 %d/%d</string>
<string name="pin_passwords_title">数字键盘布局</string>
<string name="pin_passwords_summary">输入卷的密码时使用纯数字键盘</string>
<string name="volume_type_label">卷的类型:</string>
<string name="gocryptfs">Gocryptfs</string>
<string name="cryfs">CryFS</string>
<string name="gocryptfs_disabled">Gocrytfs支持已关闭</string>
<string name="cryfs_disabled">CryFS支持已关闭</string>
<string name="file_op_delete_msg">正在删除文件…</string>
<string name="volume_type">(%s)</string>
<string name="volume_type_read_only">(%s, 只读)</string>
<string name="volume_type_inaccessible">(%s, 不可访问)</string>
<string name="io_error">I/O出错</string>
<string name="use_fingerprint">使用指纹替代当前密码</string>
<string name="remember_volume">记住卷</string>
<string name="open_volume">打开卷</string>
<string name="choose_existing_volume">请选择一个已存在的卷</string>
<string name="volume_unlocked">卷已锁定</string>
<string name="lock_volume">锁定卷</string>
<string name="lock">锁定</string>
<string name="ux">用户体验</string>
<string name="theme_color">主题色</string>
<string name="theme_color_summary">更改DroidFS用户界面主题色</string>
<string name="black_theme">黑色主题</string>
<string name="password_fallback">返回密码验证</string>
<string name="password_fallback_summary">当指纹验证取消时自动弹出密码验证</string>
<string name="unknown_error_code">未知错误: %d</string>
<string name="config_load_error">无法读取配置. 请确保卷可访问</string>
<string name="wrong_password">无法解密配置文件. 请检查你的密码</string>
<string name="filesystem_id_changed">文件系统的ID与上一次打开时的ID不一样. 这可能意味着攻击者已经将文件系统替换</string>
<string name="inaccessible_base_dir">卷不存在或者不可访问</string>
<string name="task_failed">任务失败: %s</string>
<string name="usf_expose">暴露已打开的卷</string>
<string name="usf_expose_summary">允许其他应用软件通过DocumentsProviders接口读取卷内的文件</string>
<string name="usf_saf_write">允许写入</string>
<string name="usf_saf_write_summary">允许其他应用读取和删改卷内的文件</string>
<string name="saf">存储访问框架(SAF)</string>
<string name="tmp_export_failed">导出失败: %s</string>
<string name="export_failed_create">无法创建导出文件</string>
<string name="export_failed_export">导出文件失败</string>
<string name="export_mem">导出至内存</string>
<string name="export_disk">导出至磁盘</string>
</resources>

View File

@ -38,12 +38,6 @@
<item>Pink</item> <item>Pink</item>
</string-array> </string-array>
<string-array name="export_methods">
<item>Auto (depending on available memory)</item>
<item>Temporary export on disk (reliable but may leave traces)</item>
<item>Memory file (safer but doesn\'t always work)</item>
</string-array>
<!-- don't translate the following otherwise the app will crash --> <!-- don't translate the following otherwise the app will crash -->
<string-array name="sort_orders_values"> <string-array name="sort_orders_values">
<item>name</item> <item>name</item>
@ -63,10 +57,4 @@
<item>purple</item> <item>purple</item>
<item>pink</item> <item>pink</item>
</string-array> </string-array>
<string-array name="export_methods_values">
<item>auto</item>
<item>disk</item>
<item>memory</item>
</string-array>
</resources> </resources>

View File

@ -273,10 +273,4 @@
<string name="export_failed_export">failed to export file</string> <string name="export_failed_export">failed to export file</string>
<string name="export_mem">Exporting to memory…</string> <string name="export_mem">Exporting to memory…</string>
<string name="export_disk">Exporting to disk…</string> <string name="export_disk">Exporting to disk…</string>
<string name="memfd_create_unsupported">Your current kernel does not support memfd_create(). This feature requires a minimum kernel version of %s.</string>
<string name="export_method">Export method</string>
<string name="export_method_summary">File export method. Used for sharing, external opening and accessing exposed files.</string>
<string name="debug">Debug</string>
<string name="logcat_title">DroidFS Logcat</string>
<string name="logcat_saved">Logcat saved</string>
</resources> </resources>

View File

@ -93,16 +93,6 @@
</PreferenceCategory> </PreferenceCategory>
<PreferenceCategory android:title="@string/debug">
<Preference
android:key="logcat"
android:title="Logcat"
android:summary="View the DroidFS logcat"
android:icon="@drawable/icon_debug"/>
</PreferenceCategory>
<PreferenceCategory android:title="@string/about"> <PreferenceCategory android:title="@string/about">
<Preference <Preference
@ -120,6 +110,7 @@
</Preference> </Preference>
<Preference <Preference
android:key="version"
android:icon="@drawable/icon_info" android:icon="@drawable/icon_info"
android:title="@string/version" android:title="@string/version"
android:summary="@string/versionName"/> <!--added by gradle at build time--> android:summary="@string/versionName"/> <!--added by gradle at build time-->

View File

@ -78,15 +78,6 @@
android:title="@string/usf_saf_write" android:title="@string/usf_saf_write"
android:summary="@string/usf_saf_write_summary" /> android:summary="@string/usf_saf_write_summary" />
<ListPreference
android:key="export_method"
android:entries="@array/export_methods"
android:entryValues="@array/export_methods_values"
android:defaultValue="auto"
android:title="@string/export_method"
android:summary="@string/export_method_summary"
android:icon="@drawable/icon_settings"/>
</PreferenceCategory> </PreferenceCategory>
</PreferenceScreen> </PreferenceScreen>

View File

@ -1,6 +1,13 @@
plugins { buildscript {
id("com.android.application") version '8.4.0' apply false ext.kotlin_version = '1.8.21'
id("org.jetbrains.kotlin.android") version "1.9.24" apply false repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:7.4.2'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
} }
allprojects { allprojects {
@ -10,6 +17,6 @@ allprojects {
} }
} }
tasks.register('clean', Delete) { task clean(type: Delete) {
delete layout.buildDirectory delete rootProject.buildDir
} }

View File

@ -1,6 +0,0 @@
- New unsafe feature: Expose open volumes
- Ability to grant read-write access to external apps
- New German translation
- Dependencies updates
- UX fixes
- Crashes fixes

View File

@ -1,2 +0,0 @@
- Dependencies updates
- Fix database upgrade crash

View File

@ -1 +0,0 @@
- Really fix database upgrade crash

View File

@ -1,2 +0,0 @@
- Fix crash on Android 13
- Upgrade CameraX version

View File

@ -7,6 +7,10 @@
# Specifies the JVM arguments used for the daemon process. # Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings. # The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx2048m org.gradle.jvmargs=-Xmx2048m
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the # AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app"s APK # Android operating system, and which are packaged with your app"s APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn # https://developer.android.com/topic/libraries/support-library/androidx-rn
@ -14,6 +18,4 @@ android.useAndroidX=true
# Kotlin code style for this project: "official" or "obsolete": # Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official kotlin.code.style=official
android.native.buildOutput=verbose android.native.buildOutput=verbose
android.nonTransitiveRClass=false
org.gradle.configuration-cache=true

Binary file not shown.

View File

@ -1,8 +1,7 @@
#Sun Jun 02 15:50:33 UTC 2024 #Wed Feb 01 20:48:39 UTC 2023
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionSha256Sum=a4b4158601f8636cdeeab09bd76afb640030bb5b144aafe261a5e8af027dc612 distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip distributionSha256Sum=7ba68c54029790ab444b39d7e293d3236b2632631fb5f2e012bb28b4ff669e4b
networkTimeout=10000
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME

269
gradlew vendored
View File

@ -1,7 +1,7 @@
#!/bin/sh #!/usr/bin/env sh
# #
# Copyright © 2015-2021 the original authors. # Copyright 2015 the original author or authors.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
@ -17,101 +17,67 @@
# #
############################################################################## ##############################################################################
# ##
# Gradle start up script for POSIX generated by Gradle. ## Gradle start up script for UN*X
# ##
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
############################################################################## ##############################################################################
# Attempt to set APP_HOME # Attempt to set APP_HOME
# Resolve links: $0 may be a link # Resolve links: $0 may be a link
app_path=$0 PRG="$0"
# Need this for relative symlinks.
# Need this for daisy-chained symlinks. while [ -h "$PRG" ] ; do
while ls=`ls -ld "$PRG"`
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path link=`expr "$ls" : '.*-> \(.*\)$'`
[ -h "$app_path" ] if expr "$link" : '/.*' > /dev/null; then
do PRG="$link"
ls=$( ls -ld "$app_path" ) else
link=${ls#*' -> '} PRG=`dirname "$PRG"`"/$link"
case $link in #( fi
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
# This is normally unused APP_NAME="Gradle"
# shellcheck disable=SC2034 APP_BASE_NAME=`basename "$0"`
APP_BASE_NAME=${0##*/}
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value. # Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum MAX_FD="maximum"
warn () { warn () {
echo "$*" echo "$*"
} >&2 }
die () { die () {
echo echo
echo "$*" echo "$*"
echo echo
exit 1 exit 1
} >&2 }
# OS specific support (must be 'true' or 'false'). # OS specific support (must be 'true' or 'false').
cygwin=false cygwin=false
msys=false msys=false
darwin=false darwin=false
nonstop=false nonstop=false
case "$( uname )" in #( case "`uname`" in
CYGWIN* ) cygwin=true ;; #( CYGWIN* )
Darwin* ) darwin=true ;; #( cygwin=true
MSYS* | MINGW* ) msys=true ;; #( ;;
NONSTOP* ) nonstop=true ;; Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
@ -121,9 +87,9 @@ CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
if [ -n "$JAVA_HOME" ] ; then if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables # IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java JAVACMD="$JAVA_HOME/jre/sh/java"
else else
JAVACMD=$JAVA_HOME/bin/java JAVACMD="$JAVA_HOME/bin/java"
fi fi
if [ ! -x "$JAVACMD" ] ; then if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
@ -132,7 +98,7 @@ Please set the JAVA_HOME variable in your environment to match the
location of your Java installation." location of your Java installation."
fi fi
else else
JAVACMD=java JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the Please set the JAVA_HOME variable in your environment to match the
@ -140,105 +106,80 @@ location of your Java installation."
fi fi
# Increase the maximum file descriptors if we can. # Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
case $MAX_FD in #( MAX_FD_LIMIT=`ulimit -H -n`
max*) if [ $? -eq 0 ] ; then
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
# shellcheck disable=SC3045 MAX_FD="$MAX_FD_LIMIT"
MAX_FD=$( ulimit -H -n ) || fi
warn "Could not query maximum file descriptor limit" ulimit -n $MAX_FD
esac if [ $? -ne 0 ] ; then
case $MAX_FD in #( warn "Could not set maximum file descriptor limit: $MAX_FD"
'' | soft) :;; #( fi
*) else
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
# shellcheck disable=SC3045 fi
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi fi
# Collect all arguments for the java command, stacking in reverse order: # For Darwin, add options to specify how the application appears in the dock
# * args from the command line if $darwin; then
# * the main class name GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
# * -classpath fi
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java # For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=$( cygpath --unix "$JAVACMD" ) JAVACMD=`cygpath --unix "$JAVACMD"`
# Now convert the arguments - kludge to limit ourselves to /bin/sh # We build the pattern for arguments to be converted via cygpath
for arg do ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
if SEP=""
case $arg in #( for dir in $ROOTDIRSRAW ; do
-*) false ;; # don't mess with options #( ROOTDIRS="$ROOTDIRS$SEP$dir"
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath SEP="|"
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=`expr $i + 1`
done
case $i in
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi fi
# Collect all arguments for the java command; # Escape application args
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of save () {
# shell script including quotes and variable substitutions, so put them in for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
# double quotes to make sure that they get re-expanded; and echo " "
# * put everything else in single quotes, so that it's not re-expanded. }
APP_ARGS=`save "$@"`
set -- \ # Collect all arguments for the java command, following the shell quoting and substitution rules
"-Dorg.gradle.appname=$APP_BASE_NAME" \ eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@" exec "$JAVACMD" "$@"

15
gradlew.bat vendored
View File

@ -14,7 +14,7 @@
@rem limitations under the License. @rem limitations under the License.
@rem @rem
@if "%DEBUG%"=="" @echo off @if "%DEBUG%" == "" @echo off
@rem ########################################################################## @rem ##########################################################################
@rem @rem
@rem Gradle startup script for Windows @rem Gradle startup script for Windows
@ -25,8 +25,7 @@
if "%OS%"=="Windows_NT" setlocal if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0 set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=. if "%DIRNAME%" == "" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0 set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME% set APP_HOME=%DIRNAME%
@ -41,7 +40,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1 %JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute if "%ERRORLEVEL%" == "0" goto execute
echo. echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
@ -76,15 +75,13 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
:end :end
@rem End local scope for the variables with windows NT shell @rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd if "%ERRORLEVEL%"=="0" goto mainEnd
:fail :fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code! rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL% if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
if %EXIT_CODE% equ 0 set EXIT_CODE=1 exit /b 1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd :mainEnd
if "%OS%"=="Windows_NT" endlocal if "%OS%"=="Windows_NT" endlocal

@ -1 +1 @@
Subproject commit 48631380a7a8b5f0932078e2b643e06c3f433890 Subproject commit c74b374ec49a1f47b9879b8fbc7b72b046ef55fd

View File

@ -1,8 +1,2 @@
pluginManagement {
repositories {
google()
mavenCentral()
}
}
include ':app', ':libpdfviewer:app' include ':app', ':libpdfviewer:app'
rootProject.name = "DroidFS" rootProject.name = "DroidFS"