mirror of
https://github.com/advplyr/audiobookshelf-app.git
synced 2026-08-06 20:08:48 +02:00
Support for android 8+, selectable download location, permissions check, recursive delete
This commit is contained in:
@@ -6,6 +6,9 @@
|
||||
<!-- Permissions -->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<!-- <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />-->
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
@@ -14,7 +17,8 @@
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/AppTheme"
|
||||
android:usesCleartextTraffic="true">
|
||||
android:usesCleartextTraffic="true"
|
||||
android:requestLegacyExternalStorage="true">
|
||||
|
||||
<activity
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode"
|
||||
|
||||
@@ -1,18 +1,32 @@
|
||||
package com.audiobookshelf.app
|
||||
|
||||
import android.app.DownloadManager
|
||||
import android.content.ContentUris
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.provider.MediaStore
|
||||
import android.util.Log
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import com.anggrayudi.storage.SimpleStorage
|
||||
import com.anggrayudi.storage.SimpleStorageHelper
|
||||
import com.anggrayudi.storage.callback.FileCallback
|
||||
import com.anggrayudi.storage.callback.FolderPickerCallback
|
||||
import com.anggrayudi.storage.callback.StorageAccessCallback
|
||||
import com.anggrayudi.storage.file.*
|
||||
import com.anggrayudi.storage.media.FileDescription
|
||||
|
||||
import com.getcapacitor.JSObject
|
||||
import com.getcapacitor.Plugin
|
||||
import com.getcapacitor.PluginCall
|
||||
import com.getcapacitor.PluginMethod
|
||||
import com.getcapacitor.annotation.CapacitorPlugin
|
||||
import org.json.JSONObject
|
||||
import java.io.File
|
||||
import java.util.concurrent.ExecutorService
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
|
||||
@CapacitorPlugin(name = "AudioDownloader")
|
||||
@@ -22,281 +36,415 @@ class AudioDownloader : Plugin() {
|
||||
lateinit var mainActivity:MainActivity
|
||||
lateinit var downloadManager:DownloadManager
|
||||
|
||||
data class AudiobookDownload(val url: String, val filename: String, val downloadId: Long)
|
||||
var downloads:MutableList<AudiobookDownload> = mutableListOf()
|
||||
|
||||
data class CoverItem(val name: String, val coverUrl: String)
|
||||
data class AudiobookItem(val id: Long, val uri: Uri, val name: String, val size: Int, val duration: Int, val coverUrl: String) {
|
||||
data class AudiobookItem(val uri: Uri, val name: String, val size: Long, val coverUrl: String) {
|
||||
fun toJSObject() : JSObject {
|
||||
var obj = JSObject()
|
||||
obj.put("id", this.id)
|
||||
obj.put("uri", this.uri)
|
||||
obj.put("name", this.name)
|
||||
obj.put("size", this.size)
|
||||
obj.put("duration", this.duration)
|
||||
obj.put("coverUrl", this.coverUrl)
|
||||
return obj
|
||||
}
|
||||
}
|
||||
var audiobookItems:MutableList<AudiobookItem> = mutableListOf()
|
||||
|
||||
override fun load() {
|
||||
mainActivity = (activity as MainActivity)
|
||||
downloadManager = activity.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
|
||||
// storage = SimpleStorage(mainActivity)
|
||||
|
||||
var recieverEvent : (evt: String, id: Long) -> Unit = { evt: String, id: Long ->
|
||||
Log.d(tag, "RECEIVE EVT $evt $id")
|
||||
if (evt == "complete") {
|
||||
var path = downloadManager.getUriForDownloadedFile(id)
|
||||
|
||||
var download = downloads.find { it.downloadId == id }
|
||||
var filename = download?.filename
|
||||
|
||||
var jsobj = JSObject()
|
||||
jsobj.put("downloadId", id)
|
||||
jsobj.put("contentUrl", path)
|
||||
jsobj.put("filename", filename)
|
||||
notifyListeners("onDownloadComplete", jsobj)
|
||||
downloads = downloads.filter { it.downloadId != id } as MutableList<AudiobookDownload>
|
||||
}
|
||||
if (evt == "complete") {}
|
||||
if (evt == "clicked") {
|
||||
Log.d(tag, "Clicked $id back in the audiodownloader")
|
||||
}
|
||||
}
|
||||
|
||||
mainActivity.registerBroadcastReceiver(recieverEvent)
|
||||
|
||||
|
||||
setupSimpleStorage()
|
||||
|
||||
Log.d(tag, "Build SDK ${Build.VERSION.SDK_INT}")
|
||||
// Android 9 OR Below Request Permissions
|
||||
// if (Build.VERSION.SDK_INT <= android.os.Build.VERSION_CODES.P) {
|
||||
// Log.d(tag, "Requires Permission")
|
||||
//// storage.requestStorageAccess(9)
|
||||
// var jsobj = JSObject()
|
||||
// jsobj.put("value", "required")
|
||||
// notifyListeners("permission", jsobj)
|
||||
// } else {
|
||||
// Log.d(tag, "Does not request permission")
|
||||
// }
|
||||
}
|
||||
|
||||
fun loadAudiobooks() {
|
||||
var covers = loadCovers()
|
||||
|
||||
val projection = arrayOf(
|
||||
MediaStore.Audio.Media._ID,
|
||||
MediaStore.Audio.Media.DISPLAY_NAME,
|
||||
MediaStore.Audio.Media.DURATION,
|
||||
MediaStore.Audio.Media.SIZE,
|
||||
MediaStore.Audio.Media.IS_AUDIOBOOK,
|
||||
MediaStore.Audio.Media.RELATIVE_PATH
|
||||
)
|
||||
|
||||
var _audiobookItems:MutableList<AudiobookItem> = mutableListOf()
|
||||
val selection = "${MediaStore.Audio.Media.IS_AUDIOBOOK} == ?"
|
||||
val selectionArgs = arrayOf("1")
|
||||
val sortOrder = "${MediaStore.Audio.Media.DISPLAY_NAME} ASC"
|
||||
|
||||
activity.applicationContext.contentResolver.query(
|
||||
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
|
||||
projection,
|
||||
selection,
|
||||
selectionArgs,
|
||||
sortOrder
|
||||
)?.use { cursor ->
|
||||
|
||||
val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media._ID)
|
||||
val nameColumn =
|
||||
cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DISPLAY_NAME)
|
||||
val durationColumn =
|
||||
cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DURATION)
|
||||
val sizeColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.SIZE)
|
||||
val isAudiobookColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.IS_AUDIOBOOK)
|
||||
var relativePathColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.RELATIVE_PATH)
|
||||
|
||||
while (cursor.moveToNext()) {
|
||||
val id = cursor.getLong(idColumn)
|
||||
val name = cursor.getString(nameColumn)
|
||||
val duration = cursor.getInt(durationColumn)
|
||||
val size = cursor.getInt(sizeColumn)
|
||||
var isAudiobook = cursor.getInt(isAudiobookColumn)
|
||||
var relativePath = cursor.getString(relativePathColumn)
|
||||
|
||||
if (isAudiobook == 1) {
|
||||
val contentUri: Uri = ContentUris.withAppendedId(
|
||||
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
|
||||
id
|
||||
)
|
||||
|
||||
Log.d(tag, "Got Content FRom MEdia STORE $id $contentUri, Name: $name, Dur: $duration, Size: $size, relativePath: $relativePath")
|
||||
var audiobookId = File(name).nameWithoutExtension
|
||||
var coverItem:CoverItem? = covers.find{it.name == audiobookId}
|
||||
var coverUrl = coverItem?.coverUrl ?: ""
|
||||
|
||||
_audiobookItems.add(AudiobookItem(id, contentUri, name, duration, size, coverUrl))
|
||||
}
|
||||
}
|
||||
audiobookItems = _audiobookItems
|
||||
|
||||
var audiobookObjs:List<JSObject> = _audiobookItems.map{ it.toJSObject() }
|
||||
|
||||
var mediaItemNoticePayload = JSObject()
|
||||
mediaItemNoticePayload.put("items", audiobookObjs)
|
||||
notifyListeners("onMediaLoaded", mediaItemNoticePayload)
|
||||
private fun setupSimpleStorage() {
|
||||
mainActivity.storageHelper.onFolderSelected = { requestCode, folder ->
|
||||
Log.d(tag, "FOLDER SELECTED $requestCode ${folder.name} ${folder.uri}")
|
||||
var jsobj = JSObject()
|
||||
jsobj.put("value", "granted")
|
||||
jsobj.put("uri", folder.uri)
|
||||
jsobj.put("absolutePath", folder.getAbsolutePath(context))
|
||||
jsobj.put("storageId", folder.getStorageId(context))
|
||||
jsobj.put("storageType", folder.getStorageType(context))
|
||||
jsobj.put("simplePath", folder.getSimplePath(context))
|
||||
jsobj.put("basePath", folder.getBasePath(context))
|
||||
notifyListeners("permission", jsobj)
|
||||
}
|
||||
}
|
||||
|
||||
fun loadCovers() : MutableList<CoverItem> {
|
||||
val projection = arrayOf(
|
||||
MediaStore.Images.Media._ID,
|
||||
MediaStore.Images.Media.DISPLAY_NAME
|
||||
)
|
||||
val sortOrder = "${MediaStore.Images.Media.DISPLAY_NAME} ASC"
|
||||
mainActivity.storage.storageAccessCallback = object : StorageAccessCallback {
|
||||
override fun onRootPathNotSelected(
|
||||
requestCode: Int,
|
||||
rootPath: String,
|
||||
uri: Uri,
|
||||
selectedStorageType: StorageType,
|
||||
expectedStorageType: StorageType
|
||||
) {
|
||||
Log.d(tag, "STORAGE ACCESS CALLBACK")
|
||||
}
|
||||
|
||||
var coverItems:MutableList<CoverItem> = mutableListOf()
|
||||
override fun onCanceledByUser(requestCode: Int) {
|
||||
Log.d(tag, "STORAGE ACCESS CALLBACK")
|
||||
}
|
||||
|
||||
activity.applicationContext.contentResolver.query(
|
||||
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
|
||||
projection,
|
||||
null,
|
||||
null,
|
||||
sortOrder
|
||||
)?.use { cursor ->
|
||||
val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID)
|
||||
val nameColumn =
|
||||
cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DISPLAY_NAME)
|
||||
override fun onExpectedStorageNotSelected(requestCode: Int, selectedFolder: DocumentFile, selectedStorageType: StorageType, expectedBasePath: String, expectedStorageType: StorageType) {
|
||||
Log.d(tag, "STORAGE ACCESS CALLBACK")
|
||||
}
|
||||
|
||||
while (cursor.moveToNext()) {
|
||||
val id = cursor.getLong(idColumn)
|
||||
val filename = cursor.getString(nameColumn)
|
||||
val contentUri: Uri = ContentUris.withAppendedId(
|
||||
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
|
||||
id
|
||||
)
|
||||
override fun onStoragePermissionDenied(requestCode: Int) {
|
||||
Log.d(tag, "STORAGE ACCESS CALLBACK")
|
||||
}
|
||||
|
||||
var name = File(filename).nameWithoutExtension
|
||||
Log.d(tag, "Got IMAGE FRom Media STORE $id $contentUri, Name: $name")
|
||||
|
||||
var coverItem = CoverItem(name, contentUri.toString())
|
||||
coverItems.add(coverItem)
|
||||
override fun onRootPathPermissionGranted(requestCode: Int, root: DocumentFile) {
|
||||
Log.d(tag, "STORAGE ACCESS CALLBACK")
|
||||
}
|
||||
}
|
||||
return coverItems
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.R)
|
||||
@PluginMethod
|
||||
fun load(call: PluginCall) {
|
||||
loadAudiobooks()
|
||||
fun requestStoragePermission(call: PluginCall) {
|
||||
Log.d(tag, "Request Storage Permissions")
|
||||
mainActivity.storageHelper.requestStorageAccess()
|
||||
call.resolve()
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
fun downloadCover(call: PluginCall) {
|
||||
var audiobookId = call.data.getString("audiobookId", "audiobook").toString()
|
||||
var url = call.data.getString("downloadUrl", "unknown").toString()
|
||||
var title = call.data.getString("title", "Cover").toString()
|
||||
var filename = call.data.getString("filename", "audiobook.jpg").toString()
|
||||
fun checkStoragePermission(call: PluginCall) {
|
||||
var res = false
|
||||
|
||||
Log.d(tag, "Called download cover: $url")
|
||||
|
||||
var dlRequest = DownloadManager.Request(Uri.parse(url))
|
||||
dlRequest.setTitle("Cover Art: $title")
|
||||
dlRequest.setDescription("Cover art for audiobook")
|
||||
dlRequest.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_ONLY_COMPLETION)
|
||||
|
||||
var file:File = File(audiobookId, filename)
|
||||
Log.d(tag, "FILE ${file.path} | ${file.canonicalPath}")
|
||||
dlRequest.setDestinationInExternalPublicDir(Environment.DIRECTORY_AUDIOBOOKS, file.path)
|
||||
var downloadId = downloadManager.enqueue(dlRequest)
|
||||
|
||||
var progressReceiver : (prog: Long) -> Unit = { prog: Long ->
|
||||
//
|
||||
if (Build.VERSION.SDK_INT <= android.os.Build.VERSION_CODES.P) {
|
||||
res = SimpleStorage.hasStoragePermission(context)
|
||||
Log.d(tag, "Check Storage Access $res")
|
||||
} else {
|
||||
Log.d(tag, "Has permission on Android 10 or up")
|
||||
res = true
|
||||
}
|
||||
|
||||
var doneReceiver : (success: Boolean) -> Unit = { success: Boolean ->
|
||||
var jsobj = JSObject()
|
||||
if (success) {
|
||||
var path = downloadManager.getUriForDownloadedFile(downloadId)
|
||||
jsobj.put("url", path)
|
||||
call.resolve(jsobj)
|
||||
var jsobj = JSObject()
|
||||
jsobj.put("value", res)
|
||||
call.resolve(jsobj)
|
||||
}
|
||||
|
||||
fun checkUriExists(uri: Uri?): Boolean {
|
||||
if (uri == null) return false
|
||||
val resolver = context.contentResolver
|
||||
//1. Check Uri
|
||||
var cursor: Cursor? = null
|
||||
val isUriExist: Boolean = try {
|
||||
cursor = resolver.query(uri, null, null, null, null)
|
||||
//cursor null: content Uri was invalid or some other error occurred
|
||||
//cursor.moveToFirst() false: Uri was ok but no entry found.
|
||||
(cursor != null && cursor.moveToFirst())
|
||||
} catch (t: Throwable) {
|
||||
false
|
||||
} finally {
|
||||
try {
|
||||
cursor?.close()
|
||||
} catch (t: Throwable) {
|
||||
}
|
||||
false
|
||||
}
|
||||
return isUriExist
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
fun load(call: PluginCall) {
|
||||
var audiobookUrls = call.data.getJSONArray("audiobookUrls")
|
||||
var len = audiobookUrls?.length()
|
||||
if (len == null) {
|
||||
len = 0
|
||||
}
|
||||
Log.d(tag, "CALLED LOAD $len")
|
||||
var audiobookItems:MutableList<AudiobookItem> = mutableListOf()
|
||||
|
||||
(0 until len).forEach {
|
||||
var jsobj = audiobookUrls.get(it) as JSONObject
|
||||
var audiobookUrl = jsobj.get("contentUrl").toString()
|
||||
var coverUrl = jsobj.get("coverUrl").toString()
|
||||
var storageId = ""
|
||||
if(jsobj.has("storageId")) jsobj.get("storageId").toString()
|
||||
|
||||
var basePath = ""
|
||||
if(jsobj.has("basePath")) jsobj.get("basePath").toString()
|
||||
|
||||
var coverBasePath = ""
|
||||
if(jsobj.has("coverBasePath")) jsobj.get("coverBasePath").toString()
|
||||
|
||||
Log.d(tag, "LOOKUP $storageId $basePath $audiobookUrl")
|
||||
|
||||
var audiobookFile: DocumentFile? = null
|
||||
var coverFile: DocumentFile? = null
|
||||
|
||||
// Android 9 OR Below use storage id and base path
|
||||
if (Build.VERSION.SDK_INT <= android.os.Build.VERSION_CODES.P) {
|
||||
audiobookFile = DocumentFileCompat.fromSimplePath(context, storageId, basePath)
|
||||
if (coverUrl != null && coverUrl != "") {
|
||||
coverFile = DocumentFileCompat.fromSimplePath(context, storageId, coverBasePath)
|
||||
}
|
||||
} else {
|
||||
jsobj.put("failed", true)
|
||||
call.resolve(jsobj)
|
||||
// Android 10 and up manually deleting will still load the file causing crash
|
||||
var exists = checkUriExists(Uri.parse(audiobookUrl))
|
||||
if (exists) {
|
||||
Log.d(tag, "Audiobook exists")
|
||||
audiobookFile = DocumentFileCompat.fromUri(context, Uri.parse(audiobookUrl))
|
||||
} else {
|
||||
Log.e(tag, "Audiobook does not exist")
|
||||
}
|
||||
|
||||
var coverExists = checkUriExists(Uri.parse(coverUrl))
|
||||
if (coverExists) {
|
||||
Log.d(tag, "Cover Exists")
|
||||
coverFile = DocumentFileCompat.fromUri(context, Uri.parse(coverUrl))
|
||||
} else if (coverUrl != null && coverUrl != "") {
|
||||
Log.e(tag, "Cover does not exist")
|
||||
}
|
||||
}
|
||||
|
||||
if (audiobookFile == null) {
|
||||
Log.e(tag, "Audiobook was not found $audiobookUrl")
|
||||
} else {
|
||||
var _name = audiobookFile.name
|
||||
if (_name == null) _name = ""
|
||||
var _coverUrl = ""
|
||||
if (coverFile != null) _coverUrl = coverFile.uri.toString()
|
||||
|
||||
var size = audiobookFile.length()
|
||||
var abItem = AudiobookItem(audiobookFile.uri, _name, size, _coverUrl)
|
||||
audiobookItems.add(abItem)
|
||||
}
|
||||
}
|
||||
|
||||
var progressUpdater = DownloadProgressUpdater(downloadManager, downloadId, progressReceiver, doneReceiver)
|
||||
progressUpdater.run()
|
||||
var audiobookObjs:List<JSObject> = audiobookItems.map{ it.toJSObject() }
|
||||
var mediaItemNoticePayload = JSObject()
|
||||
mediaItemNoticePayload.put("items", audiobookObjs)
|
||||
notifyListeners("onMediaLoaded", mediaItemNoticePayload)
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
fun download(call: PluginCall) {
|
||||
var audiobookId = call.data.getString("audiobookId", "audiobook").toString()
|
||||
var url = call.data.getString("downloadUrl", "unknown").toString()
|
||||
var coverDownloadUrl = call.data.getString("coverDownloadUrl", "").toString()
|
||||
var title = call.data.getString("title", "Audiobook").toString()
|
||||
var filename = call.data.getString("filename", "audiobook.mp3").toString()
|
||||
var coverFilename = call.data.getString("coverFilename", "cover.png").toString()
|
||||
var downloadFolderUrl = call.data.getString("downloadFolderUrl", "").toString()
|
||||
var folder = DocumentFileCompat.fromUri(context, Uri.parse(downloadFolderUrl))!!
|
||||
Log.d(tag, "Called download: $url | Folder: ${folder.name} | $downloadFolderUrl")
|
||||
|
||||
Log.d(tag, "Called download: $url")
|
||||
var dlfilename = audiobookId + "." + File(filename).extension
|
||||
var coverdlfilename = audiobookId + "." + File(coverFilename).extension
|
||||
|
||||
var canWriteToFolder = folder.canWrite()
|
||||
if (!canWriteToFolder) {
|
||||
Log.e(tag, "Error Cannot Write to Folder ${folder.baseName}")
|
||||
val ret = JSObject()
|
||||
ret.put("error", "Cannot write to ${folder.baseName}")
|
||||
call.resolve(ret)
|
||||
return
|
||||
}
|
||||
|
||||
var dlRequest = DownloadManager.Request(Uri.parse(url))
|
||||
dlRequest.setTitle(title)
|
||||
dlRequest.setDescription("Downloading to Audiobooks directory")
|
||||
dlRequest.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_ONLY_COMPLETION)
|
||||
dlRequest.setDescription("Downloading to ${folder.name}")
|
||||
dlRequest.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
|
||||
dlRequest.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, dlfilename)
|
||||
|
||||
var file:File = File(audiobookId, filename)
|
||||
Log.d(tag, "FILE ${file.path} | ${file.canonicalPath}")
|
||||
dlRequest.setDestinationInExternalPublicDir(Environment.DIRECTORY_AUDIOBOOKS, file.path)
|
||||
var audiobookDownloadId = downloadManager.enqueue(dlRequest)
|
||||
var coverDownloadId:Long? = null
|
||||
|
||||
var downloadId = downloadManager.enqueue(dlRequest)
|
||||
|
||||
var download = AudiobookDownload(url, filename, downloadId)
|
||||
downloads.add(download)
|
||||
|
||||
var progressReceiver : (prog: Long) -> Unit = { prog: Long ->
|
||||
var jsobj = JSObject()
|
||||
jsobj.put("filename", filename)
|
||||
jsobj.put("downloadId", downloadId)
|
||||
jsobj.put("progress", prog)
|
||||
notifyListeners("onDownloadProgress", jsobj)
|
||||
if (coverDownloadUrl != "") {
|
||||
var coverDlRequest = DownloadManager.Request(Uri.parse(coverDownloadUrl))
|
||||
coverDlRequest.setTitle("Cover: $title")
|
||||
coverDlRequest.setDescription("Downloading to ${folder.name}")
|
||||
coverDlRequest.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_ONLY_COMPLETION)
|
||||
coverDlRequest.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, coverdlfilename)
|
||||
coverDownloadId = downloadManager.enqueue(coverDlRequest)
|
||||
}
|
||||
|
||||
var doneReceiver : (success: Boolean) -> Unit = { success: Boolean ->
|
||||
Log.d(tag, "RECIEVER DONE, SUCCES? $success")
|
||||
var progressReceiver : (id:Long, prog: Long) -> Unit = { id:Long, prog: Long ->
|
||||
if (id == audiobookDownloadId) {
|
||||
var jsobj = JSObject()
|
||||
jsobj.put("audiobookId", audiobookId)
|
||||
jsobj.put("progress", prog)
|
||||
notifyListeners("onDownloadProgress", jsobj)
|
||||
}
|
||||
}
|
||||
|
||||
var progressUpdater = DownloadProgressUpdater(downloadManager, downloadId, progressReceiver, doneReceiver)
|
||||
var coverDocFile:DocumentFile? = null
|
||||
|
||||
var doneReceiver : (id:Long, success: Boolean) -> Unit = { id:Long, success: Boolean ->
|
||||
Log.d(tag, "RECEIVER DONE $id, SUCCES? $success")
|
||||
var docfile:DocumentFile? = null
|
||||
if (id == coverDownloadId) {
|
||||
docfile = DocumentFileCompat.fromPublicFolder(context, PublicDirectory.DOWNLOADS, coverdlfilename)
|
||||
|
||||
Log.d(tag, "Move Cover File ${docfile?.name}")
|
||||
} else if (id == audiobookDownloadId) {
|
||||
docfile = DocumentFileCompat.fromPublicFolder(context, PublicDirectory.DOWNLOADS, dlfilename)
|
||||
Log.d(tag, "Move Audiobook File ${docfile?.name}")
|
||||
}
|
||||
|
||||
var callback = object : FileCallback() {
|
||||
override fun onPrepare() {
|
||||
Log.d(tag, "PREPARING MOVE FILE")
|
||||
}
|
||||
override fun onFailed(errorCode:ErrorCode) {
|
||||
Log.e(tag, "FAILED MOVE FILE $errorCode")
|
||||
docfile?.delete()
|
||||
coverDocFile?.delete()
|
||||
|
||||
if (id == audiobookDownloadId) {
|
||||
var jsobj = JSObject()
|
||||
jsobj.put("audiobookId", audiobookId)
|
||||
jsobj.put("error", "Move failed")
|
||||
notifyListeners("onDownloadFailed", jsobj)
|
||||
}
|
||||
}
|
||||
override fun onCompleted(result:Any) {
|
||||
var resultDocFile = result as DocumentFile
|
||||
var simplePath = resultDocFile.getSimplePath(context)
|
||||
var storageId = resultDocFile.getStorageId(context)
|
||||
var size = resultDocFile.length()
|
||||
Log.d(tag, "Finished Moving File, NAME: ${resultDocFile.name} | $storageId | SimplePath: $simplePath")
|
||||
|
||||
var abFolder = folder.findFolder(title)
|
||||
var jsobj = JSObject()
|
||||
jsobj.put("audiobookId", audiobookId)
|
||||
jsobj.put("downloadId", id)
|
||||
jsobj.put("storageId", storageId)
|
||||
jsobj.put("storageType", resultDocFile.getStorageType(context))
|
||||
jsobj.put("folderUrl", abFolder?.uri)
|
||||
jsobj.put("folderName", abFolder?.name)
|
||||
jsobj.put("downloadFolderUrl", downloadFolderUrl)
|
||||
jsobj.put("contentUrl", resultDocFile.uri)
|
||||
jsobj.put("basePath", resultDocFile.getBasePath(context))
|
||||
jsobj.put("filename", filename)
|
||||
jsobj.put("simplePath", simplePath)
|
||||
jsobj.put("size", size)
|
||||
|
||||
if (resultDocFile.name == filename) {
|
||||
Log.d(tag, "Audiobook Finishing Moving")
|
||||
} else if (resultDocFile.name == coverFilename) {
|
||||
coverDocFile = docfile
|
||||
Log.d(tag, "Audiobook Cover Finished Moving")
|
||||
jsobj.put("isCover", true)
|
||||
}
|
||||
notifyListeners("onDownloadComplete", jsobj)
|
||||
}
|
||||
}
|
||||
|
||||
val executorService: ExecutorService = Executors.newFixedThreadPool(4)
|
||||
executorService.execute {
|
||||
if (id == coverDownloadId) {
|
||||
docfile?.moveFileTo(context, folder, FileDescription(coverFilename, title, MimeType.IMAGE), callback)
|
||||
} else if (id == audiobookDownloadId) {
|
||||
docfile?.moveFileTo(context, folder, FileDescription(filename, title, MimeType.AUDIO), callback)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var progressUpdater = DownloadProgressUpdater(downloadManager, audiobookDownloadId, progressReceiver, doneReceiver)
|
||||
progressUpdater.run()
|
||||
if (coverDownloadId != null) {
|
||||
var coverProgressUpdater = DownloadProgressUpdater(downloadManager, coverDownloadId, progressReceiver, doneReceiver)
|
||||
coverProgressUpdater.run()
|
||||
}
|
||||
|
||||
val ret = JSObject()
|
||||
ret.put("value", downloadId)
|
||||
ret.put("audiobookDownloadId", audiobookDownloadId)
|
||||
ret.put("coverDownloadId", coverDownloadId)
|
||||
call.resolve(ret)
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
fun delete(call: PluginCall) {
|
||||
var audiobookId = call.data.getString("audiobookId", "audiobook").toString()
|
||||
var filename = call.data.getString("filename", "audiobook.mp3").toString()
|
||||
var url = call.data.getString("url", "").toString()
|
||||
var coverUrl = call.data.getString("coverUrl", "").toString()
|
||||
fun selectFolder(call: PluginCall) {
|
||||
mainActivity.storage.folderPickerCallback = object : FolderPickerCallback {
|
||||
override fun onFolderSelected(requestCode: Int, folder: DocumentFile) {
|
||||
Log.d(tag, "ONF OLDER SELECRTED ${folder.uri} ${folder.name}")
|
||||
|
||||
// Does Not Work
|
||||
// var audiobookDirRoot = activity.applicationContext.getExternalFilesDir(Environment.DIRECTORY_AUDIOBOOKS)
|
||||
// Log.d(tag, "AUDIOBOOK DIR ROOT $audiobookDirRoot")
|
||||
// var result = audiobookDirRoot?.deleteRecursively()
|
||||
// Log.d(tag, "DONE DELETING FOLDER $result")
|
||||
var absolutePath = folder.getAbsolutePath(activity)
|
||||
var storageId = folder.getStorageId(activity)
|
||||
var storageType = folder.getStorageType(activity)
|
||||
var simplePath = folder.getSimplePath(activity)
|
||||
var basePath = folder.getBasePath(activity)
|
||||
|
||||
// Does Not Work
|
||||
// var audiobookDir = File(audiobookDirRoot, audiobookId + "/")
|
||||
// Log.d(tag, "Delete Audiobook DIR ${audiobookDir.path} is dir ${audiobookDir.isDirectory}")
|
||||
// var result = audiobookDir.deleteRecursively()
|
||||
//
|
||||
var jsobj = JSObject()
|
||||
jsobj.put("uri", folder.uri)
|
||||
jsobj.put("absolutePath", absolutePath)
|
||||
jsobj.put("storageId", storageId)
|
||||
jsobj.put("storageType", storageType)
|
||||
jsobj.put("simplePath", simplePath)
|
||||
jsobj.put("basePath", basePath)
|
||||
call.resolve(jsobj)
|
||||
}
|
||||
|
||||
// Does Not Work
|
||||
// var audiobookDir = activity.applicationContext.getExternalFilesDir(Environment.DIRECTORY_AUDIOBOOKS)
|
||||
// Log.d(tag, "AUDIOBOOK DIR ${audiobookDir?.path}")
|
||||
// var dir = File(audiobookDir, "$audiobookId/")
|
||||
// Log.d(tag, "DIR DIR ${dir.path}")
|
||||
// var res = dir.delete()
|
||||
// Log.d(tag, "DELETED $res")
|
||||
override fun onStorageAccessDenied(requestCode: Int, folder: DocumentFile?, storageType: StorageType) {
|
||||
Log.e(tag, "STORAGE ACCESS DENIED")
|
||||
var jsobj = JSObject()
|
||||
jsobj.put("error", "Access Denied")
|
||||
call.resolve(jsobj)
|
||||
}
|
||||
|
||||
var contentResolver = activity.applicationContext.contentResolver
|
||||
contentResolver.delete(Uri.parse(url), null, null)
|
||||
|
||||
if (coverUrl != "") {
|
||||
contentResolver.delete(Uri.parse(coverUrl), null, null)
|
||||
override fun onStoragePermissionDenied(requestCode: Int) {
|
||||
Log.d(tag, "STORAGE PERMISSION DENIED $requestCode")
|
||||
var jsobj = JSObject()
|
||||
jsobj.put("error", "Permission Denied")
|
||||
call.resolve(jsobj)
|
||||
}
|
||||
}
|
||||
|
||||
call.resolve()
|
||||
mainActivity.storage.openFolderPicker(6)
|
||||
}
|
||||
|
||||
internal class DownloadProgressUpdater(private val manager: DownloadManager, private val downloadId: Long, private var receiver: (Long) -> Unit, private var doneReceiver: (Boolean) -> Unit) : Thread() {
|
||||
@PluginMethod
|
||||
fun delete(call: PluginCall) {
|
||||
var url = call.data.getString("url", "").toString()
|
||||
var coverUrl = call.data.getString("coverUrl", "").toString()
|
||||
var folderUrl = call.data.getString("folderUrl", "").toString()
|
||||
|
||||
if (folderUrl != "") {
|
||||
Log.d(tag, "CALLED DELETE FIOLDER: $folderUrl")
|
||||
var folder = DocumentFileCompat.fromUri(context, Uri.parse(folderUrl))
|
||||
var success = folder?.deleteRecursively(context)
|
||||
var jsobj = JSObject()
|
||||
jsobj.put("success", success)
|
||||
call.resolve()
|
||||
} else {
|
||||
// Older audiobooks did not store a folder url, use cover and audiobook url
|
||||
var abExists = checkUriExists(Uri.parse(url))
|
||||
if (abExists) {
|
||||
var abfile = DocumentFileCompat.fromUri(context, Uri.parse(url))
|
||||
abfile?.delete()
|
||||
}
|
||||
|
||||
var coverExists = checkUriExists(Uri.parse(coverUrl))
|
||||
if (coverExists) {
|
||||
var coverfile = DocumentFileCompat.fromUri(context, Uri.parse(coverUrl))
|
||||
coverfile?.delete()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal class DownloadProgressUpdater(private val manager: DownloadManager, private val downloadId: Long, private var receiver: (Long, Long) -> Unit, private var doneReceiver: (Long, Boolean) -> Unit) : Thread() {
|
||||
private val query: DownloadManager.Query = DownloadManager.Query()
|
||||
private var totalBytes: Int = 0
|
||||
private var TAG = "DownloadProgressUpdater"
|
||||
@@ -308,34 +456,49 @@ class AudioDownloader : Plugin() {
|
||||
override fun run() {
|
||||
Log.d(TAG, "RUN FOR ID $downloadId")
|
||||
var keepRunning = true
|
||||
var increment = 0
|
||||
while (keepRunning) {
|
||||
Thread.sleep(500)
|
||||
increment++
|
||||
|
||||
if (increment % 4 == 0) {
|
||||
Log.d(TAG, "Loop $increment : $downloadId")
|
||||
}
|
||||
|
||||
manager.query(query).use {
|
||||
if (it.moveToFirst()) {
|
||||
|
||||
//get total bytes of the file
|
||||
if (totalBytes <= 0) {
|
||||
totalBytes = it.getInt(it.getColumnIndex(DownloadManager.COLUMN_TOTAL_SIZE_BYTES))
|
||||
if (totalBytes <= 0) {
|
||||
Log.e(TAG, "Download Is 0 Bytes $downloadId")
|
||||
doneReceiver(downloadId, false)
|
||||
keepRunning = false
|
||||
this.interrupt()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
val downloadStatus = it.getInt(it.getColumnIndex(DownloadManager.COLUMN_STATUS))
|
||||
val bytesDownloadedSoFar = it.getInt(it.getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR))
|
||||
|
||||
if (increment % 4 == 0) {
|
||||
Log.d(TAG, "BYTES $increment : $downloadId : $bytesDownloadedSoFar : TOTAL: $totalBytes")
|
||||
}
|
||||
|
||||
if (downloadStatus == DownloadManager.STATUS_SUCCESSFUL || downloadStatus == DownloadManager.STATUS_FAILED) {
|
||||
if (downloadStatus == DownloadManager.STATUS_SUCCESSFUL) {
|
||||
doneReceiver(true)
|
||||
doneReceiver(downloadId, true)
|
||||
} else {
|
||||
doneReceiver(false)
|
||||
doneReceiver(downloadId, false)
|
||||
}
|
||||
keepRunning = false
|
||||
this.interrupt()
|
||||
} else {
|
||||
//update progress
|
||||
val percentProgress = ((bytesDownloadedSoFar * 100L) / totalBytes)
|
||||
receiver(percentProgress)
|
||||
receiver(downloadId, percentProgress)
|
||||
}
|
||||
|
||||
} else {
|
||||
Log.e(TAG, "NOT FOUND IN QUERY")
|
||||
keepRunning = false
|
||||
@@ -343,6 +506,5 @@ class AudioDownloader : Plugin() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,12 +2,10 @@ package com.audiobookshelf.app
|
||||
|
||||
import android.app.DownloadManager
|
||||
import android.content.*
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.IBinder
|
||||
import android.os.Looper
|
||||
import android.os.*
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import com.anggrayudi.storage.SimpleStorage
|
||||
import com.anggrayudi.storage.SimpleStorageHelper
|
||||
import com.getcapacitor.BridgeActivity
|
||||
|
||||
|
||||
@@ -21,23 +19,19 @@ class MainActivity : BridgeActivity() {
|
||||
lateinit var pluginCallback : () -> Unit
|
||||
lateinit var downloaderCallback : (String, Long) -> Unit
|
||||
|
||||
val storageHelper = SimpleStorageHelper(this)
|
||||
val storage = SimpleStorage(this)
|
||||
|
||||
val broadcastReceiver = object: BroadcastReceiver() {
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
when (intent?.action) {
|
||||
DownloadManager.ACTION_DOWNLOAD_COMPLETE -> {
|
||||
var thisdlid = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, 0L)
|
||||
|
||||
downloaderCallback("complete", thisdlid)
|
||||
|
||||
Log.d(tag, "DOWNNLAOD COMPELTE $thisdlid")
|
||||
Toast.makeText(this@MainActivity, "Download Completed $thisdlid", Toast.LENGTH_SHORT)
|
||||
}
|
||||
DownloadManager.ACTION_NOTIFICATION_CLICKED -> {
|
||||
var thisdlid = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, 0L)
|
||||
downloaderCallback("clicked", thisdlid)
|
||||
|
||||
Log.d(tag, "CLICKED NOTFIFICAIONT $thisdlid")
|
||||
Toast.makeText(this@MainActivity, "Download CLICKED $thisdlid", Toast.LENGTH_SHORT)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -45,6 +39,7 @@ class MainActivity : BridgeActivity() {
|
||||
|
||||
public override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
Log.d(tag, "onCreate")
|
||||
registerPlugin(MyNativeAudio::class.java)
|
||||
registerPlugin(AudioDownloader::class.java)
|
||||
@@ -57,7 +52,7 @@ class MainActivity : BridgeActivity() {
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
// unregisterReceiver(broadcastReceiver)
|
||||
unregisterReceiver(broadcastReceiver)
|
||||
}
|
||||
|
||||
override fun onPostCreate(savedInstanceState: Bundle?) {
|
||||
@@ -101,4 +96,29 @@ class MainActivity : BridgeActivity() {
|
||||
fun registerBroadcastReceiver(cb: (String, Long) -> Unit) {
|
||||
downloaderCallback = cb
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
storageHelper.onSaveInstanceState(outState)
|
||||
super.onSaveInstanceState(outState)
|
||||
}
|
||||
|
||||
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
|
||||
super.onRestoreInstanceState(savedInstanceState)
|
||||
storageHelper.onRestoreInstanceState(savedInstanceState)
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
// Mandatory for Activity, but not for Fragment & ComponentActivity
|
||||
storageHelper.storage.onActivityResult(requestCode, resultCode, data)
|
||||
}
|
||||
|
||||
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||
Log.d(tag, "onRequestPermissionResult $requestCode")
|
||||
permissions.forEach { Log.d(tag, "PERMISSION $it") }
|
||||
grantResults.forEach { Log.d(tag, "GRANTREUSLTS $it") }
|
||||
// Mandatory for Activity, but not for Fragment & ComponentActivity
|
||||
storageHelper.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user