mirror of
https://github.com/Shabinder/SpotiFlyer.git
synced 2024-11-22 17:14:32 +01:00
(WIP)Android SAF
This commit is contained in:
parent
96fdd52ef4
commit
ea48d929a4
@ -46,7 +46,6 @@ import com.arkivanov.decompose.extensions.compose.jetbrains.rememberRootComponen
|
|||||||
import com.arkivanov.mvikotlin.logging.store.LoggingStoreFactory
|
import com.arkivanov.mvikotlin.logging.store.LoggingStoreFactory
|
||||||
import com.arkivanov.mvikotlin.main.store.DefaultStoreFactory
|
import com.arkivanov.mvikotlin.main.store.DefaultStoreFactory
|
||||||
import com.github.k1rakishou.fsaf.FileChooser
|
import com.github.k1rakishou.fsaf.FileChooser
|
||||||
import com.github.k1rakishou.fsaf.FileManager
|
|
||||||
import com.github.k1rakishou.fsaf.callback.directory.DirectoryChooserCallback
|
import com.github.k1rakishou.fsaf.callback.directory.DirectoryChooserCallback
|
||||||
import com.google.accompanist.insets.ProvideWindowInsets
|
import com.google.accompanist.insets.ProvideWindowInsets
|
||||||
import com.google.accompanist.insets.navigationBarsPadding
|
import com.google.accompanist.insets.navigationBarsPadding
|
||||||
@ -58,7 +57,6 @@ import com.shabinder.common.models.Actions
|
|||||||
import com.shabinder.common.models.DownloadStatus
|
import com.shabinder.common.models.DownloadStatus
|
||||||
import com.shabinder.common.models.PlatformActions
|
import com.shabinder.common.models.PlatformActions
|
||||||
import com.shabinder.common.models.PlatformActions.Companion.SharedPreferencesKey
|
import com.shabinder.common.models.PlatformActions.Companion.SharedPreferencesKey
|
||||||
import com.shabinder.common.models.SpotiFlyerBaseDir
|
|
||||||
import com.shabinder.common.models.Status
|
import com.shabinder.common.models.Status
|
||||||
import com.shabinder.common.models.TrackDetails
|
import com.shabinder.common.models.TrackDetails
|
||||||
import com.shabinder.common.models.methods
|
import com.shabinder.common.models.methods
|
||||||
@ -148,34 +146,13 @@ class MainActivity : ComponentActivity() {
|
|||||||
|
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
private fun setUpOnPrefClickListener() {
|
private fun setUpOnPrefClickListener() {
|
||||||
/*Get User Permission to access External SD*//*
|
/*Get User Permission to access External SD*/
|
||||||
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).apply {
|
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).apply {
|
||||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
||||||
addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
|
addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
|
||||||
}
|
}
|
||||||
startActivityForResult(intent, externalSDWriteAccess)*/
|
startActivityForResult(intent, externalSDWriteAccess)
|
||||||
val fileChooser = FileChooser(applicationContext)
|
|
||||||
fileChooser.openChooseDirectoryDialog(object : DirectoryChooserCallback() {
|
|
||||||
override fun onResult(uri: Uri) {
|
|
||||||
println("treeUri = $uri")
|
|
||||||
// Can be only used using SAF
|
|
||||||
contentResolver.takePersistableUriPermission(uri,
|
|
||||||
Intent.FLAG_GRANT_READ_URI_PERMISSION or
|
|
||||||
Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
|
||||||
val treeDocumentFile = DocumentFile.fromTreeUri(applicationContext, uri)
|
|
||||||
|
|
||||||
dir.setDownloadDirectory(uri)
|
|
||||||
showPopUpMessage("New Download Directory Set")
|
|
||||||
GlobalScope.launch {
|
|
||||||
dir.createDirectories()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCancel(reason: String) {
|
|
||||||
println("Canceled by user")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showPopUpMessage(string: String, long: Boolean = false) {
|
private fun showPopUpMessage(string: String, long: Boolean = false) {
|
||||||
@ -327,24 +304,19 @@ class MainActivity : ComponentActivity() {
|
|||||||
|
|
||||||
externalSDWriteAccess -> {
|
externalSDWriteAccess -> {
|
||||||
// Can be only used using SAF
|
// Can be only used using SAF
|
||||||
/*if (resultCode == RESULT_OK) {
|
if (resultCode == RESULT_OK) {
|
||||||
val treeUri: Uri? = data?.data
|
val treeUri: Uri? = data?.data
|
||||||
if (treeUri == null){
|
if (treeUri == null) {
|
||||||
showPopUpMessage("Some Error Occurred While Setting New Download Directory")
|
showPopUpMessage("Some Error Occurred While Setting New Download Directory")
|
||||||
}else {
|
}else {
|
||||||
// Persistently save READ & WRITE Access to whole Selected Directory Tree
|
// Persistently save READ & WRITE Access to whole Selected Directory Tree
|
||||||
contentResolver.takePersistableUriPermission(treeUri,
|
contentResolver.takePersistableUriPermission(treeUri,
|
||||||
Intent.FLAG_GRANT_READ_URI_PERMISSION or
|
Intent.FLAG_GRANT_READ_URI_PERMISSION or
|
||||||
Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
||||||
dir.setDownloadDirectory(com.shabinder.common.models.File(
|
dir.setDownloadDirectory(treeUri)
|
||||||
DocumentFile.fromTreeUri(applicationContext,treeUri)?.createDirectory("SpotiFlyer")!!)
|
|
||||||
)
|
|
||||||
showPopUpMessage("New Download Directory Set")
|
showPopUpMessage("New Download Directory Set")
|
||||||
GlobalScope.launch {
|
|
||||||
dir.createDirectories()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}*/
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,17 +16,16 @@
|
|||||||
|
|
||||||
package com.shabinder.common.di
|
package com.shabinder.common.di
|
||||||
|
|
||||||
|
import android.content.ContentValues
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
import android.util.Log
|
import android.provider.MediaStore
|
||||||
import androidx.compose.ui.graphics.asImageBitmap
|
import androidx.compose.ui.graphics.asImageBitmap
|
||||||
import androidx.documentfile.provider.DocumentFile
|
|
||||||
import co.touchlab.kermit.Kermit
|
import co.touchlab.kermit.Kermit
|
||||||
import com.github.k1rakishou.fsaf.FileManager
|
import com.github.k1rakishou.fsaf.FileManager
|
||||||
import com.github.k1rakishou.fsaf.file.AbstractFile
|
|
||||||
import com.github.k1rakishou.fsaf.file.DirectorySegment
|
import com.github.k1rakishou.fsaf.file.DirectorySegment
|
||||||
import com.github.k1rakishou.fsaf.file.FileSegment
|
import com.github.k1rakishou.fsaf.file.FileSegment
|
||||||
import com.github.k1rakishou.fsaf.manager.base_directory.BaseDirectory
|
import com.github.k1rakishou.fsaf.manager.base_directory.BaseDirectory
|
||||||
@ -45,13 +44,13 @@ import kotlinx.coroutines.launch
|
|||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
import org.koin.core.component.get
|
import org.koin.core.component.get
|
||||||
import java.io.FileInputStream
|
|
||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.net.HttpURLConnection
|
import java.net.HttpURLConnection
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Ignore Deprecation
|
* Ignore Deprecation
|
||||||
* Deprecation is only a Suggestion P-)
|
* Deprecation is only a Suggestion P-)
|
||||||
@ -60,22 +59,32 @@ import java.net.URL
|
|||||||
actual class Dir actual constructor(
|
actual class Dir actual constructor(
|
||||||
private val logger: Kermit,
|
private val logger: Kermit,
|
||||||
private val settings: Settings,
|
private val settings: Settings,
|
||||||
private val spotiFlyerDatabase: SpotiFlyerDatabase,
|
spotiFlyerDatabase: SpotiFlyerDatabase,
|
||||||
): KoinComponent {
|
): KoinComponent {
|
||||||
|
|
||||||
private val context: Context = get()
|
private val context: Context = get()
|
||||||
val fileManager = FileManager(context)
|
val fileManager = FileManager(context)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
fileManager.registerBaseDir<SpotiFlyerBaseDir>(SpotiFlyerBaseDir({ getDirType() },
|
fileManager.apply {
|
||||||
getJavaFile = {
|
registerBaseDir<SpotiFlyerBaseDir>(SpotiFlyerBaseDir({ getDirType() },
|
||||||
java.io.File(
|
getJavaFile = {
|
||||||
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC).toString()
|
java.io.File(
|
||||||
+ "/SpotiFlyer/"
|
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC)
|
||||||
)
|
.toString()
|
||||||
},
|
+ "/SpotiFlyer/"
|
||||||
getSAFUri = { null }
|
)
|
||||||
))
|
},
|
||||||
|
getSAFUri = {
|
||||||
|
settings.getStringOrNull(DirKey)?.let {
|
||||||
|
Uri.parse(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
))
|
||||||
|
defaultDir().documentFile?.let {
|
||||||
|
createSnapshot(it,true)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@ -105,27 +114,32 @@ actual class Dir actual constructor(
|
|||||||
|
|
||||||
actual fun setDownloadDirectory(newBasePath:File) = settings.putString(
|
actual fun setDownloadDirectory(newBasePath:File) = settings.putString(
|
||||||
DirKey,
|
DirKey,
|
||||||
newBasePath.documentFile?.getFullPath()!!
|
newBasePath.documentFile!!.getFullPath()
|
||||||
)
|
)
|
||||||
|
|
||||||
fun setDownloadDirectory(treeUri:Uri) {
|
fun setDownloadDirectory(treeUri:Uri) {
|
||||||
fileManager.registerBaseDir<SpotiFlyerBaseDir>(SpotiFlyerBaseDir(
|
try {
|
||||||
{ getDirType() },
|
fileManager.apply {
|
||||||
getJavaFile = {
|
registerBaseDir<SpotiFlyerBaseDir>(SpotiFlyerBaseDir(
|
||||||
null
|
{ getDirType() },
|
||||||
},
|
getJavaFile = {
|
||||||
getSAFUri = {
|
null
|
||||||
treeUri
|
},
|
||||||
|
getSAFUri = {
|
||||||
|
treeUri
|
||||||
|
}
|
||||||
|
))
|
||||||
|
fromUri(treeUri)?.let { createSnapshot(it,true) }
|
||||||
}
|
}
|
||||||
))
|
} catch (e:IllegalArgumentException) {
|
||||||
|
methods.value.showPopUpMessage("This Directory is already set as Download Directory")
|
||||||
|
}
|
||||||
|
GlobalScope.launch {
|
||||||
|
setDownloadDirectory(File(fileManager.fromUri(treeUri)))
|
||||||
|
createDirectories()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("DEPRECATION")// By Default Save Files to /Music/SpotiFlyer/
|
|
||||||
private val defaultBaseDir = SpotiFlyerBaseDir({ getDirType() },
|
|
||||||
getJavaFile = {java.io.File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC).toString() + "/SpotiFlyer/")},
|
|
||||||
getSAFUri = { null }
|
|
||||||
)
|
|
||||||
|
|
||||||
// Image Cache Path
|
// Image Cache Path
|
||||||
// We Will Handling Image relating operations using java.io.File (reason: Faster)
|
// We Will Handling Image relating operations using java.io.File (reason: Faster)
|
||||||
actual val imageCachePath: String get() = methods.value.platformActions.imageCacheDir.absolutePath + "/"
|
actual val imageCachePath: String get() = methods.value.platformActions.imageCacheDir.absolutePath + "/"
|
||||||
@ -145,25 +159,9 @@ actual class Dir actual constructor(
|
|||||||
fileManager.create(dirPath.documentFile!!)
|
fileManager.create(dirPath.documentFile!!)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*try {
|
|
||||||
val yourAppDir = File(dirPath)
|
|
||||||
|
|
||||||
if (!yourAppDir.exists() && !yourAppDir.isDirectory) { // create empty directory
|
|
||||||
if (yourAppDir.mkdirs()) { logger.i { "$dirPath created" } } else {
|
|
||||||
logger.e { "Unable to create Dir: $dirPath!" }
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
logger.i { "$dirPath already exists" }
|
|
||||||
}
|
|
||||||
} catch (e: SecurityException) {
|
|
||||||
//TRY USING SAF
|
|
||||||
Log.d("Directory","USING SAF to create $dirPath")
|
|
||||||
val file = DocumentFile.fromTreeUri(context, Uri.parse(defaultDir()))
|
|
||||||
DocumentFile.fromFile()
|
|
||||||
}*/
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("unused")
|
||||||
actual suspend fun clearCache(): Unit = withContext(dispatcherIO) {
|
actual suspend fun clearCache(): Unit = withContext(dispatcherIO) {
|
||||||
try {
|
try {
|
||||||
java.io.File(imageCachePath).deleteRecursively()
|
java.io.File(imageCachePath).deleteRecursively()
|
||||||
@ -178,20 +176,20 @@ actual class Dir actual constructor(
|
|||||||
trackDetails: TrackDetails,
|
trackDetails: TrackDetails,
|
||||||
postProcess: (track: TrackDetails) -> Unit,
|
postProcess: (track: TrackDetails) -> Unit,
|
||||||
): Unit = withContext(dispatcherIO) {
|
): Unit = withContext(dispatcherIO) {
|
||||||
val songFile = java.io.File(imageCachePath+"Tracks/"+ removeIllegalChars(trackDetails.title) + ".mp3")
|
val mediaFile = java.io.File(imageCachePath+"Tracks/"+ removeIllegalChars(trackDetails.title) + ".mp3")
|
||||||
try {
|
try {
|
||||||
|
|
||||||
/*Make intermediate Dirs if they don't exist yet*/
|
/*Make intermediate Dirs if they don't exist yet*/
|
||||||
if(!songFile.exists()) {
|
if(!mediaFile.exists()) {
|
||||||
songFile.parentFile?.mkdirs()
|
mediaFile.parentFile?.mkdirs()
|
||||||
}
|
}
|
||||||
|
// Write Bytes to Media File
|
||||||
|
mediaFile.writeBytes(mp3ByteArray)
|
||||||
|
|
||||||
if(mp3ByteArray.isNotEmpty()) songFile.writeBytes(mp3ByteArray)
|
// Add Metadata to Media File
|
||||||
|
Mp3File(mediaFile)
|
||||||
Mp3File(songFile)
|
|
||||||
.removeAllTags()
|
.removeAllTags()
|
||||||
.setId3v1Tags(trackDetails)
|
.setId3v1Tags(trackDetails)
|
||||||
.setId3v2TagsAndSaveFile(trackDetails,songFile.absolutePath)
|
.setId3v2TagsAndSaveFile(trackDetails,mediaFile.absolutePath)
|
||||||
|
|
||||||
// Copy File to Desired Location
|
// Copy File to Desired Location
|
||||||
val documentFile = when(getDirType()){
|
val documentFile = when(getDirType()){
|
||||||
@ -201,105 +199,52 @@ actual class Dir actual constructor(
|
|||||||
BaseDirectory.ActiveBaseDirType.JavaFileBaseDir -> {
|
BaseDirectory.ActiveBaseDirType.JavaFileBaseDir -> {
|
||||||
fileManager.fromPath(trackDetails.outputFilePath)
|
fileManager.fromPath(trackDetails.outputFilePath)
|
||||||
}
|
}
|
||||||
}.also { fileManager.create(it!!) }
|
}.also {
|
||||||
|
// Create Desired File if it doesn't exists yet
|
||||||
|
fileManager.create(it!!)
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
fileManager.copyFileContents(
|
fileManager.copyFileContents(
|
||||||
fileManager.fromRawFile(songFile),
|
fileManager.fromRawFile(mediaFile),
|
||||||
documentFile!!
|
documentFile!!
|
||||||
)
|
)
|
||||||
songFile.deleteOnExit()
|
mediaFile.deleteOnExit()
|
||||||
/*val inStream = FileInputStream(songFile)
|
|
||||||
|
|
||||||
val buffer = ByteArray(1024)
|
|
||||||
var readLen: Int
|
|
||||||
while (inStream.read(buffer).also { readLen = it } != -1) {
|
|
||||||
outStream?.write(buffer, 0, readLen)
|
|
||||||
}
|
|
||||||
inStream.close()
|
|
||||||
// write the output file (You have now copied the file)
|
|
||||||
outStream?.flush()
|
|
||||||
outStream?.close()*/
|
|
||||||
|
|
||||||
}catch (e:Exception) {
|
}catch (e:Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
|
|
||||||
documentFile?.let {
|
documentFile?.let {
|
||||||
addToLibrary(File(it))
|
addToLibrary(File(it),trackDetails)
|
||||||
}
|
}
|
||||||
|
|
||||||
/*when (trackDetails.outputFilePath.substringAfterLast('.')) {
|
|
||||||
".mp3" -> {
|
|
||||||
Mp3File(songFile)
|
|
||||||
.removeAllTags()
|
|
||||||
.setId3v1Tags(trackDetails)
|
|
||||||
.setId3v2TagsAndSaveFile(trackDetails,songFile.absolutePath)
|
|
||||||
|
|
||||||
// Copy File to DocumentUri
|
|
||||||
val documentFile = DocumentFile.fromSingleUri(context,Uri.parse(trackDetails.outputFilePath))
|
|
||||||
try {
|
|
||||||
val outStream = context.contentResolver.openOutputStream(documentFile?.uri!!)
|
|
||||||
val inStream = FileInputStream(songFile)
|
|
||||||
|
|
||||||
val buffer = ByteArray(1024)
|
|
||||||
var readLen: Int
|
|
||||||
while (inStream.read(buffer).also { readLen = it } != -1) {
|
|
||||||
outStream?.write(buffer, 0, readLen)
|
|
||||||
}
|
|
||||||
inStream.close()
|
|
||||||
// write the output file (You have now copied the file)
|
|
||||||
outStream?.flush()
|
|
||||||
outStream?.close()
|
|
||||||
|
|
||||||
}catch (e:Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
|
||||||
|
|
||||||
documentFile?.let {
|
|
||||||
addToLibrary(File(it))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
".m4a" -> {
|
|
||||||
*//*FFmpeg.executeAsync(
|
|
||||||
"-i ${m4aFile.absolutePath} -y -b:a 160k -acodec libmp3lame -vn ${m4aFile.absolutePath.substringBeforeLast('.') + ".mp3"}"
|
|
||||||
){ _, returnCode ->
|
|
||||||
when (returnCode) {
|
|
||||||
Config.RETURN_CODE_SUCCESS -> {
|
|
||||||
//FFMPEG task Completed
|
|
||||||
logger.d{ "Async command execution completed successfully." }
|
|
||||||
scope.launch {
|
|
||||||
Mp3File(File(m4aFile.absolutePath.substringBeforeLast('.') + ".mp3"))
|
|
||||||
.removeAllTags()
|
|
||||||
.setId3v1Tags(trackDetails)
|
|
||||||
.setId3v2TagsAndSaveFile(trackDetails)
|
|
||||||
addToLibrary(m4aFile.absolutePath.substringBeforeLast('.') + ".mp3")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Config.RETURN_CODE_CANCEL -> {
|
|
||||||
logger.d{"Async command execution cancelled by user."}
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
logger.d { "Async command execution failed with rc=$returnCode" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}*//*
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
// TODO
|
|
||||||
}
|
|
||||||
}*/
|
|
||||||
}catch (e:Exception){
|
}catch (e:Exception){
|
||||||
withContext(Dispatchers.Main){
|
e.printStackTrace()
|
||||||
//Toast.makeText(appContext,"Could Not Create File:\n${songFile.absolutePath}",Toast.LENGTH_SHORT).show()
|
if(mediaFile.exists()) mediaFile.delete()
|
||||||
}
|
logger.e { "${mediaFile.absolutePath} could not be created" }
|
||||||
if(songFile.exists()) songFile.delete()
|
|
||||||
logger.e { "${songFile.absolutePath} could not be created" }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
actual fun addToLibrary(file: File) {
|
actual fun addToLibrary(file: File,track: TrackDetails) {
|
||||||
// methods.value.platformActions.addToLibrary(path)
|
try {
|
||||||
|
when (getDirType()) {
|
||||||
|
BaseDirectory.ActiveBaseDirType.SafBaseDir -> {
|
||||||
|
val values = ContentValues(4).apply {
|
||||||
|
put(MediaStore.Audio.Media.TITLE, track.title)
|
||||||
|
put(MediaStore.Audio.Media.DISPLAY_NAME, track.title)
|
||||||
|
put(MediaStore.Audio.Media.DATE_ADDED,
|
||||||
|
(System.currentTimeMillis() / 1000).toInt())
|
||||||
|
put(MediaStore.Audio.Media.MIME_TYPE, "audio/mpeg")
|
||||||
|
}
|
||||||
|
context.contentResolver.insert(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
|
||||||
|
values)
|
||||||
|
}
|
||||||
|
BaseDirectory.ActiveBaseDirType.JavaFileBaseDir -> {
|
||||||
|
file.documentFile?.getFullPath()?.let {
|
||||||
|
methods.value.platformActions.addToLibrary(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e:Exception) { e.printStackTrace() }
|
||||||
}
|
}
|
||||||
|
|
||||||
actual suspend fun loadImage(url: String): Picture = withContext(dispatcherIO){
|
actual suspend fun loadImage(url: String): Picture = withContext(dispatcherIO){
|
||||||
@ -319,6 +264,7 @@ actual class Dir actual constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("BlockingMethodInNonBlockingContext")
|
||||||
actual suspend fun cacheImage(image: Any, path: String):Unit = withContext(dispatcherIO) {
|
actual suspend fun cacheImage(image: Any, path: String):Unit = withContext(dispatcherIO) {
|
||||||
try {
|
try {
|
||||||
java.io.File(path).parentFile?.mkdirs()
|
java.io.File(path).parentFile?.mkdirs()
|
||||||
@ -330,6 +276,7 @@ actual class Dir actual constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("BlockingMethodInNonBlockingContext")
|
||||||
private suspend fun freshImage(url: String): Bitmap? = withContext(dispatcherIO) {
|
private suspend fun freshImage(url: String): Bitmap? = withContext(dispatcherIO) {
|
||||||
try {
|
try {
|
||||||
val source = URL(url)
|
val source = URL(url)
|
||||||
@ -372,59 +319,17 @@ actual class Dir actual constructor(
|
|||||||
subFolder: String,
|
subFolder: String,
|
||||||
extension: String,
|
extension: String,
|
||||||
):File {
|
):File {
|
||||||
|
// Create Intermediate Directories
|
||||||
val file = fileManager.create(
|
val file = fileManager.create(
|
||||||
defaultDir().documentFile!!, //Base Dir
|
defaultDir().documentFile!!, //Base Dir
|
||||||
DirectorySegment(removeIllegalChars(type)),
|
DirectorySegment(removeIllegalChars(type)),
|
||||||
DirectorySegment(removeIllegalChars(subFolder)),
|
DirectorySegment(removeIllegalChars(subFolder)),
|
||||||
|
FileSegment(removeIllegalChars(itemName) + extension)
|
||||||
)
|
)
|
||||||
return File(file?.clone(FileSegment(removeIllegalChars(itemName) + extension)))/*.also {
|
|
||||||
if(fileManager.getLength(it.documentFile!!) == 0L){
|
|
||||||
fileManager.delete(it.documentFile!!)
|
|
||||||
}
|
|
||||||
}*/
|
|
||||||
/*GlobalScope.launch {
|
|
||||||
// Create Intermediate Directories
|
|
||||||
var file = defaultDir().documentFile
|
|
||||||
file = file.findFile(removeIllegalChars(type))
|
|
||||||
?: file.createDirectory(removeIllegalChars(type))
|
|
||||||
?: throw Exception("Couldn't Find/Create $type Directory")
|
|
||||||
|
|
||||||
if (subFolder.isNotEmpty()) file.findFile(removeIllegalChars(subFolder))
|
|
||||||
?: file.createDirectory(removeIllegalChars(subFolder))
|
|
||||||
?: throw Exception("Couldn't Find/Create $subFolder Directory")
|
|
||||||
|
|
||||||
}
|
|
||||||
val sep = "%2F"
|
|
||||||
val finalUri = defaultDir().documentFile.uri.toString() + sep +
|
|
||||||
removeIllegalChars(type) + sep +
|
|
||||||
removeIllegalChars(subFolder) + sep +
|
|
||||||
removeIllegalChars(itemName) + extension
|
|
||||||
return File(
|
|
||||||
DocumentFile.fromSingleUri(context,Uri.parse(finalUri))!!
|
|
||||||
).also {
|
|
||||||
Log.d("Final Output File",it.documentFile.uri.toString())
|
|
||||||
}*/
|
|
||||||
|
|
||||||
|
|
||||||
/*file = file?.findFile(removeIllegalChars(type))
|
|
||||||
?: file?.createDirectory(removeIllegalChars(type))
|
|
||||||
?: throw Exception("Couldn't Find/Create $type Directory")
|
|
||||||
|
|
||||||
if (subFolder.isNotEmpty()) file = file.findFile(removeIllegalChars(subFolder))
|
|
||||||
?: file.createDirectory(removeIllegalChars(subFolder))
|
|
||||||
?: throw Exception("Couldn't Find/Create $subFolder Directory")
|
|
||||||
|
|
||||||
// TODO check Mime
|
|
||||||
file = file.findFile(removeIllegalChars(itemName))
|
|
||||||
?: file.createFile("audio/mpeg",removeIllegalChars(itemName))
|
|
||||||
?: throw Exception("Couldn't Find/Create ${removeIllegalChars(itemName) + extension} File")
|
|
||||||
Log.d("Final Output File",file.uri.toString())
|
|
||||||
|
|
||||||
return File(file).also {
|
return File(file).also {
|
||||||
val size = it.documentFile.length()
|
if(fileManager.getLength(it.documentFile!!) == 0L) fileManager.delete(it.documentFile!!)
|
||||||
Log.d("File size", size.toString())
|
}
|
||||||
if(size == 0L) it.documentFile.delete()
|
|
||||||
}*/
|
//?.clone(FileSegment(removeIllegalChars(itemName) + extension)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,39 +18,31 @@ package com.shabinder.common.di.worker
|
|||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.DownloadManager
|
import android.app.DownloadManager
|
||||||
import android.app.DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED
|
|
||||||
import android.app.Notification
|
import android.app.Notification
|
||||||
import android.app.NotificationChannel
|
import android.app.NotificationChannel
|
||||||
import android.app.NotificationManager
|
import android.app.NotificationManager
|
||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
import android.app.PendingIntent.FLAG_CANCEL_CURRENT
|
import android.app.PendingIntent.FLAG_CANCEL_CURRENT
|
||||||
import android.app.Service
|
import android.app.Service
|
||||||
import android.content.BroadcastReceiver
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.IntentFilter
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.os.PowerManager
|
import android.os.PowerManager
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import androidx.core.net.toUri
|
|
||||||
import co.touchlab.kermit.Kermit
|
import co.touchlab.kermit.Kermit
|
||||||
import com.github.k1rakishou.fsaf.FileManager
|
|
||||||
import com.github.k1rakishou.fsaf.file.AbstractFile
|
|
||||||
import com.shabinder.common.di.*
|
import com.shabinder.common.di.*
|
||||||
import com.shabinder.common.di.providers.getData
|
import com.shabinder.common.di.providers.getData
|
||||||
import com.shabinder.common.di.utils.ParallelExecutor
|
import com.shabinder.common.di.utils.ParallelExecutor
|
||||||
import com.shabinder.common.models.DownloadResult
|
import com.shabinder.common.models.DownloadResult
|
||||||
import com.shabinder.common.models.DownloadStatus
|
import com.shabinder.common.models.DownloadStatus
|
||||||
|
import com.shabinder.common.models.Status
|
||||||
import com.shabinder.common.models.TrackDetails
|
import com.shabinder.common.models.TrackDetails
|
||||||
import com.shabinder.downloader.models.formats.Format
|
import com.shabinder.downloader.models.formats.Format
|
||||||
import com.shabinder.common.models.Status
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
import kotlinx.coroutines.flow.collect
|
import kotlinx.coroutines.flow.collect
|
||||||
@ -128,8 +120,7 @@ class ForegroundService : Service(), CoroutineScope {
|
|||||||
|
|
||||||
val downloadObjects: ArrayList<TrackDetails>? = (
|
val downloadObjects: ArrayList<TrackDetails>? = (
|
||||||
it.getParcelableArrayListExtra("object") ?: it.extras?.getParcelableArrayList(
|
it.getParcelableArrayListExtra("object") ?: it.extras?.getParcelableArrayList(
|
||||||
"object"
|
"object")
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
downloadObjects?.let { list ->
|
downloadObjects?.let { list ->
|
||||||
@ -216,13 +207,11 @@ class ForegroundService : Service(), CoroutineScope {
|
|||||||
is DownloadResult.Error -> {
|
is DownloadResult.Error -> {
|
||||||
launch {
|
launch {
|
||||||
logger.d(tag) { it.message }
|
logger.d(tag) { it.message }
|
||||||
/*logger.d(tag) { "${track.title} Requesting Download thru Android DM" }
|
|
||||||
downloadUsingDM(url, track.outputFilePath, track)*/
|
|
||||||
removeFromNotification("Downloading ${track.title}")
|
removeFromNotification("Downloading ${track.title}")
|
||||||
failed++
|
failed++
|
||||||
|
updateNotification()
|
||||||
|
sendTrackBroadcast(Status.FAILED.name,track)
|
||||||
}
|
}
|
||||||
updateNotification()
|
|
||||||
sendTrackBroadcast(Status.FAILED.name,track)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
is DownloadResult.Progress -> {
|
is DownloadResult.Progress -> {
|
||||||
@ -252,14 +241,10 @@ class ForegroundService : Service(), CoroutineScope {
|
|||||||
}
|
}
|
||||||
logger.d(tag) { "${track.title} Download Completed" }
|
logger.d(tag) { "${track.title} Download Completed" }
|
||||||
downloaded++
|
downloaded++
|
||||||
} catch (
|
} catch (e: Exception) {
|
||||||
e: Exception
|
// Download Failed
|
||||||
) {
|
|
||||||
// Try downloading using android DM
|
|
||||||
logger.d(tag) { "${track.title} Download Failed! Error:Fetch!!!!" }
|
logger.d(tag) { "${track.title} Download Failed! Error:Fetch!!!!" }
|
||||||
failed++
|
failed++
|
||||||
/*logger.d(tag) { "${track.title} Requesting Download thru Android DM" }
|
|
||||||
downloadUsingDM(url, track.outputFilePath, track)*/
|
|
||||||
}
|
}
|
||||||
removeFromNotification("Downloading ${track.title}")
|
removeFromNotification("Downloading ${track.title}")
|
||||||
}
|
}
|
||||||
@ -267,54 +252,6 @@ class ForegroundService : Service(), CoroutineScope {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* If Custom Downloader Fails , Android Download Manager To RESCUE!!
|
|
||||||
**/
|
|
||||||
private fun downloadUsingDM(url: String, outputDir: String, track: TrackDetails) {
|
|
||||||
launch {
|
|
||||||
val uri = Uri.parse(url)
|
|
||||||
val request = DownloadManager.Request(uri).apply {
|
|
||||||
setAllowedNetworkTypes(
|
|
||||||
DownloadManager.Request.NETWORK_WIFI or
|
|
||||||
DownloadManager.Request.NETWORK_MOBILE
|
|
||||||
)
|
|
||||||
setAllowedOverRoaming(false)
|
|
||||||
setTitle(track.title)
|
|
||||||
setDescription("Spotify Downloader Working Up here...")
|
|
||||||
setDestinationUri(File(outputDir).toUri())
|
|
||||||
setNotificationVisibility(VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start Download
|
|
||||||
val downloadID = downloadManager.enqueue(request)
|
|
||||||
logger.d("DownloadManager") { "Download Request Sent" }
|
|
||||||
|
|
||||||
val onDownloadComplete: BroadcastReceiver = object : BroadcastReceiver() {
|
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
|
||||||
// Fetching the download id received with the broadcast
|
|
||||||
val id = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1)
|
|
||||||
// Checking if the received broadcast is for our enqueued download by matching download id
|
|
||||||
if (downloadID == id) {
|
|
||||||
allTracksStatus[track.title] = DownloadStatus.Converting
|
|
||||||
launch { dir.saveFileWithMetadata(byteArrayOf(), track){}; converted++ }
|
|
||||||
// Unregister this broadcast Receiver
|
|
||||||
this@ForegroundService.unregisterReceiver(this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
registerReceiver(onDownloadComplete, IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is the method that can be called to update the Notification
|
|
||||||
*/
|
|
||||||
private fun updateNotification() {
|
|
||||||
val mNotificationManager: NotificationManager =
|
|
||||||
getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
||||||
mNotificationManager.notify(notificationId, getNotification())
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun releaseWakeLock() {
|
private fun releaseWakeLock() {
|
||||||
logger.d(tag) { "Releasing Wake Lock" }
|
logger.d(tag) { "Releasing Wake Lock" }
|
||||||
try {
|
try {
|
||||||
@ -341,13 +278,17 @@ class ForegroundService : Service(), CoroutineScope {
|
|||||||
service.createNotificationChannel(channel)
|
service.createNotificationChannel(channel)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Time To Wrap UP
|
||||||
|
* - `Clean Up` and `Stop this Foreground Service`
|
||||||
|
* */
|
||||||
private fun killService() {
|
private fun killService() {
|
||||||
launch {
|
launch {
|
||||||
logger.d(tag) { "Killing Self" }
|
logger.d(tag) { "Killing Self" }
|
||||||
messageList = mutableListOf("Cleaning And Exiting", "", "", "", "")
|
messageList = mutableListOf("Cleaning And Exiting", "", "", "", "")
|
||||||
downloadService.close()
|
downloadService.close()
|
||||||
updateNotification()
|
updateNotification()
|
||||||
// dir.defaultDir().documentFile?.let { cleanFiles(it,dir.fileManager,logger) }
|
dir.defaultDir().documentFile?.let { cleanFiles(it,dir.fileManager,logger) }
|
||||||
cleanFiles(File(dir.imageCachePath + "Tracks/"),logger)
|
cleanFiles(File(dir.imageCachePath + "Tracks/"),logger)
|
||||||
messageList = mutableListOf("", "", "", "", "")
|
messageList = mutableListOf("", "", "", "", "")
|
||||||
releaseWakeLock()
|
releaseWakeLock()
|
||||||
@ -375,6 +316,9 @@ class ForegroundService : Service(), CoroutineScope {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Create A New Notification with all the updated data
|
||||||
|
* */
|
||||||
private fun getNotification(): Notification = NotificationCompat.Builder(this, channelId).run {
|
private fun getNotification(): Notification = NotificationCompat.Builder(this, channelId).run {
|
||||||
setSmallIcon(R.drawable.ic_download_arrow)
|
setSmallIcon(R.drawable.ic_download_arrow)
|
||||||
setContentTitle("Total: $total Completed:$converted Failed:$failed")
|
setContentTitle("Total: $total Completed:$converted Failed:$failed")
|
||||||
@ -402,6 +346,15 @@ class ForegroundService : Service(), CoroutineScope {
|
|||||||
updateNotification()
|
updateNotification()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is the method that can be called to update the Notification
|
||||||
|
*/
|
||||||
|
private fun updateNotification() {
|
||||||
|
val mNotificationManager: NotificationManager =
|
||||||
|
getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
|
mNotificationManager.notify(notificationId, getNotification())
|
||||||
|
}
|
||||||
|
|
||||||
private fun sendTrackBroadcast(action: String, track: TrackDetails) {
|
private fun sendTrackBroadcast(action: String, track: TrackDetails) {
|
||||||
val intent = Intent().apply {
|
val intent = Intent().apply {
|
||||||
setAction(action)
|
setAction(action)
|
||||||
|
@ -37,7 +37,10 @@ fun cleanFiles(directory: AbstractFile,fm: FileManager,logger: Kermit) {
|
|||||||
if (fm.isDirectory(file)) {
|
if (fm.isDirectory(file)) {
|
||||||
cleanFiles(file, fm, logger)
|
cleanFiles(file, fm, logger)
|
||||||
} else if (fm.isFile(file)) {
|
} else if (fm.isFile(file)) {
|
||||||
if (file.getFullPath().substringAfterLast(".") != "mp3") {
|
if (file.getFullPath().substringAfterLast(".") != "mp3"
|
||||||
|
||
|
||||||
|
fm.getLength(file) == 0L
|
||||||
|
) {
|
||||||
logger.d("Files Cleaning") { "Cleaning ${file.getFullPath()}" }
|
logger.d("Files Cleaning") { "Cleaning ${file.getFullPath()}" }
|
||||||
fm.delete(file)
|
fm.delete(file)
|
||||||
}
|
}
|
||||||
|
@ -56,7 +56,7 @@ expect class Dir (
|
|||||||
suspend fun loadImage(url: String): Picture
|
suspend fun loadImage(url: String): Picture
|
||||||
suspend fun clearCache()
|
suspend fun clearCache()
|
||||||
suspend fun saveFileWithMetadata(mp3ByteArray: ByteArray, trackDetails: TrackDetails,postProcess:(track: TrackDetails)->Unit = {})
|
suspend fun saveFileWithMetadata(mp3ByteArray: ByteArray, trackDetails: TrackDetails,postProcess:(track: TrackDetails)->Unit = {})
|
||||||
fun addToLibrary(file: File)
|
fun addToLibrary(file: File,track: TrackDetails)
|
||||||
fun finalOutputFile(itemName: String, type: String, subFolder: String, extension: String = ".mp3"): File
|
fun finalOutputFile(itemName: String, type: String, subFolder: String, extension: String = ".mp3"): File
|
||||||
fun finalOutputPath(itemName: String, type: String, subFolder: String, extension: String = ".mp3"): String
|
fun finalOutputPath(itemName: String, type: String, subFolder: String, extension: String = ".mp3"): String
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user