Media playlist

This commit is contained in:
Matéo Duparc 2021-03-17 21:11:14 +01:00
parent e92804ae34
commit 86e9572431
Signed by untrusted user: hardcoresushi
GPG Key ID: 007F84120107191E
14 changed files with 160 additions and 200 deletions

View File

@ -15,8 +15,8 @@ android {
applicationId "sushi.hardcore.droidfs"
minSdkVersion 21
targetSdkVersion 29
versionCode 11
versionName "1.4.3"
versionCode 12
versionName "1.4.4"
ndk {
abiFilters 'x86_64', 'armeabi-v7a', 'arm64-v8a'
@ -54,10 +54,12 @@ dependencies {
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
implementation "com.jaredrummler:cyanea:1.0.2"
implementation "com.github.bumptech.glide:glide:4.11.0"
implementation "com.google.android.exoplayer:exoplayer-core:2.11.7"
implementation "com.google.android.exoplayer:exoplayer-ui:2.11.7"
implementation "androidx.biometric:biometric:1.1.0"
def exoplayer_version = "2.13.2"
implementation "com.google.android.exoplayer:exoplayer-core:$exoplayer_version"
implementation "com.google.android.exoplayer:exoplayer-ui:$exoplayer_version"
def camerax_version = "1.1.0-alpha01"
implementation "androidx.camera:camera-camera2:$camerax_version"
implementation "androidx.camera:camera-lifecycle:$camerax_version"

View File

@ -21,7 +21,7 @@ class ConstValues {
Pair("text", listOf("txt", "json", "conf", "log", "xml", "java", "kt", "py", "pl", "rb", "go", "c", "h", "cpp", "hpp", "sh", "bat", "js", "html", "css", "php", "yml", "yaml", "ini", "md"))
)
private fun isExtensionType(extensionType: String, path: String): Boolean {
fun isExtensionType(extensionType: String, path: String): Boolean {
return fileExtensions[extensionType]?.contains(File(path).extension.toLowerCase(Locale.ROOT)) ?: false
}

View File

@ -112,12 +112,11 @@ open class BaseExplorerActivity : BaseActivity() {
}
}
private fun startFileViewer(cls: Class<*>, filePath: String, sortOrder: String = ""){
val intent = Intent(this, cls)
intent.putExtra("path", filePath)
intent.putExtra("sessionID", gocryptfsVolume.sessionID)
if (sortOrder.isNotEmpty()){
intent.putExtra("sortOrder", sortOrder)
private fun startFileViewer(cls: Class<*>, filePath: String){
val intent = Intent(this, cls).apply {
putExtra("path", filePath)
putExtra("sessionID", gocryptfsVolume.sessionID)
putExtra("sortOrder", sortOrderValues[currentSortOrderIndex])
}
isStartingActivity = true
startActivity(intent)
@ -142,7 +141,7 @@ open class BaseExplorerActivity : BaseActivity() {
setCurrentPath(PathUtils.getParentPath(currentDirectoryPath))
}
isImage(fullPath) -> {
startFileViewer(ImageViewer::class.java, fullPath, sortOrderValues[currentSortOrderIndex])
startFileViewer(ImageViewer::class.java, fullPath)
}
isVideo(fullPath) -> {
startFileViewer(VideoPlayer::class.java, fullPath)

View File

@ -9,6 +9,22 @@ class AudioPlayer: MediaPlayer(){
override fun viewFile() {
setContentView(R.layout.activity_audio_player)
super.viewFile()
refreshFileName()
}
override fun getFileType(): String {
return "audio"
}
override fun bindPlayer(player: SimpleExoPlayer) {
audio_controller.player = player
}
override fun onPlaylistIndexChanged() {
refreshFileName()
}
private fun refreshFileName() {
val filename = File(filePath).name
val pos = filename.lastIndexOf('.')
music_title.text = if (pos != -1){
@ -17,8 +33,4 @@ class AudioPlayer: MediaPlayer(){
filename
}
}
override fun bindPlayer(player: SimpleExoPlayer) {
audio_controller.player = player
}
}

View File

@ -3,16 +3,23 @@ package sushi.hardcore.droidfs.file_viewers
import android.os.Bundle
import android.view.View
import sushi.hardcore.droidfs.BaseActivity
import sushi.hardcore.droidfs.ConstValues
import sushi.hardcore.droidfs.GocryptfsVolume
import sushi.hardcore.droidfs.R
import sushi.hardcore.droidfs.content_providers.RestrictedFileProvider
import sushi.hardcore.droidfs.GocryptfsVolume
import sushi.hardcore.droidfs.explorers.ExplorerElement
import sushi.hardcore.droidfs.util.PathUtils
import sushi.hardcore.droidfs.widgets.ColoredAlertDialogBuilder
abstract class FileViewerActivity: BaseActivity() {
lateinit var gocryptfsVolume: GocryptfsVolume
lateinit var filePath: String
protected lateinit var gocryptfsVolume: GocryptfsVolume
protected lateinit var filePath: String
private var isFinishingIntentionally = false
private var usf_keep_open = false
private var wasMapped = false
protected val mappedPlaylist = mutableListOf<ExplorerElement>()
protected var currentPlaylistIndex = -1
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
filePath = intent.getStringExtra("path")!!
@ -22,6 +29,7 @@ abstract class FileViewerActivity: BaseActivity() {
hideSystemUi()
viewFile()
}
open fun hideSystemUi(){
window.decorView.systemUiVisibility =
View.SYSTEM_UI_FLAG_FULLSCREEN/* or
@ -31,14 +39,18 @@ abstract class FileViewerActivity: BaseActivity() {
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION*/
}
abstract fun getFileType(): String
abstract fun viewFile()
override fun onUserInteraction() {
super.onUserInteraction()
if (window.decorView.systemUiVisibility and View.SYSTEM_UI_FLAG_FULLSCREEN == 0){
hideSystemUi()
}
}
fun loadWholeFile(path: String): ByteArray? {
protected fun loadWholeFile(path: String): ByteArray? {
val fileSize = gocryptfsVolume.getSize(path)
if (fileSize >= 0){
try {
@ -86,7 +98,49 @@ abstract class FileViewerActivity: BaseActivity() {
return null
}
protected fun goBackToExplorer(){
protected fun createPlaylist() {
if (!wasMapped){
for (e in gocryptfsVolume.recursiveMapFiles(PathUtils.getParentPath(filePath))){
if (e.isRegularFile) {
if (ConstValues.isExtensionType(getFileType(), e.name) || filePath == e.fullPath) {
mappedPlaylist.add(e)
}
}
}
val sortOrder = intent.getStringExtra("sortOrder") ?: "name"
ExplorerElement.sortBy(sortOrder, mappedPlaylist)
//find current index
for ((i, e) in mappedPlaylist.withIndex()){
if (filePath == e.fullPath){
currentPlaylistIndex = i
break
}
}
wasMapped = true
}
}
protected fun playlistNext(forward: Boolean) {
createPlaylist()
currentPlaylistIndex = if (forward) {
(currentPlaylistIndex+1)%mappedPlaylist.size
} else {
var x = (currentPlaylistIndex-1)%mappedPlaylist.size
if (x < 0) {
x += mappedPlaylist.size
}
x
}
filePath = mappedPlaylist[currentPlaylistIndex].fullPath
}
protected fun refreshPlaylist() {
mappedPlaylist.clear()
wasMapped = false
createPlaylist()
}
protected fun goBackToExplorer() {
isFinishingIntentionally = true
finish()
}

View File

@ -1,7 +1,9 @@
package sushi.hardcore.droidfs.file_viewers
import android.net.Uri
import com.google.android.exoplayer2.upstream.*
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.ConstValues
import sushi.hardcore.droidfs.GocryptfsVolume
import kotlin.math.ceil
@ -11,10 +13,8 @@ class GocryptfsDataSource(private val gocryptfsVolume: GocryptfsVolume, private
private var handleID = -1
private var fileSize: Long = -1
private var fileOffset: Long = 0
override fun open(dataSpec: DataSpec?): Long {
dataSpec?.let {
fileOffset = dataSpec.position
}
override fun open(dataSpec: DataSpec): Long {
fileOffset = dataSpec.position
handleID = gocryptfsVolume.openReadMode(filePath)
fileSize = gocryptfsVolume.getSize(filePath)
return fileSize
@ -28,7 +28,7 @@ class GocryptfsDataSource(private val gocryptfsVolume: GocryptfsVolume, private
gocryptfsVolume.closeFile(handleID)
}
override fun addTransferListener(transferListener: TransferListener?) {
override fun addTransferListener(transferListener: TransferListener) {
//too lazy to implement this
}
@ -44,7 +44,7 @@ class GocryptfsDataSource(private val gocryptfsVolume: GocryptfsVolume, private
} else {
ByteArray(tmpReadLength)
}
val read = gocryptfsVolume.readFile(handleID, fileOffset, tmpBuff)
val read = gocryptfsVolume.readFile(handleID, fileOffset, tmpBuff)
System.arraycopy(tmpBuff, 0, buffer, offset+totalRead, read)
fileOffset += read
totalRead += read
@ -52,7 +52,7 @@ class GocryptfsDataSource(private val gocryptfsVolume: GocryptfsVolume, private
return totalRead
}
class Factory(private val gocryptfsVolume: GocryptfsVolume, private val filePath: String): DataSource.Factory{
class Factory(private val gocryptfsVolume: GocryptfsVolume, private val filePath: String): DataSource.Factory {
override fun createDataSource(): DataSource {
return GocryptfsDataSource(gocryptfsVolume, filePath)
}

View File

@ -16,9 +16,6 @@ import com.bumptech.glide.load.resource.bitmap.BitmapTransformation
import kotlinx.android.synthetic.main.activity_image_viewer.*
import sushi.hardcore.droidfs.ConstValues
import sushi.hardcore.droidfs.R
import sushi.hardcore.droidfs.explorers.ExplorerElement
import sushi.hardcore.droidfs.util.MiscUtils
import sushi.hardcore.droidfs.util.PathUtils
import sushi.hardcore.droidfs.widgets.ColoredAlertDialogBuilder
import sushi.hardcore.droidfs.widgets.ZoomableImageView
import java.io.ByteArrayInputStream
@ -36,11 +33,7 @@ class ImageViewer: FileViewerActivity() {
private lateinit var glideImage: RequestBuilder<Drawable>
private var x1 = 0F
private var x2 = 0F
private val mappedImages = mutableListOf<ExplorerElement>()
private lateinit var sortOrder: String
private var wasMapped = false
private var slideshowActive = false
private var currentMappedImageIndex = -1
private var rotationAngle: Float = 0F
private var rotatedBitmap: Bitmap? = null
private val handler = Handler()
@ -54,6 +47,11 @@ class ImageViewer: FileViewerActivity() {
swipeImage(-1F, true)
}
}
override fun getFileType(): String {
return "image"
}
override fun viewFile() {
setContentView(R.layout.activity_image_viewer)
image_viewer.setOnInteractionListener(object : ZoomableImageView.OnInteractionListener {
@ -106,38 +104,13 @@ class ImageViewer: FileViewerActivity() {
}
private fun swipeImage(deltaX: Float, slideshowSwipe: Boolean = false){
if (!wasMapped){
for (e in gocryptfsVolume.recursiveMapFiles(PathUtils.getParentPath(filePath))){
if (e.isRegularFile && ConstValues.isImage(e.name)){
mappedImages.add(e)
}
}
sortOrder = intent.getStringExtra("sortOrder") ?: "name"
ExplorerElement.sortBy(sortOrder, mappedImages)
for ((i, e) in mappedImages.withIndex()){
if (filePath == e.fullPath){
currentMappedImageIndex = i
break
}
}
wasMapped = true
}
if (mappedImages.size == 0){ //can happen on deleting images
goBackToExplorer()
} else {
currentMappedImageIndex = if (deltaX < 0){
MiscUtils.incrementIndex(currentMappedImageIndex, mappedImages)
} else {
MiscUtils.decrementIndex(currentMappedImageIndex, mappedImages)
}
filePath = mappedImages[currentMappedImageIndex].fullPath
loadImage()
if (slideshowActive){
if (!slideshowSwipe) { //reset slideshow delay if user swipes
handler.removeCallbacks(slideshowNext)
}
handler.postDelayed(slideshowNext, ConstValues.slideshow_delay)
playlistNext(deltaX < 0)
loadImage()
if (slideshowActive){
if (!slideshowSwipe) { //reset slideshow delay if user swipes
handler.removeCallbacks(slideshowNext)
}
handler.postDelayed(slideshowNext, ConstValues.slideshow_delay)
}
}
@ -147,10 +120,13 @@ class ImageViewer: FileViewerActivity() {
.setTitle(R.string.warning)
.setPositiveButton(R.string.ok) { _, _ ->
if (gocryptfsVolume.removeFile(filePath)){
currentMappedImageIndex = MiscUtils.decrementIndex(currentMappedImageIndex, mappedImages)
mappedImages.clear()
wasMapped = false
swipeImage(-1F)
playlistNext(true)
refreshPlaylist()
if (mappedPlaylist.size == 0) { //deleted all images of the playlist
goBackToExplorer()
} else {
loadImage()
}
} else {
ColoredAlertDialogBuilder(this)
.keepFullScreen()

View File

@ -1,18 +1,17 @@
package sushi.hardcore.droidfs.file_viewers
import android.view.WindowManager
import androidx.appcompat.app.AlertDialog
import com.google.android.exoplayer2.ExoPlaybackException
import com.google.android.exoplayer2.MediaItem
import com.google.android.exoplayer2.Player
import com.google.android.exoplayer2.SimpleExoPlayer
import com.google.android.exoplayer2.extractor.ExtractorsFactory
import com.google.android.exoplayer2.extractor.flac.FlacExtractor
import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor
import com.google.android.exoplayer2.extractor.mp3.Mp3Extractor
import com.google.android.exoplayer2.extractor.mp4.Mp4Extractor
import com.google.android.exoplayer2.extractor.ogg.OggExtractor
import com.google.android.exoplayer2.extractor.wav.WavExtractor
import com.google.android.exoplayer2.source.LoopingMediaSource
import com.google.android.exoplayer2.source.MediaSource
import com.google.android.exoplayer2.source.ProgressiveMediaSource
import sushi.hardcore.droidfs.ConstValues
import sushi.hardcore.droidfs.R
@ -20,46 +19,48 @@ import sushi.hardcore.droidfs.widgets.ColoredAlertDialogBuilder
abstract class MediaPlayer: FileViewerActivity() {
private lateinit var player: SimpleExoPlayer
private var currentWindow = 0
private var playbackPosition: Long = 0
private lateinit var errorDialog: AlertDialog
override fun viewFile() {
errorDialog = ColoredAlertDialogBuilder(this)
.setTitle(R.string.error)
.setMessage(R.string.playing_failed)
.setCancelable(false)
.setPositiveButton(R.string.ok) { _, _ -> goBackToExplorer()}
.create()
hideSystemUi()
initializePlayer()
}
abstract fun bindPlayer(player: SimpleExoPlayer)
protected open fun onPlaylistIndexChanged() {}
private fun initializePlayer(){
player = SimpleExoPlayer.Builder(this).build()
bindPlayer(player)
private fun createMediaSource(filePath: String): MediaSource {
val dataSourceFactory = GocryptfsDataSource.Factory(gocryptfsVolume, filePath)
val mediaSource = ProgressiveMediaSource.Factory(dataSourceFactory, ExtractorsFactory {
arrayOf(
return ProgressiveMediaSource.Factory(dataSourceFactory, { arrayOf(
MatroskaExtractor(),
Mp4Extractor(),
Mp3Extractor(),
OggExtractor(),
WavExtractor(),
FlacExtractor()
)
}).createMediaSource(ConstValues.fakeUri)
player.seekTo(currentWindow, playbackPosition)
) }).createMediaSource(MediaItem.fromUri(ConstValues.fakeUri))
}
private fun initializePlayer(){
player = SimpleExoPlayer.Builder(this).build()
bindPlayer(player)
createPlaylist()
for (e in mappedPlaylist) {
player.addMediaSource(createMediaSource(e.fullPath))
}
player.repeatMode = Player.REPEAT_MODE_ALL
player.seekToDefaultPosition(currentPlaylistIndex)
player.playWhenReady = true
player.addListener(object : Player.EventListener{
override fun onPlayerError(error: ExoPlaybackException) {
if (error.type == ExoPlaybackException.TYPE_SOURCE){
errorDialog.show()
ColoredAlertDialogBuilder(this@MediaPlayer)
.setTitle(R.string.error)
.setMessage(R.string.playing_failed)
.setCancelable(false)
.setPositiveButton(R.string.ok) { _, _ -> goBackToExplorer()}
.show()
}
}
override fun onIsPlayingChanged(isPlaying: Boolean) {
if (isPlaying){
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
@ -67,20 +68,20 @@ abstract class MediaPlayer: FileViewerActivity() {
window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
}
}
override fun onPositionDiscontinuity(reason: Int) {
if (player.currentWindowIndex != currentPlaylistIndex) {
playlistNext(player.currentWindowIndex == (currentPlaylistIndex+1)%mappedPlaylist.size)
onPlaylistIndexChanged()
}
}
})
player.prepare(LoopingMediaSource(mediaSource), false, false)
}
private fun releasePlayer(){
if (::player.isInitialized) {
playbackPosition = player.currentPosition
currentWindow = player.currentWindowIndex
player.release()
}
player.prepare()
}
override fun onDestroy() {
super.onDestroy()
releasePlayer()
if (::player.isInitialized) {
player.release()
}
}
}

View File

@ -1,5 +1,6 @@
package sushi.hardcore.droidfs.file_viewers
import android.annotation.SuppressLint
import android.text.Editable
import android.text.TextWatcher
import android.view.Menu
@ -24,6 +25,11 @@ class TextEditor: FileViewerActivity() {
override fun hideSystemUi() {
//don't hide system ui
}
override fun getFileType(): String {
return "text"
}
override fun viewFile() {
loadWholeFile(filePath)?.let {
fileName = File(filePath).name
@ -60,6 +66,7 @@ class TextEditor: FileViewerActivity() {
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
if (!changedSinceLastSave){
changedSinceLastSave = true
@SuppressLint("SetTextI18n")
titleText.text = "*$fileName"
}
}

View File

@ -13,4 +13,8 @@ class VideoPlayer: MediaPlayer() {
override fun bindPlayer(player: SimpleExoPlayer) {
video_player.player = player
}
override fun getFileType(): String {
return "video"
}
}

View File

@ -1,18 +0,0 @@
package sushi.hardcore.droidfs.util
object MiscUtils {
fun incrementIndex(index: Int, list: List<Any>): Int {
var i = index+1
if (i >= list.size){
i = 0
}
return i
}
fun decrementIndex(index: Int, list: List<Any>): Int {
var i = index-1
if (i < 0){
i = list.size-1
}
return i
}
}

View File

@ -19,7 +19,6 @@
android:id="@+id/audio_controller"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:controller_layout_id="@layout/exo_custom_playback_control"
app:show_timeout="0"/>
</LinearLayout>

View File

@ -2,15 +2,13 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:gravity="center"
android:background="@color/fullScreenBackgroundColor">
<com.google.android.exoplayer2.ui.SimpleExoPlayerView
<com.google.android.exoplayer2.ui.PlayerView
android:id="@+id/video_player"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
app:controller_layout_id="@layout/exo_custom_playback_control"/>
android:layout_gravity="center"/>
</LinearLayout>

View File

@ -1,74 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:layoutDirection="ltr"
android:background="#CC000000"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:paddingTop="4dp"
android:orientation="horizontal">
<ImageButton android:id="@id/exo_rew"
style="@style/ExoMediaButton.Rewind"/>
<ImageButton android:id="@id/exo_shuffle"
style="@style/ExoMediaButton"/>
<ImageButton android:id="@id/exo_repeat_toggle"
style="@style/ExoMediaButton"/>
<ImageButton android:id="@id/exo_play"
style="@style/ExoMediaButton.Play"/>
<ImageButton android:id="@id/exo_pause"
style="@style/ExoMediaButton.Pause"/>
<ImageButton android:id="@id/exo_ffwd"
style="@style/ExoMediaButton.FastForward"/>
<ImageButton android:id="@id/exo_vr"
style="@style/ExoMediaButton.VR"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView android:id="@id/exo_position"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="14sp"
android:textStyle="bold"
android:paddingLeft="4dp"
android:paddingRight="4dp"
android:includeFontPadding="false"
android:textColor="#FFBEBEBE"/>
<View android:id="@id/exo_progress_placeholder"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="26dp"/>
<TextView android:id="@id/exo_duration"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="14sp"
android:textStyle="bold"
android:paddingLeft="4dp"
android:paddingRight="4dp"
android:includeFontPadding="false"
android:textColor="#FFBEBEBE"/>
</LinearLayout>
</LinearLayout>