forked from hardcoresushi/DroidFS
TODO.md & Update dependencies
This commit is contained in:
parent
2bbf003df5
commit
e51bd2ceba
26
TODO.md
Normal file
26
TODO.md
Normal file
@ -0,0 +1,26 @@
|
||||
# TODO
|
||||
|
||||
Here is a list of features that it would be nice to have in DroidFS.
|
||||
|
||||
## Security
|
||||
- [hardened_malloc](https://github.com/GrapheneOS/hardened_malloc) compatibility ([#181](https://github.com/hardcore-sushi/DroidFS/issues/181))
|
||||
- Internal keyboard for passwords
|
||||
|
||||
## UX
|
||||
- File associations editor
|
||||
- Modifiable CryFS scrypt parameters
|
||||
- Alert dialog showing details of file operations
|
||||
- Internal file browser to select volumes
|
||||
|
||||
## Health
|
||||
- F-Droid ABI split
|
||||
- OpenSSL & FFmpeg as git submodules (useful for F-Droid)
|
||||
- Remove all android:configChanges from AndroidManifest.xml
|
||||
- More efficient thumbnails cache
|
||||
- Guide for translators
|
||||
- Usage & code documentation
|
||||
- Automated tests
|
||||
|
||||
## And:
|
||||
- All the [feature requests on the GitHub repo](https://github.com/hardcore-sushi/DroidFS/issues?q=is%3Aissue+is%3Aopen+label%3Aenhancement)
|
||||
- All the [feature requests on the Gitea repo](https://forge.chapril.org/hardcoresushi/DroidFS/issues?q=&state=open&labels=748)
|
@ -110,16 +110,16 @@ dependencies {
|
||||
implementation "androidx.preference:preference-ktx:1.2.0"
|
||||
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
|
||||
implementation 'com.google.android.material:material:1.8.0'
|
||||
implementation "com.github.bumptech.glide:glide:4.13.2"
|
||||
implementation 'com.github.bumptech.glide:glide:4.15.1'
|
||||
implementation "androidx.biometric:biometric-ktx:1.2.0-alpha05"
|
||||
|
||||
def exoplayer_version = "2.18.5"
|
||||
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-alpha05"
|
||||
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"
|
||||
|
@ -223,9 +223,9 @@ public final class SucklessRecorder implements VideoOutput {
|
||||
*/
|
||||
DISABLED,
|
||||
/**
|
||||
* The recording is being recorded with audio.
|
||||
* Audio recording is enabled for the running recording.
|
||||
*/
|
||||
ACTIVE,
|
||||
ENABLED,
|
||||
/**
|
||||
* The audio encoder encountered errors.
|
||||
*/
|
||||
@ -817,6 +817,24 @@ public final class SucklessRecorder implements VideoOutput {
|
||||
}
|
||||
}
|
||||
|
||||
void mute(@NonNull SucklessRecording activeRecording, boolean muted) {
|
||||
synchronized (mLock) {
|
||||
if (!isSameRecording(activeRecording, mPendingRecordingRecord) && !isSameRecording(
|
||||
activeRecording, mActiveRecordingRecord)) {
|
||||
// If this Recording is no longer active, log and treat as a no-op.
|
||||
// This is not technically an error since the recording can be finalized
|
||||
// asynchronously.
|
||||
Logger.d(TAG,
|
||||
"mute() called on a recording that is no longer active: "
|
||||
+ activeRecording.getOutputOptions());
|
||||
return;
|
||||
}
|
||||
RecordingRecord finalRecordingRecord = isSameRecording(activeRecording,
|
||||
mPendingRecordingRecord) ? mPendingRecordingRecord : mActiveRecordingRecord;
|
||||
mSequentialExecutor.execute(() -> muteInternal(finalRecordingRecord, muted));
|
||||
}
|
||||
}
|
||||
|
||||
private void finalizePendingRecording(@NonNull RecordingRecord recordingToFinalize,
|
||||
@VideoRecordError int error, @Nullable Throwable cause) {
|
||||
recordingToFinalize.finalizeRecording(Uri.EMPTY);
|
||||
@ -949,8 +967,8 @@ public final class SucklessRecorder implements VideoOutput {
|
||||
(transformationInfo) -> mSurfaceTransformationInfo = transformationInfo);
|
||||
Size surfaceSize = surfaceRequest.getResolution();
|
||||
// Fetch and cache nearest encoder profiles, if one exists.
|
||||
VideoCapabilities capabilities =
|
||||
VideoCapabilities.from(surfaceRequest.getCamera().getCameraInfo());
|
||||
LegacyVideoCapabilities capabilities =
|
||||
LegacyVideoCapabilities.from(surfaceRequest.getCamera().getCameraInfo());
|
||||
Quality highestSupportedQuality = capabilities.findHighestSupportedQualityFor(surfaceSize);
|
||||
Logger.d(TAG, "Using supported quality of " + highestSupportedQuality
|
||||
+ " for surface size " + surfaceSize);
|
||||
@ -1368,13 +1386,13 @@ public final class SucklessRecorder implements VideoOutput {
|
||||
// Fall-through
|
||||
case ERROR_SOURCE:
|
||||
// Fall-through
|
||||
case ACTIVE:
|
||||
case ENABLED:
|
||||
// Fall-through
|
||||
case DISABLED:
|
||||
throw new AssertionError(
|
||||
"Incorrectly invoke startInternal in audio state " + mAudioState);
|
||||
case IDLING:
|
||||
setAudioState(recordingToStart.hasAudioEnabled() ? AudioState.ACTIVE
|
||||
setAudioState(recordingToStart.hasAudioEnabled() ? AudioState.ENABLED
|
||||
: AudioState.DISABLED);
|
||||
break;
|
||||
case INITIALIZING:
|
||||
@ -1385,7 +1403,7 @@ public final class SucklessRecorder implements VideoOutput {
|
||||
}
|
||||
try {
|
||||
setupAudio(recordingToStart);
|
||||
setAudioState(AudioState.ACTIVE);
|
||||
setAudioState(AudioState.ENABLED);
|
||||
} catch (AudioSourceAccessException | InvalidConfigException e) {
|
||||
Logger.e(TAG, "Unable to create audio resource with error: ", e);
|
||||
AudioState audioState;
|
||||
@ -1403,7 +1421,7 @@ public final class SucklessRecorder implements VideoOutput {
|
||||
|
||||
initEncoderAndAudioSourceCallbacks(recordingToStart);
|
||||
if (isAudioEnabled()) {
|
||||
mAudioSource.start();
|
||||
mAudioSource.start(recordingToStart.isMuted());
|
||||
mAudioEncoder.start();
|
||||
}
|
||||
mVideoEncoder.start();
|
||||
@ -1535,8 +1553,6 @@ public final class SucklessRecorder implements VideoOutput {
|
||||
public void onSilenceStateChanged(boolean silenced) {
|
||||
if (mIsAudioSourceSilenced != silenced) {
|
||||
mIsAudioSourceSilenced = silenced;
|
||||
mAudioErrorCause = silenced ? new IllegalStateException(
|
||||
"The audio source has been silenced.") : null;
|
||||
updateInProgressStatusEvent();
|
||||
} else {
|
||||
Logger.w(TAG, "Audio source silenced transitions"
|
||||
@ -1579,9 +1595,9 @@ public final class SucklessRecorder implements VideoOutput {
|
||||
@Override
|
||||
public void onEncodedData(@NonNull EncodedData encodedData) {
|
||||
if (mAudioState == AudioState.DISABLED) {
|
||||
throw new AssertionError(
|
||||
"Audio is not enabled but audio encoded data is "
|
||||
+ "produced.");
|
||||
encodedData.close();
|
||||
throw new AssertionError("Audio is not enabled but audio "
|
||||
+ "encoded data is being produced.");
|
||||
}
|
||||
|
||||
// If the media muxer doesn't yet exist, we may need to create and
|
||||
@ -1847,6 +1863,19 @@ public final class SucklessRecorder implements VideoOutput {
|
||||
}
|
||||
}
|
||||
|
||||
@ExecutedBy("mSequentialExecutor")
|
||||
private void muteInternal(@NonNull RecordingRecord recordingToMute, boolean muted) {
|
||||
if (recordingToMute.isMuted() == muted) {
|
||||
return;
|
||||
}
|
||||
recordingToMute.mute(muted);
|
||||
// Only mute/unmute audio source if recording is in-progress and it is not already stopping.
|
||||
if (mInProgressRecording == recordingToMute && !mInProgressRecordingStopping
|
||||
&& mAudioSource != null) {
|
||||
mAudioSource.mute(muted);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
|
||||
static void notifyEncoderSourceStopped(@NonNull Encoder encoder) {
|
||||
if (encoder instanceof SucklessEncoderImpl) {
|
||||
@ -1941,8 +1970,10 @@ public final class SucklessRecorder implements VideoOutput {
|
||||
// Audio will not be initialized until the first recording with audio enabled is
|
||||
// started. So if the audio state is INITIALIZING, consider the audio is disabled.
|
||||
return AudioStats.AUDIO_STATE_DISABLED;
|
||||
case ACTIVE:
|
||||
if (mIsAudioSourceSilenced) {
|
||||
case ENABLED:
|
||||
if (mInProgressRecording != null && mInProgressRecording.isMuted()) {
|
||||
return AudioStats.AUDIO_STATE_MUTED;
|
||||
} else if (mIsAudioSourceSilenced) {
|
||||
return AudioStats.AUDIO_STATE_SOURCE_SILENCED;
|
||||
} else {
|
||||
return AudioStats.AUDIO_STATE_ACTIVE;
|
||||
@ -1971,7 +2002,7 @@ public final class SucklessRecorder implements VideoOutput {
|
||||
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
|
||||
@ExecutedBy("mSequentialExecutor")
|
||||
boolean isAudioEnabled() {
|
||||
return mAudioState == AudioState.ACTIVE;
|
||||
return mAudioState == AudioState.ENABLED;
|
||||
}
|
||||
|
||||
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
|
||||
@ -2043,7 +2074,7 @@ public final class SucklessRecorder implements VideoOutput {
|
||||
break;
|
||||
case DISABLED:
|
||||
// Fall-through
|
||||
case ACTIVE:
|
||||
case ENABLED:
|
||||
setAudioState(AudioState.IDLING);
|
||||
mAudioSource.stop();
|
||||
break;
|
||||
@ -2497,6 +2528,8 @@ public final class SucklessRecorder implements VideoOutput {
|
||||
/* no-op by default */
|
||||
});
|
||||
|
||||
private final AtomicBoolean mMuted = new AtomicBoolean(false);
|
||||
|
||||
@NonNull
|
||||
static RecordingRecord from(@NonNull SucklessPendingRecording pendingRecording, long recordingId) {
|
||||
return new AutoValue_SucklessRecorder_RecordingRecord(
|
||||
@ -2769,6 +2802,14 @@ public final class SucklessRecorder implements VideoOutput {
|
||||
finalizeRecordingInternal(mRecordingFinalizer.getAndSet(null), uri);
|
||||
}
|
||||
|
||||
void mute(boolean muted) {
|
||||
mMuted.set(muted);
|
||||
}
|
||||
|
||||
boolean isMuted() {
|
||||
return mMuted.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Close this recording, as if calling {@link #finalizeRecording(Uri)} with parameter
|
||||
* {@link Uri#EMPTY}.
|
||||
|
@ -159,6 +159,25 @@ public final class SucklessRecording implements AutoCloseable {
|
||||
close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Mutes or un-mutes the current recording.
|
||||
*
|
||||
* <p>The output file will contain an audio track even the whole recording is muted. Create a
|
||||
* recording without calling {@link PendingRecording#withAudioEnabled()} to record a file
|
||||
* with no audio track.
|
||||
*
|
||||
* <p>Muting or unmuting a recording that isn't created
|
||||
* {@link PendingRecording#withAudioEnabled()} with audio enabled is no-op.
|
||||
*
|
||||
* @param muted mutes the recording if {@code true}, un-mutes otherwise.
|
||||
*/
|
||||
public void mute(boolean muted) {
|
||||
if (mIsClosed.get()) {
|
||||
throw new IllegalStateException("The recording has been stopped.");
|
||||
}
|
||||
mRecorder.mute(this, muted);
|
||||
}
|
||||
|
||||
/**
|
||||
* Close this recording.
|
||||
*
|
||||
|
@ -1675,4 +1675,3 @@ public class SucklessEncoderImpl implements Encoder {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -1683,4 +1683,3 @@ public class EncoderImpl implements Encoder {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -226,9 +226,9 @@ public final class Recorder implements VideoOutput {
|
||||
*/
|
||||
DISABLED,
|
||||
/**
|
||||
* The recording is being recorded with audio.
|
||||
* Audio recording is enabled for the running recording.
|
||||
*/
|
||||
ACTIVE,
|
||||
ENABLED,
|
||||
/**
|
||||
* The audio encoder encountered errors.
|
||||
*/
|
||||
@ -907,6 +907,24 @@ public final class Recorder implements VideoOutput {
|
||||
}
|
||||
}
|
||||
|
||||
void mute(@NonNull Recording activeRecording, boolean muted) {
|
||||
synchronized (mLock) {
|
||||
if (!isSameRecording(activeRecording, mPendingRecordingRecord) && !isSameRecording(
|
||||
activeRecording, mActiveRecordingRecord)) {
|
||||
// If this Recording is no longer active, log and treat as a no-op.
|
||||
// This is not technically an error since the recording can be finalized
|
||||
// asynchronously.
|
||||
Logger.d(TAG,
|
||||
"mute() called on a recording that is no longer active: "
|
||||
+ activeRecording.getOutputOptions());
|
||||
return;
|
||||
}
|
||||
RecordingRecord finalRecordingRecord = isSameRecording(activeRecording,
|
||||
mPendingRecordingRecord) ? mPendingRecordingRecord : mActiveRecordingRecord;
|
||||
mSequentialExecutor.execute(() -> muteInternal(finalRecordingRecord, muted));
|
||||
}
|
||||
}
|
||||
|
||||
private void finalizePendingRecording(@NonNull RecordingRecord recordingToFinalize,
|
||||
@VideoRecordError int error, @Nullable Throwable cause) {
|
||||
recordingToFinalize.finalizeRecording(Uri.EMPTY);
|
||||
@ -1039,8 +1057,8 @@ public final class Recorder implements VideoOutput {
|
||||
(transformationInfo) -> mSurfaceTransformationInfo = transformationInfo);
|
||||
Size surfaceSize = surfaceRequest.getResolution();
|
||||
// Fetch and cache nearest encoder profiles, if one exists.
|
||||
VideoCapabilities capabilities =
|
||||
VideoCapabilities.from(surfaceRequest.getCamera().getCameraInfo());
|
||||
LegacyVideoCapabilities capabilities =
|
||||
LegacyVideoCapabilities.from(surfaceRequest.getCamera().getCameraInfo());
|
||||
Quality highestSupportedQuality = capabilities.findHighestSupportedQualityFor(surfaceSize);
|
||||
Logger.d(TAG, "Using supported quality of " + highestSupportedQuality
|
||||
+ " for surface size " + surfaceSize);
|
||||
@ -1473,13 +1491,13 @@ public final class Recorder implements VideoOutput {
|
||||
// Fall-through
|
||||
case ERROR_SOURCE:
|
||||
// Fall-through
|
||||
case ACTIVE:
|
||||
case ENABLED:
|
||||
// Fall-through
|
||||
case DISABLED:
|
||||
throw new AssertionError(
|
||||
"Incorrectly invoke startInternal in audio state " + mAudioState);
|
||||
case IDLING:
|
||||
setAudioState(recordingToStart.hasAudioEnabled() ? AudioState.ACTIVE
|
||||
setAudioState(recordingToStart.hasAudioEnabled() ? AudioState.ENABLED
|
||||
: AudioState.DISABLED);
|
||||
break;
|
||||
case INITIALIZING:
|
||||
@ -1490,7 +1508,7 @@ public final class Recorder implements VideoOutput {
|
||||
}
|
||||
try {
|
||||
setupAudio(recordingToStart);
|
||||
setAudioState(AudioState.ACTIVE);
|
||||
setAudioState(AudioState.ENABLED);
|
||||
} catch (AudioSourceAccessException | InvalidConfigException e) {
|
||||
Logger.e(TAG, "Unable to create audio resource with error: ", e);
|
||||
AudioState audioState;
|
||||
@ -1508,7 +1526,7 @@ public final class Recorder implements VideoOutput {
|
||||
|
||||
initEncoderAndAudioSourceCallbacks(recordingToStart);
|
||||
if (isAudioEnabled()) {
|
||||
mAudioSource.start();
|
||||
mAudioSource.start(recordingToStart.isMuted());
|
||||
mAudioEncoder.start();
|
||||
}
|
||||
mVideoEncoder.start();
|
||||
@ -1640,8 +1658,6 @@ public final class Recorder implements VideoOutput {
|
||||
public void onSilenceStateChanged(boolean silenced) {
|
||||
if (mIsAudioSourceSilenced != silenced) {
|
||||
mIsAudioSourceSilenced = silenced;
|
||||
mAudioErrorCause = silenced ? new IllegalStateException(
|
||||
"The audio source has been silenced.") : null;
|
||||
updateInProgressStatusEvent();
|
||||
} else {
|
||||
Logger.w(TAG, "Audio source silenced transitions"
|
||||
@ -1684,9 +1700,9 @@ public final class Recorder implements VideoOutput {
|
||||
@Override
|
||||
public void onEncodedData(@NonNull EncodedData encodedData) {
|
||||
if (mAudioState == AudioState.DISABLED) {
|
||||
throw new AssertionError(
|
||||
"Audio is not enabled but audio encoded data is "
|
||||
+ "produced.");
|
||||
encodedData.close();
|
||||
throw new AssertionError("Audio is not enabled but audio "
|
||||
+ "encoded data is being produced.");
|
||||
}
|
||||
|
||||
// If the media muxer doesn't yet exist, we may need to create and
|
||||
@ -1952,6 +1968,19 @@ public final class Recorder implements VideoOutput {
|
||||
}
|
||||
}
|
||||
|
||||
@ExecutedBy("mSequentialExecutor")
|
||||
private void muteInternal(@NonNull RecordingRecord recordingToMute, boolean muted) {
|
||||
if (recordingToMute.isMuted() == muted) {
|
||||
return;
|
||||
}
|
||||
recordingToMute.mute(muted);
|
||||
// Only mute/unmute audio source if recording is in-progress and it is not already stopping.
|
||||
if (mInProgressRecording == recordingToMute && !mInProgressRecordingStopping
|
||||
&& mAudioSource != null) {
|
||||
mAudioSource.mute(muted);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
|
||||
static void notifyEncoderSourceStopped(@NonNull Encoder encoder) {
|
||||
if (encoder instanceof EncoderImpl) {
|
||||
@ -2046,8 +2075,10 @@ public final class Recorder implements VideoOutput {
|
||||
// Audio will not be initialized until the first recording with audio enabled is
|
||||
// started. So if the audio state is INITIALIZING, consider the audio is disabled.
|
||||
return AudioStats.AUDIO_STATE_DISABLED;
|
||||
case ACTIVE:
|
||||
if (mIsAudioSourceSilenced) {
|
||||
case ENABLED:
|
||||
if (mInProgressRecording != null && mInProgressRecording.isMuted()) {
|
||||
return AudioStats.AUDIO_STATE_MUTED;
|
||||
} else if (mIsAudioSourceSilenced) {
|
||||
return AudioStats.AUDIO_STATE_SOURCE_SILENCED;
|
||||
} else {
|
||||
return AudioStats.AUDIO_STATE_ACTIVE;
|
||||
@ -2076,7 +2107,7 @@ public final class Recorder implements VideoOutput {
|
||||
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
|
||||
@ExecutedBy("mSequentialExecutor")
|
||||
boolean isAudioEnabled() {
|
||||
return mAudioState == AudioState.ACTIVE;
|
||||
return mAudioState == AudioState.ENABLED;
|
||||
}
|
||||
|
||||
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
|
||||
@ -2148,7 +2179,7 @@ public final class Recorder implements VideoOutput {
|
||||
break;
|
||||
case DISABLED:
|
||||
// Fall-through
|
||||
case ACTIVE:
|
||||
case ENABLED:
|
||||
setAudioState(AudioState.IDLING);
|
||||
mAudioSource.stop();
|
||||
break;
|
||||
@ -2602,6 +2633,8 @@ public final class Recorder implements VideoOutput {
|
||||
/* no-op by default */
|
||||
});
|
||||
|
||||
private final AtomicBoolean mMuted = new AtomicBoolean(false);
|
||||
|
||||
@NonNull
|
||||
static RecordingRecord from(@NonNull PendingRecording pendingRecording, long recordingId) {
|
||||
return new AutoValue_Recorder_RecordingRecord(
|
||||
@ -2930,6 +2963,14 @@ public final class Recorder implements VideoOutput {
|
||||
finalizeRecordingInternal(mRecordingFinalizer.getAndSet(null), uri);
|
||||
}
|
||||
|
||||
void mute(boolean muted) {
|
||||
mMuted.set(muted);
|
||||
}
|
||||
|
||||
boolean isMuted() {
|
||||
return mMuted.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Close this recording, as if calling {@link #finalizeRecording(Uri)} with parameter
|
||||
* {@link Uri#EMPTY}.
|
||||
|
@ -156,6 +156,25 @@ public final class Recording implements AutoCloseable {
|
||||
close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Mutes or un-mutes the current recording.
|
||||
*
|
||||
* <p>The output file will contain an audio track even the whole recording is muted. Create a
|
||||
* recording without calling {@link PendingRecording#withAudioEnabled()} to record a file
|
||||
* with no audio track.
|
||||
*
|
||||
* <p>Muting or unmuting a recording that isn't created
|
||||
* {@link PendingRecording#withAudioEnabled()} with audio enabled is no-op.
|
||||
*
|
||||
* @param muted mutes the recording if {@code true}, un-mutes otherwise.
|
||||
*/
|
||||
public void mute(boolean muted) {
|
||||
if (mIsClosed.get()) {
|
||||
throw new IllegalStateException("The recording has been stopped.");
|
||||
}
|
||||
mRecorder.mute(this, muted);
|
||||
}
|
||||
|
||||
/**
|
||||
* Close this recording.
|
||||
*
|
||||
|
@ -1,5 +1,5 @@
|
||||
buildscript {
|
||||
ext.kotlin_version = "1.8.10"
|
||||
ext.kotlin_version = '1.8.21'
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
|
Loading…
Reference in New Issue
Block a user