Compare commits

..

1 Commits

Author SHA1 Message Date
Matéo Duparc e851f381a0
Volume provider (unsecure!!!) 2023-08-28 00:35:12 +02:00
53 changed files with 468 additions and 1792 deletions

View File

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

View File

@ -11,7 +11,7 @@ For mortals: Encrypted storage compatible with already existing softwares.
</p>
# Support
The creator of DroidFS works as a freelance developer and privacy consultant. I am currently looking for new clients! If you are interested, take a look at the [website](https://arkensys.dedyn.io). Alternatively, you can directly support DroidFS by making a [donation](https://forge.chapril.org/hardcoresushi/DroidFS/src/branch/master/DONATE.txt).
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 ❤️.
@ -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.
</li>
<li><h4>Allow opening files with other applications*:</h4>
Decrypt and open file using external apps. These apps could save and send the files thus opened.
</li>
<li><h4>Allow exporting files:</h4>
Decrypt and write file to disk (external storage). Any app with storage permissions could access exported files.
</li>
<li><h4>Allow sharing files via the android share menu*:</h4>
Decrypt and share file with other apps. These apps could save and send the files thus shared.
</li>
<li><h4>Allow saving password hash using fingerprint:</h4>
Generate an AES-256 GCM key in the Android Keystore (protected by fingerprint authentication), then use it to encrypt the volume password hash and store it to the DroidFS internal storage. This require Android v6.0+. If your device is not encrypted, extracting the encryption key with physical access may be possible.
</li>
<li><h4>Keep volume open when the app goes in background:</h4>
Don't close the volume when you leave the app but keep running it in the background. Anyone going back to the activity could have access to the volume.
</li>
<li><h4>Allow opening files with other applications*:</h4>
Decrypt and open file using external apps. These apps could save and send the files thus opened.
</li>
<li><h4>Expose open volumes*:</h4>
Allow open volumes to be browsed in the system file explorer (<a href="https://developer.android.com/guide/topics/providers/document-provider">DocumentProvider</a> API). Encrypted files can then be selected from other applications, potentially with permanent access. This feature requires <i>"Keep volume open when the app goes in background"</i> to be enabled.
</li>
<li><h4>Grant write access:</h4>
Files opened with another applications can be modified by them. This applies to both previous unsafe features.
<li><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>
</ul>
\* These features may require temporarily writing the plain file to disk (DroidFS internal storage). This file can be read by applications with root access or by physical access if your device is not encrypted. For files small enough and on a 3.17+ kernel, DroidFS will try to use memory-only storage using `memfd_create(2)` (can break some apps).
* 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.
# Download
<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>
# 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.

View File

@ -20,25 +20,21 @@ if (hasProperty("nosplits")) {
}
android {
compileSdk 34
ndkVersion "25.2.9519653"
compileSdkVersion 33
buildToolsVersion "33.0.0"
ndkVersion "25.1.8937393"
namespace "sushi.hardcore.droidfs"
compileOptions {
targetCompatibility JavaVersion.VERSION_17
sourceCompatibility JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "17"
targetCompatibility JavaVersion.VERSION_1_8
}
defaultConfig {
applicationId "sushi.hardcore.droidfs"
minSdkVersion 21
targetSdkVersion 32
versionCode 36
versionName "2.1.3"
versionCode 32
versionName "2.0.2"
ndk {
abiFilters "x86", "x86_64", "armeabi-v7a", "arm64-v8a"
@ -63,7 +59,7 @@ android {
}
}
applicationVariants.configureEach { variant ->
applicationVariants.all { variant ->
variant.resValue "string", "versionName", variant.versionName
buildConfigField "boolean", "CRYFS_DISABLED", "${project.ext.disableCryFS}"
buildConfigField "boolean", "GOCRYPTFS_DISABLED", "${project.ext.disableGocryptfs}"
@ -71,7 +67,6 @@ android {
buildFeatures {
viewBinding true
buildConfig true
}
buildTypes {
@ -103,34 +98,34 @@ android {
dependencies {
implementation project(":libpdfviewer:app")
implementation 'androidx.core:core-ktx:1.12.0'
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
implementation 'androidx.core:core-ktx:1.10.0'
implementation "androidx.appcompat:appcompat:1.6.1"
implementation "androidx.constraintlayout:constraintlayout:2.1.4"
def lifecycle_version = "2.6.2"
def lifecycle_version = "2.6.1"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-process:$lifecycle_version"
implementation "androidx.sqlite:sqlite-ktx:2.3.1"
implementation "androidx.preference:preference-ktx:1.2.1"
implementation "androidx.preference:preference-ktx:1.2.0"
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.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"
def media3_version = "1.1.1"
implementation "androidx.media3:media3-exoplayer:$media3_version"
implementation 'androidx.media3:media3-ui:1.1.1'
implementation "androidx.media3:media3-datasource:$media3_version"
def exoplayer_version = "2.18.6"
implementation "com.google.android.exoplayer:exoplayer-core:$exoplayer_version"
implementation "com.google.android.exoplayer:exoplayer-ui:$exoplayer_version"
implementation "androidx.concurrent:concurrent-futures:1.1.0"
def camerax_version = "1.3.0-rc02"
def camerax_version = "1.3.0-alpha06"
implementation "androidx.camera:camera-camera2:$camerax_version"
implementation "androidx.camera:camera-lifecycle:$camerax_version"
implementation "androidx.camera:camera-view:$camerax_version"
implementation "androidx.camera:camera-extensions:$camerax_version"
def autoValueVersion = '1.10.4'
def autoValueVersion = "1.10.1"
implementation "com.google.auto.value:auto-value-annotations:$autoValueVersion"
annotationProcessor "com.google.auto.value:auto-value:$autoValueVersion"
}

@ -1 +1 @@
Subproject commit 6388eaf433a4196f10389921d5e346c90ee3d793
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.explorers.ExplorerElement
@ -8,17 +28,4 @@
-keepclassmembers class sushi.hardcore.droidfs.video_recording.FFmpegMuxer {
void writePacket(byte[]);
void seek(long);
}
# Required for Intent.getParcelableExtra() to work on Android 13
-keep class sushi.hardcore.droidfs.VolumeData {
public int describeContents();
}
-keep class sushi.hardcore.droidfs.VolumeData$* {
static public android.os.Parcelable$Creator CREATOR;
}
-keep class sushi.hardcore.droidfs.filesystems.EncryptedVolume {
public int describeContents();
}
-keep class sushi.hardcore.droidfs.filesystems.EncryptedVolume$* {
static public android.os.Parcelable$Creator CREATOR;
}

View File

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

View File

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

View File

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

View File

@ -35,7 +35,6 @@ import android.media.MediaCodec.BufferInfo;
import android.media.MediaCodecInfo;
import android.media.MediaFormat;
import android.os.Bundle;
import android.os.SystemClock;
import android.util.Range;
import android.view.Surface;
@ -1055,7 +1054,6 @@ public class SucklessEncoderImpl implements Encoder {
if (mIsVideoEncoder) {
Timebase inputTimebase;
if (DeviceQuirks.get(CameraUseInconsistentTimebaseQuirk.class) != null) {
Logger.w(mTag, "CameraUseInconsistentTimebaseQuirk is enabled");
inputTimebase = null;
} else {
inputTimebase = mInputTimebase;
@ -1067,7 +1065,7 @@ public class SucklessEncoderImpl implements Encoder {
}
@Override
public void onInputBufferAvailable(@NonNull MediaCodec mediaCodec, int index) {
public void onInputBufferAvailable(MediaCodec mediaCodec, int index) {
mEncoderExecutor.execute(() -> {
if (mStopped) {
Logger.w(mTag, "Receives input frame after codec is reset.");
@ -1133,15 +1131,6 @@ public class SucklessEncoderImpl implements Encoder {
if (checkBufferInfo(bufferInfo)) {
if (!mHasFirstData) {
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);
mLastSentAdjustedTimeUs = outBufferInfo.presentationTimeUs;

View File

@ -35,7 +35,6 @@ import android.media.MediaCodec.BufferInfo;
import android.media.MediaCodecInfo;
import android.media.MediaFormat;
import android.os.Bundle;
import android.os.SystemClock;
import android.util.Range;
import android.view.Surface;
@ -1054,7 +1053,6 @@ public class EncoderImpl implements Encoder {
if (mIsVideoEncoder) {
Timebase inputTimebase;
if (DeviceQuirks.get(CameraUseInconsistentTimebaseQuirk.class) != null) {
Logger.w(mTag, "CameraUseInconsistentTimebaseQuirk is enabled");
inputTimebase = null;
} else {
inputTimebase = mInputTimebase;
@ -1066,7 +1064,7 @@ public class EncoderImpl implements Encoder {
}
@Override
public void onInputBufferAvailable(@NonNull MediaCodec mediaCodec, int index) {
public void onInputBufferAvailable(MediaCodec mediaCodec, int index) {
mEncoderExecutor.execute(() -> {
if (mStopped) {
Logger.w(mTag, "Receives input frame after codec is reset.");
@ -1132,15 +1130,6 @@ public class EncoderImpl implements Encoder {
if (checkBufferInfo(bufferInfo)) {
if (!mHasFirstData) {
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);
mLastSentAdjustedTimeUs = outBufferInfo.presentationTimeUs;

View File

@ -56,8 +56,8 @@ public final class PendingRecording {
private final OutputOptions mOutputOptions;
private Consumer<VideoRecordEvent> mEventListener;
private Executor mListenerExecutor;
private boolean mAudioEnabled = false;
private boolean mIsPersistent = false;
PendingRecording(@NonNull Context context, @NonNull Recorder recorder,
@NonNull OutputOptions options) {
@ -102,10 +102,6 @@ public final class PendingRecording {
return mAudioEnabled;
}
boolean isPersistent() {
return mIsPersistent;
}
/**
* Enables audio to be recorded for this recording.
*
@ -141,69 +137,6 @@ public final class PendingRecording {
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.
*
@ -224,13 +157,7 @@ public final class PendingRecording {
*
* <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
* the recording needs to be active. If the recording is garbage collected, the
* {@link VideoRecordEvent.Finalize} event will contain error
* {@link VideoRecordEvent.Finalize#ERROR_RECORDING_GARBAGE_COLLECTED}.
*
* <p>The {@link Recording} will be stopped automatically if the {@link VideoCapture} its
* {@link Recorder} is attached to is unbound unless it's created
* {@link #asPersistentRecording() as a persistent recording}.
* the recording needs to be active.
*
* @throws IllegalStateException if the associated Recorder currently has an unfinished
* active recording.

View File

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

View File

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

View File

@ -1,7 +1,5 @@
#!/bin/sh
set -e
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 ..
done

View File

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

View File

@ -160,7 +160,7 @@ class ChangePasswordActivity: BaseActivity() {
)
}
if (success) {
if (volumeDatabase.isHashSaved(volume)) {
if (volumeDatabase.isHashSaved(volume.name)) {
volumeDatabase.removeHash(volume)
}
}

View File

@ -20,7 +20,6 @@ import kotlinx.coroutines.launch
import sushi.hardcore.droidfs.Constants.DEFAULT_VOLUME_KEY
import sushi.hardcore.droidfs.adapters.VolumeAdapter
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.DialogDeleteVolumeBinding
import sushi.hardcore.droidfs.explorers.ExplorerRouter
@ -196,7 +195,7 @@ class MainActivity : BaseActivity(), VolumeAdapter.Listener {
private fun removeVolume(volume: VolumeData) {
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) {
@ -325,14 +324,7 @@ class MainActivity : BaseActivity(), VolumeAdapter.Listener {
DocumentFile.fromFile(File(volume.name)),
DocumentFile.fromFile(hiddenVolumeFile.parentFile!!),
) {
VolumeData(
VolumeData.newUuid(),
volume.shortName,
true,
volume.type,
volume.encryptedHash,
volume.iv
)
VolumeData(volume.shortName, true, volume.type, volume.encryptedHash, volume.iv)
}
}
}
@ -382,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)
return true
}
@ -406,7 +398,6 @@ class MainActivity : BaseActivity(), VolumeAdapter.Listener {
val path = PathUtils.getFullPathFromTreeUri(dstRootDirectory.uri, this)
if (path == null) null
else VolumeData(
VolumeData.newUuid(),
PathUtils.pathJoin(path, name),
false,
volume.type,
@ -475,8 +466,7 @@ class MainActivity : BaseActivity(), VolumeAdapter.Listener {
DocumentFile.fromFile(srcPath).renameTo(newName)
}
if (success) {
volumeDatabase.renameVolume(volume, newDBName)
VolumeProvider.notifyRootsChanged(this)
volumeDatabase.renameVolume(volume.name, newDBName)
unselect(position)
if (volume.name == volumeOpener.defaultVolumeName) {
with (sharedPrefs.edit()) {

View File

@ -191,7 +191,7 @@ class SettingsActivity : BaseActivity() {
}
VolumeProvider.usfExpose = checked
updateView(usfExpose = checked)
VolumeProvider.notifyRootsChanged(requireContext())
VolumeProvider.notifyRootChanged(requireContext())
true
}
switchSafWrite.setOnPreferenceChangeListener { _, checked ->

View File

@ -8,19 +8,10 @@ import sushi.hardcore.droidfs.filesystems.GocryptfsVolume
import sushi.hardcore.droidfs.util.PathUtils
import java.io.File
import java.io.FileInputStream
import java.util.UUID
class VolumeData(
val uuid: String,
val name: String,
val isHidden: Boolean = false,
val type: Byte,
var encryptedHash: ByteArray? = null,
var iv: ByteArray? = null
) : Parcelable {
class VolumeData(val name: String, val isHidden: Boolean = false, val type: Byte, var encryptedHash: ByteArray? = null, var iv: ByteArray? = null): Parcelable {
constructor(parcel: Parcel) : this(
parcel.readString()!!,
parcel.readString()!!,
parcel.readByte() != 0.toByte(),
parcel.readByte(),
@ -32,7 +23,12 @@ class VolumeData(
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 {
val volumePath = getFullPath(filesDir)
@ -66,7 +62,6 @@ class VolumeData(
override fun writeToParcel(dest: Parcel, flags: Int) {
with (dest) {
writeString(uuid)
writeString(name)
writeByte(if (isHidden) 1 else 0)
writeByte(type)
@ -79,10 +74,12 @@ class VolumeData(
if (other !is VolumeData) {
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 {
const val VOLUMES_DIRECTORY = "volumes"
@ -93,17 +90,8 @@ class VolumeData(
override fun newArray(size: Int) = arrayOfNulls<VolumeData>(size)
}
fun newUuid(): String = UUID.randomUUID().toString()
fun getHiddenVolumeFullPath(filesDir: String, name: String): String {
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 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 {
private const val TAG = "VolumeDatabase"
private const val TABLE_NAME = "Volumes"
private const val COLUMN_UUID = "uuid"
private const val COLUMN_NAME = "name"
private const val COLUMN_HIDDEN = "hidden"
private const val COLUMN_TYPE = "type"
private const val COLUMN_HASH = "hash"
private const val COLUMN_IV = "iv"
}
const val TAG = "VolumeDatabase"
const val TABLE_NAME = "Volumes"
const val COLUMN_NAME = "name"
const val COLUMN_HIDDEN = "hidden"
const val COLUMN_TYPE = "type"
const val COLUMN_HASH = "hash"
const val COLUMN_IV = "iv"
private fun createTable(db: SQLiteDatabase) =
db.execSQL(
"CREATE TABLE IF NOT EXISTS $TABLE_NAME (" +
"$COLUMN_UUID TEXT PRIMARY KEY," +
"$COLUMN_NAME TEXT," +
"$COLUMN_HIDDEN SHORT," +
"$COLUMN_TYPE BLOB," +
"$COLUMN_HASH BLOB," +
"$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")
}
}
private fun contentValuesFromVolume(volume: VolumeData): ContentValues {
val contentValues = ContentValues()
contentValues.put(COLUMN_NAME, volume.name)
contentValues.put(COLUMN_HIDDEN, volume.isHidden)
contentValues.put(COLUMN_TYPE, byteArrayOf(volume.type))
contentValues.put(COLUMN_HASH, volume.encryptedHash)
contentValues.put(COLUMN_IV, volume.iv)
return contentValues
}
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 {
return File(
VolumeData.getFullPath(volumeName, true, context.filesDir.path)
VolumeData(
volumeName,
true,
EncryptedVolume.GOCRYPTFS_VOLUME_TYPE
).getFullPath(context.filesDir.path)
).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 {
return VolumeData(
cursor.getString(cursor.getColumnIndexOrThrow(COLUMN_UUID)),
cursor.getString(cursor.getColumnIndexOrThrow(COLUMN_NAME)),
cursor.getShort(cursor.getColumnIndexOrThrow(COLUMN_HIDDEN)) == 1.toShort(),
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 {
if (!isVolumeSaved(volume.name, volume.isHidden)) {
return (writableDatabase.insert(TABLE_NAME, null, ContentValues().apply {
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 (writableDatabase.insert(TABLE_NAME, null, contentValuesFromVolume(volume)) >= 0.toLong())
}
return false
}
@ -214,8 +157,8 @@ class VolumeDatabase(private val context: Context): SQLiteOpenHelper(context, Co
return list
}
fun isHashSaved(volume: VolumeData): Boolean {
val cursor = readableDatabase.rawQuery("SELECT $COLUMN_HASH FROM $TABLE_NAME WHERE $COLUMN_UUID=?", arrayOf(volume.uuid))
fun isHashSaved(volumeName: String): Boolean {
val cursor = readableDatabase.query(TABLE_NAME, arrayOf(COLUMN_NAME, COLUMN_HASH), "$COLUMN_NAME=?", arrayOf(volumeName), null, null, null)
var isHashSaved = false
if (cursor.moveToNext()) {
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 {
return writableDatabase.update(TABLE_NAME, ContentValues().apply {
put(COLUMN_HASH, volume.encryptedHash)
put(COLUMN_IV, volume.iv)
}, "$COLUMN_UUID=?", arrayOf(volume.uuid)) > 0
return writableDatabase.update(TABLE_NAME, contentValuesFromVolume(volume), "$COLUMN_NAME=?", arrayOf(volume.name)) > 0
}
fun removeHash(volume: VolumeData): Boolean {
return writableDatabase.update(
TABLE_NAME,
ContentValues().apply {
put(COLUMN_HASH, null as ByteArray?)
put(COLUMN_IV, null as ByteArray?)
}, "$COLUMN_UUID=?", arrayOf(volume.uuid)
) > 0
TABLE_NAME, contentValuesFromVolume(
VolumeData(
volume.name,
volume.isHidden,
volume.type,
null,
null
)
), "$COLUMN_NAME=?", arrayOf(volume.name)) > 0
}
fun renameVolume(volume: VolumeData, newName: String): Boolean {
return writableDatabase.update(
TABLE_NAME,
fun renameVolume(oldName: String, newName: String): Boolean {
return writableDatabase.update(TABLE_NAME,
ContentValues().apply {
put(COLUMN_NAME, newName)
},
"$COLUMN_UUID=?", arrayOf(volume.uuid)
"$COLUMN_NAME=?",arrayOf(oldName)
) > 0
}
fun removeVolume(volume: VolumeData): Boolean {
return writableDatabase.delete(TABLE_NAME, "$COLUMN_UUID=?", arrayOf(volume.uuid)) > 0
fun removeVolume(volumeName: String): Boolean {
return writableDatabase.delete(TABLE_NAME, "$COLUMN_NAME=?", arrayOf(volumeName)) > 0
}
}

View File

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

View File

@ -211,11 +211,11 @@ class CreateVolumeFragment: Fragment() {
.show()
} else {
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
volumeDatabase.apply {
if (isVolumeSaved(volumeName, isHiddenVolume)) // cleaning old saved path
removeVolume(volume)
removeVolume(volumeName)
if (rememberVolume) {
isVolumeSaved = saveVolume(volume)
}

View File

@ -350,7 +350,7 @@ class SelectPathFragment: Fragment() {
}
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) {
volumeDatabase.saveVolume(volumeData)
}

View File

@ -2,7 +2,6 @@ package sushi.hardcore.droidfs.content_providers
import android.content.ContentProvider
import android.content.ContentValues
import android.content.Intent
import android.database.Cursor
import android.database.MatrixCursor
import android.net.Uri
@ -136,7 +135,6 @@ class TemporaryFileProvider : ContentProvider() {
// this must not be cancelled
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) {
for (i in files.values) {
i.file.free()

View File

@ -1,6 +1,7 @@
package sushi.hardcore.droidfs.content_providers
import android.content.Context
import android.content.Intent
import android.database.Cursor
import android.database.MatrixCursor
import android.os.CancellationSignal
@ -43,7 +44,7 @@ class VolumeProvider: DocumentsProvider() {
var usfExpose = false
var usfSafWrite = false
fun notifyRootsChanged(context: Context) {
fun notifyRootChanged(context: Context) {
context.contentResolver.notifyChange(DocumentsContract.buildRootsUri(AUTHORITY), null)
}
}
@ -65,6 +66,7 @@ class VolumeProvider: DocumentsProvider() {
override fun queryRoots(projection: Array<out String>?): Cursor {
val cursor = MatrixCursor(projection ?: DEFAULT_ROOT_PROJECTION)
if (!usfExpose) return cursor
val previousVolumeIds = volumes.keys.toSet()
volumes.clear()
for (volume in volumeManager.listVolumes()) {
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_ICON, R.drawable.icon_document_provider)
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
}
@ -177,32 +188,6 @@ class VolumeProvider: DocumentsProvider() {
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(
documentId: String,
mode: String,
@ -210,11 +195,20 @@ class VolumeProvider: DocumentsProvider() {
): ParcelFileDescriptor? {
if (!usfExpose) return null
val document = parseDocumentId(documentId) ?: return null
val lazyExportedFile = LazyExportedFile(encryptedFileProvider, document.encryptedVolume, document.path)
val size = document.encryptedVolume.getAttr(document.path)?.size ?: run {
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(
lazyExportedFile,
exportedFile,
mode,
document.encryptedVolume,
volumeManager.getCoroutineScope(document.volumeId),
@ -243,7 +237,7 @@ class VolumeProvider: DocumentsProvider() {
null
} else {
document.encryptedVolume.closeFile(f)
document.rootId+newFile
document.rootId+"/"+newFile
}
}

View File

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

View File

@ -1,19 +1,15 @@
package sushi.hardcore.droidfs.file_viewers
import android.net.Uri
import androidx.media3.common.C
import androidx.annotation.OptIn
import androidx.media3.common.util.UnstableApi
import androidx.media3.datasource.DataSource
import androidx.media3.datasource.DataSpec
import androidx.media3.datasource.TransferListener
import com.google.android.exoplayer2.C
import com.google.android.exoplayer2.upstream.DataSource
import com.google.android.exoplayer2.upstream.DataSpec
import com.google.android.exoplayer2.upstream.TransferListener
import sushi.hardcore.droidfs.Constants
import sushi.hardcore.droidfs.filesystems.EncryptedVolume
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 fileOffset: Long = 0
private var bytesRemaining: Long = -1

View File

@ -1,22 +1,16 @@
package sushi.hardcore.droidfs.file_viewers
import android.view.WindowManager
import androidx.annotation.OptIn
import androidx.media3.common.MediaItem
import androidx.media3.common.PlaybackException
import androidx.media3.common.Player
import androidx.media3.common.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 com.google.android.exoplayer2.*
import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory
import com.google.android.exoplayer2.source.MediaSource
import com.google.android.exoplayer2.source.ProgressiveMediaSource
import com.google.android.exoplayer2.video.VideoSize
import sushi.hardcore.droidfs.Constants
import sushi.hardcore.droidfs.R
import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder
import java.io.File
@OptIn(UnstableApi::class)
abstract class MediaPlayer: FileViewerActivity() {
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.res.Configuration
import android.view.View
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.ui.PlayerView
import com.google.android.exoplayer2.ExoPlayer
import com.google.android.exoplayer2.ui.StyledPlayerView
import sushi.hardcore.droidfs.databinding.ActivityVideoPlayerBinding
class VideoPlayer: MediaPlayer() {
@ -19,7 +19,7 @@ class VideoPlayer: MediaPlayer() {
setContentView(binding.root)
applyNavigationBarMargin(binding.root)
binding.videoPlayer.doubleTapOverlay = binding.doubleTapOverlay
binding.videoPlayer.setControllerVisibilityListener(PlayerView.ControllerVisibilityListener { visibility ->
binding.videoPlayer.setControllerVisibilityListener(StyledPlayerView.ControllerVisibilityListener { visibility ->
binding.topBar.visibility = visibility
if (visibility == View.VISIBLE) {
showPartialSystemUi()

View File

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

View File

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

View File

@ -17,14 +17,14 @@ import android.widget.LinearLayout
import androidx.core.view.GestureDetectorCompat
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
import androidx.media3.ui.PlayerView
import com.google.android.exoplayer2.ui.StyledPlayerView
import sushi.hardcore.droidfs.R
class DoubleTapPlayerView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : PlayerView(context, attrs, defStyleAttr) {
) : StyledPlayerView(context, attrs, defStyleAttr) {
companion object {
const val SEEK_SECONDS = 10

View File

@ -15,11 +15,11 @@
android:textSize="@dimen/title_text_size"
android:padding="10dp"/>
<androidx.media3.ui.PlayerControlView
<com.google.android.exoplayer2.ui.StyledPlayerControlView
android:id="@+id/audio_controller"
android:layout_width="match_parent"
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_shuffle_button="true"
app:repeat_toggle_modes="all|one"/>

View File

@ -26,20 +26,20 @@
android:layout_gravity="center_horizontal"
android:background="@android:color/transparent"
android:gravity="center"
android:padding="@dimen/exo_styled_controls_padding"
android:clipToPadding="false"
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"
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"
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"
style="@style/ExoStyledControls.Button.Center.Next"/>

View File

@ -8,19 +8,19 @@
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"
android:layoutDirection="ltr">
android:clipToPadding="false">
<ImageButton android:id="@id/exo_prev"
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"
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"
style="@style/ExoStyledControls.Button.Center.Next"/>

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

@ -255,21 +255,5 @@
<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="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="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="password_fallback_summary">Solicitar la contraseña cuando se cancela la autenticación por huella dactilar</string>
</resources>

View File

@ -231,8 +231,7 @@
<string name="gocryptfs_disabled">Поддержка Gocryptfs отключена</string>
<string name="cryfs_disabled">Поддержка CryFS отключена</string>
<string name="file_op_delete_msg">Удаление файлов…</string>
<string name="volume_type_read_only">(%s, только чтение)</string>
<string name="volume_type_inaccessible">(%s, недоступен)</string>
<string name="volume_type_read_only">(%s, только для чтения)</string>
<string name="io_error">Ошибка ввода/вывода.</string>
<string name="use_fingerprint">Использовать отпечатка пальца вместо пароля</string>
<string name="remember_volume">Запомнить том</string>
@ -247,19 +246,4 @@
<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">Идентификатор файловой системы в файле конфигурации отличается сохранённого при последнем использовании тома. Это может означать, что злоумышленник подменил файловую систему.</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>
</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">通过递归映射文件夹计算大小(如果映射的文件夹很大则应当关闭该选项)</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 是隐藏的,你希望仅仅移除对该卷的记录还是完全擦除其内容?</string>
<string name="forget_only">仅删除记录</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">允许其他应用软件通过文档提供接口访问已经打开的卷</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

@ -1,11 +1,11 @@
buildscript {
ext.kotlin_version = '1.9.0'
ext.kotlin_version = '1.8.21'
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:8.1.1'
classpath 'com.android.tools.build:gradle:7.4.2'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
@ -17,6 +17,6 @@ allprojects {
}
}
tasks.register('clean', Delete) {
delete layout.buildDirectory
task clean(type: Delete) {
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.
# The setting is particularly useful for tweaking memory settings.
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
# Android operating system, and which are packaged with your app"s APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
@ -14,5 +18,4 @@ android.useAndroidX=true
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official
android.native.buildOutput=verbose
android.nonTransitiveRClass=false
android.native.buildOutput=verbose

Binary file not shown.

View File

@ -1,7 +1,7 @@
#Wed Feb 01 20:48:39 UTC 2023
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionSha256Sum=591855b517fc635b9e04de1d05d5e76ada3f89f5fc76f87978d1b245b4f69225
distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip
networkTimeout=10000
zipStoreBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip
distributionSha256Sum=7ba68c54029790ab444b39d7e293d3236b2632631fb5f2e012bb28b4ff669e4b
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");
# 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.
#
# 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/.
#
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# 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"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
MAX_FD="maximum"
warn () {
echo "$*"
} >&2
}
die () {
echo
echo "$*"
echo
exit 1
} >&2
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
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 [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD=$JAVA_HOME/bin/java
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
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."
fi
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.
Please set the JAVA_HOME variable in your environment to match the
@ -140,105 +106,80 @@ location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=$( cygpath --unix "$JAVACMD" )
JAVACMD=`cygpath --unix "$JAVACMD"`
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -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
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
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
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=`save "$@"`
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-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' ' '
)" '"$@"'
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
exec "$JAVACMD" "$@"

15
gradlew.bat vendored
View File

@ -14,7 +14,7 @@
@rem limitations under the License.
@rem
@if "%DEBUG%"=="" @echo off
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@ -25,8 +25,7 @@
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@ -41,7 +40,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
if "%ERRORLEVEL%" == "0" goto execute
echo.
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
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal

@ -1 +1 @@
Subproject commit 22965932759f232328810eadf3f02671b5c6ff99
Subproject commit c74b374ec49a1f47b9879b8fbc7b72b046ef55fd