Compare commits

..

No commits in common. "40bed2db21dd3b5031546fbc8c3885d9bd00db22" and "88bd746359eeca16ca83a7adc1efe495d52d3f7b" have entirely different histories.

6 changed files with 69 additions and 96 deletions

View File

@ -9,8 +9,6 @@ import android.system.Os
import android.util.Log import android.util.Log
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import sushi.hardcore.droidfs.filesystems.EncryptedVolume import sushi.hardcore.droidfs.filesystems.EncryptedVolume
import sushi.hardcore.droidfs.util.Compat import sushi.hardcore.droidfs.util.Compat
@ -88,9 +86,7 @@ class EncryptedFileProvider(context: Context) {
} }
override fun free() { override fun free() {
GlobalScope.launch(Dispatchers.IO) { Wiper.wipe(file)
Wiper.wipe(file)
}
} }
} }
@ -164,10 +160,8 @@ class EncryptedFileProvider(context: Context) {
exportedFile: ExportedFile, exportedFile: ExportedFile,
encryptedVolume: EncryptedVolume, encryptedVolume: EncryptedVolume,
): Boolean { ): Boolean {
val pfd = exportedFile.open(ParcelFileDescriptor.MODE_WRITE_ONLY, false) val fd = exportedFile.open(ParcelFileDescriptor.MODE_WRITE_ONLY, false).fileDescriptor
return encryptedVolume.exportFile(exportedFile.path, FileOutputStream(pfd.fileDescriptor)).also { return encryptedVolume.exportFile(exportedFile.path, FileOutputStream(fd))
pfd.close()
}
} }
enum class Error { enum class Error {

View File

@ -104,9 +104,7 @@ class FileOperationService : Service() {
activity.lifecycle.addObserver(object : DefaultLifecycleObserver { activity.lifecycle.addObserver(object : DefaultLifecycleObserver {
override fun onDestroy(owner: LifecycleOwner) { override fun onDestroy(owner: LifecycleOwner) {
activity.unbindService(serviceConnection) activity.unbindService(serviceConnection)
// Could have been more efficient with a LinkedHashMap but the JDK implementation doesn't allow service.notificationPermissionHelpers.removeLast()
// to access the latest element in O(1) unless using reflection
service.notificationPermissionHelpers.removeAll { it.activity == activity }
} }
}) })
activity.bindService( activity.bindService(

View File

@ -14,8 +14,6 @@ import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import sushi.hardcore.droidfs.BaseActivity import sushi.hardcore.droidfs.BaseActivity
import sushi.hardcore.droidfs.FileTypes import sushi.hardcore.droidfs.FileTypes
@ -23,6 +21,7 @@ import sushi.hardcore.droidfs.R
import sushi.hardcore.droidfs.VolumeManagerApp import sushi.hardcore.droidfs.VolumeManagerApp
import sushi.hardcore.droidfs.explorers.ExplorerElement import sushi.hardcore.droidfs.explorers.ExplorerElement
import sushi.hardcore.droidfs.filesystems.EncryptedVolume import sushi.hardcore.droidfs.filesystems.EncryptedVolume
import sushi.hardcore.droidfs.util.IntentUtils
import sushi.hardcore.droidfs.util.PathUtils import sushi.hardcore.droidfs.util.PathUtils
import sushi.hardcore.droidfs.util.finishOnClose import sushi.hardcore.droidfs.util.finishOnClose
import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder
@ -33,8 +32,9 @@ abstract class FileViewerActivity: BaseActivity() {
private lateinit var originalParentPath: String private lateinit var originalParentPath: String
private lateinit var windowInsetsController: WindowInsetsControllerCompat private lateinit var windowInsetsController: WindowInsetsControllerCompat
private var windowTypeMask = 0 private var windowTypeMask = 0
protected val playlist = mutableListOf<ExplorerElement>() private var foldersFirst = true
private val playlistMutex = Mutex() private var wasMapped = false
protected val mappedPlaylist = mutableListOf<ExplorerElement>()
protected var currentPlaylistIndex = -1 protected var currentPlaylistIndex = -1
private val isLegacyFullscreen = Build.VERSION.SDK_INT <= Build.VERSION_CODES.R private val isLegacyFullscreen = Build.VERSION.SDK_INT <= Build.VERSION_CODES.R
@ -46,6 +46,7 @@ abstract class FileViewerActivity: BaseActivity() {
intent.getIntExtra("volumeId", -1) intent.getIntExtra("volumeId", -1)
)!! )!!
finishOnClose(encryptedVolume) finishOnClose(encryptedVolume)
foldersFirst = sharedPrefs.getBoolean("folders_first", true)
windowInsetsController = WindowInsetsControllerCompat(window, window.decorView) windowInsetsController = WindowInsetsControllerCompat(window, window.decorView)
windowInsetsController.addOnControllableInsetsChangedListener { _, typeMask -> windowInsetsController.addOnControllableInsetsChangedListener { _, typeMask ->
windowTypeMask = typeMask windowTypeMask = typeMask
@ -130,57 +131,48 @@ abstract class FileViewerActivity: BaseActivity() {
} }
} }
protected suspend fun createPlaylist() { protected fun createPlaylist() {
playlistMutex.withLock { if (!wasMapped){
if (currentPlaylistIndex != -1) { encryptedVolume.recursiveMapFiles(originalParentPath)?.let { elements ->
// playlist already initialized for (e in elements) {
return if (e.isRegularFile) {
} if (FileTypes.isExtensionType(getFileType(), e.name) || filePath == e.fullPath) {
withContext(Dispatchers.IO) { mappedPlaylist.add(e)
if (sharedPrefs.getBoolean("map_folders", true)) { }
encryptedVolume.recursiveMapFiles(originalParentPath) }
} else {
encryptedVolume.readDir(originalParentPath)
}?.filterTo(playlist) { e ->
e.isRegularFile && (FileTypes.isExtensionType(getFileType(), e.name) || filePath == e.fullPath)
} }
val sortOrder = intent.getStringExtra("sortOrder") ?: "name"
val foldersFirst = sharedPrefs.getBoolean("folders_first", true)
ExplorerElement.sortBy(sortOrder, foldersFirst, playlist)
currentPlaylistIndex = playlist.indexOfFirst { it.fullPath == filePath }
} }
val sortOrder = intent.getStringExtra("sortOrder") ?: "name"
ExplorerElement.sortBy(sortOrder, foldersFirst, mappedPlaylist)
//find current index
for ((i, e) in mappedPlaylist.withIndex()){
if (filePath == e.fullPath){
currentPlaylistIndex = i
break
}
}
wasMapped = true
} }
} }
private fun updateCurrentItem() { protected fun playlistNext(forward: Boolean) {
filePath = playlist[currentPlaylistIndex].fullPath
}
protected suspend fun playlistNext(forward: Boolean) {
createPlaylist() createPlaylist()
currentPlaylistIndex = if (forward) { currentPlaylistIndex = if (forward) {
(currentPlaylistIndex + 1).mod(playlist.size) (currentPlaylistIndex+1)%mappedPlaylist.size
} else { } else {
(currentPlaylistIndex - 1).mod(playlist.size) var x = (currentPlaylistIndex-1)%mappedPlaylist.size
if (x < 0) {
x += mappedPlaylist.size
}
x
} }
updateCurrentItem() filePath = mappedPlaylist[currentPlaylistIndex].fullPath
} }
protected suspend fun deleteCurrentFile(): Boolean { protected fun refreshPlaylist() {
createPlaylist() // ensure we know the current position in the playlist mappedPlaylist.clear()
return if (encryptedVolume.deleteFile(filePath)) { wasMapped = false
playlist.removeAt(currentPlaylistIndex) createPlaylist()
if (playlist.size != 0) {
if (currentPlaylistIndex == playlist.size) {
// deleted the last element of the playlist, go back to the first
currentPlaylistIndex = 0
}
updateCurrentItem()
}
true
} else {
false
}
} }
protected fun goBackToExplorer() { protected fun goBackToExplorer() {

View File

@ -12,12 +12,10 @@ import android.widget.Toast
import androidx.activity.addCallback import androidx.activity.addCallback
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.lifecycleScope
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.bumptech.glide.RequestBuilder import com.bumptech.glide.RequestBuilder
import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation import com.bumptech.glide.load.resource.bitmap.BitmapTransformation
import kotlinx.coroutines.launch
import sushi.hardcore.droidfs.Constants import sushi.hardcore.droidfs.Constants
import sushi.hardcore.droidfs.R import sushi.hardcore.droidfs.R
import sushi.hardcore.droidfs.databinding.ActivityImageViewerBinding import sushi.hardcore.droidfs.databinding.ActivityImageViewerBinding
@ -107,21 +105,22 @@ class ImageViewer: FileViewerActivity() {
.keepFullScreen() .keepFullScreen()
.setTitle(R.string.warning) .setTitle(R.string.warning)
.setPositiveButton(R.string.ok) { _, _ -> .setPositiveButton(R.string.ok) { _, _ ->
lifecycleScope.launch { createPlaylist() //be sure the playlist is created before deleting if there is only one image
if (deleteCurrentFile()) { if (encryptedVolume.deleteFile(filePath)) {
if (playlist.size == 0) { // no more image left playlistNext(true)
goBackToExplorer() refreshPlaylist()
} else { if (mappedPlaylist.size == 0) { //deleted all images of the playlist
loadImage(true) goBackToExplorer()
}
} else { } else {
CustomAlertDialogBuilder(this@ImageViewer, theme) loadImage(true)
.keepFullScreen()
.setTitle(R.string.error)
.setMessage(getString(R.string.remove_failed, fileName))
.setPositiveButton(R.string.ok, null)
.show()
} }
} else {
CustomAlertDialogBuilder(this, theme)
.keepFullScreen()
.setTitle(R.string.error)
.setMessage(getString(R.string.remove_failed, fileName))
.setPositiveButton(R.string.ok, null)
.show()
} }
} }
.setNegativeButton(R.string.cancel, null) .setNegativeButton(R.string.cancel, null)
@ -199,16 +198,14 @@ class ImageViewer: FileViewerActivity() {
rotateImage() rotateImage()
} }
private fun swipeImage(deltaX: Float, slideshowSwipe: Boolean = false) { private fun swipeImage(deltaX: Float, slideshowSwipe: Boolean = false){
lifecycleScope.launch { playlistNext(deltaX < 0)
playlistNext(deltaX < 0) loadImage(true)
loadImage(true) if (slideshowActive) {
if (slideshowActive) { if (!slideshowSwipe) { //reset slideshow delay if user swipes
if (!slideshowSwipe) { // reset slideshow delay if user swipes handler.removeCallbacks(slideshowNext)
handler.removeCallbacks(slideshowNext)
}
handler.postDelayed(slideshowNext, Constants.SLIDESHOW_DELAY)
} }
handler.postDelayed(slideshowNext, Constants.SLIDESHOW_DELAY)
} }
} }

View File

@ -2,7 +2,6 @@ package sushi.hardcore.droidfs.file_viewers
import android.view.WindowManager import android.view.WindowManager
import androidx.annotation.OptIn import androidx.annotation.OptIn
import androidx.lifecycle.lifecycleScope
import androidx.media3.common.MediaItem import androidx.media3.common.MediaItem
import androidx.media3.common.PlaybackException import androidx.media3.common.PlaybackException
import androidx.media3.common.Player import androidx.media3.common.Player
@ -12,7 +11,6 @@ import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.source.MediaSource import androidx.media3.exoplayer.source.MediaSource
import androidx.media3.exoplayer.source.ProgressiveMediaSource import androidx.media3.exoplayer.source.ProgressiveMediaSource
import androidx.media3.extractor.DefaultExtractorsFactory import androidx.media3.extractor.DefaultExtractorsFactory
import kotlinx.coroutines.launch
import sushi.hardcore.droidfs.Constants import sushi.hardcore.droidfs.Constants
import sushi.hardcore.droidfs.R import sushi.hardcore.droidfs.R
import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder
@ -41,16 +39,12 @@ abstract class MediaPlayer: FileViewerActivity() {
private fun initializePlayer(){ private fun initializePlayer(){
player = ExoPlayer.Builder(this).setSeekForwardIncrementMs(5000).build() player = ExoPlayer.Builder(this).setSeekForwardIncrementMs(5000).build()
bindPlayer(player) bindPlayer(player)
player.addMediaSource(createMediaSource(filePath)) createPlaylist()
lifecycleScope.launch { for (e in mappedPlaylist) {
createPlaylist() player.addMediaSource(createMediaSource(e.fullPath))
playlist.forEachIndexed { index, e ->
if (index != currentPlaylistIndex) {
player.addMediaSource(index, createMediaSource(e.fullPath))
}
}
} }
player.repeatMode = Player.REPEAT_MODE_ALL player.repeatMode = Player.REPEAT_MODE_ALL
player.seekToDefaultPosition(currentPlaylistIndex)
player.playWhenReady = true player.playWhenReady = true
player.addListener(object : Player.Listener{ player.addListener(object : Player.Listener{
override fun onVideoSizeChanged(videoSize: VideoSize) { override fun onVideoSizeChanged(videoSize: VideoSize) {
@ -73,11 +67,9 @@ abstract class MediaPlayer: FileViewerActivity() {
} }
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) { override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
if (player.repeatMode != Player.REPEAT_MODE_ONE && currentPlaylistIndex != -1) { if (player.repeatMode != Player.REPEAT_MODE_ONE) {
lifecycleScope.launch { playlistNext(player.currentMediaItemIndex == (currentPlaylistIndex + 1) % mappedPlaylist.size)
playlistNext(player.currentMediaItemIndex == (currentPlaylistIndex + 1) % player.mediaItemCount) refreshFileName()
refreshFileName()
}
} }
} }
}) })

View File

@ -28,7 +28,7 @@ object AndroidUtils {
/** /**
* A [Manifest.permission.POST_NOTIFICATIONS] permission helper. * A [Manifest.permission.POST_NOTIFICATIONS] permission helper.
* *
* Must be initialized before [Activity.onCreate] finishes. * Must be initialized before [Activity.onCreate].
*/ */
class NotificationPermissionHelper<out A: AppCompatActivity>(val activity: A) { class NotificationPermissionHelper<out A: AppCompatActivity>(val activity: A) {
private var listener: ((Boolean) -> Unit)? = null private var listener: ((Boolean) -> Unit)? = null