Support for android 8+, selectable download location, permissions check, recursive delete

This commit is contained in:
advplyr
2021-09-19 18:44:10 -05:00
parent b4ffe0fc83
commit e124d3858f
23 changed files with 798 additions and 387 deletions
+5 -1
View File
@@ -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)
}
}