diff --git a/Server.js b/Server.js index b4cb29a6..1204fc1f 100644 --- a/Server.js +++ b/Server.js @@ -36,7 +36,7 @@ class Server extends EventEmitter { this.user = user this.store.commit('user/setUser', user) if (user) { - this.store.commit('user/setSettings', user.settings) + // this.store.commit('user/setSettings', user.settings) Storage.set({ key: 'token', value: user.token }) } else { Storage.remove({ key: 'token' }) diff --git a/android/app/build.gradle b/android/app/build.gradle index f6810c3f..0c47c826 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -5,13 +5,16 @@ plugins { } android { + kotlinOptions { + freeCompilerArgs = ['-Xjvm-default=all'] + } compileSdkVersion rootProject.ext.compileSdkVersion defaultConfig { applicationId "com.audiobookshelf.app" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 13 - versionName "0.8.4-beta" + versionCode 15 + versionName "0.9.0-beta" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" aaptOptions { // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps. @@ -35,6 +38,7 @@ repositories { } dependencies { + implementation "com.anggrayudi:storage:0.13.0" implementation fileTree(include: ['*.jar'], dir: 'libs') implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion" implementation project(':capacitor-android') diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index f34f52e7..b7dad733 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -6,6 +6,9 @@ + + + + android:usesCleartextTraffic="true" + android:requestLegacyExternalStorage="true"> = 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 = 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 - } + 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 = 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 = _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 { - 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 = 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 = 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 = 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() { } } } - } } diff --git a/android/app/src/main/java/com/audiobookshelf/app/MainActivity.kt b/android/app/src/main/java/com/audiobookshelf/app/MainActivity.kt index b640a3c9..73855bae 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/MainActivity.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/MainActivity.kt @@ -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, 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) + } } diff --git a/android/variables.gradle b/android/variables.gradle index be7273d3..633d13f2 100644 --- a/android/variables.gradle +++ b/android/variables.gradle @@ -1,5 +1,5 @@ ext { - minSdkVersion = 29 + minSdkVersion = 23 compileSdkVersion = 30 targetSdkVersion = 30 androidxActivityVersion = '1.2.0' diff --git a/components/app/Bookshelf.vue b/components/app/Bookshelf.vue index 4db9d3fe..01ff735d 100644 --- a/components/app/Bookshelf.vue +++ b/components/app/Bookshelf.vue @@ -3,12 +3,7 @@