(WIP)Android SAF

This commit is contained in:
shabinder 2021-05-14 02:51:33 +05:30
parent 96fdd52ef4
commit ea48d929a4
5 changed files with 125 additions and 292 deletions

View File

@ -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()
}
} }
}*/ }
} }
} }
} }

View File

@ -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)))
} }
} }

View File

@ -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)

View File

@ -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)
} }

View 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
} }