diff --git a/android/app/build.gradle b/android/app/build.gradle index 776f33fa..e4daf1e9 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -22,6 +22,8 @@ kotlin { android { + + namespace 'com.audiobookshelf.app' buildFeatures { viewBinding true } @@ -78,6 +80,7 @@ configurations.all { } dependencies { + implementation "androidx.core:core-splashscreen:$coreSplashScreenVersion" implementation fileTree(include: ['*.jar'], dir: 'libs') implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion" implementation project(':capacitor-android') @@ -121,7 +124,7 @@ dependencies { implementation 'io.github.pilgr:paperdb:2.7.2' // Simple Storage - implementation "com.anggrayudi:storage:0.14.0" + implementation "com.anggrayudi:storage:1.5.4" // OK HTTP implementation 'com.squareup.okhttp3:okhttp:4.9.2' diff --git a/android/app/capacitor.build.gradle b/android/app/capacitor.build.gradle index 87b6e3fb..d138132b 100644 --- a/android/app/capacitor.build.gradle +++ b/android/app/capacitor.build.gradle @@ -2,8 +2,8 @@ android { compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility JavaVersion.VERSION_11 + targetCompatibility JavaVersion.VERSION_11 } } @@ -13,8 +13,8 @@ dependencies { implementation project(':capacitor-dialog') implementation project(':capacitor-haptics') implementation project(':capacitor-network') + implementation project(':capacitor-preferences') implementation project(':capacitor-status-bar') - implementation project(':capacitor-storage') } diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index e847ffee..4738131c 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -2,7 +2,6 @@ diff --git a/android/app/src/main/assets/capacitor.plugins.json b/android/app/src/main/assets/capacitor.plugins.json index 14cdbff5..94cd80a6 100644 --- a/android/app/src/main/assets/capacitor.plugins.json +++ b/android/app/src/main/assets/capacitor.plugins.json @@ -16,11 +16,11 @@ "classpath": "com.capacitorjs.plugins.network.NetworkPlugin" }, { - "pkg": "@capacitor/status-bar", - "classpath": "com.capacitorjs.plugins.statusbar.StatusBarPlugin" + "pkg": "@capacitor/preferences", + "classpath": "com.capacitorjs.plugins.preferences.PreferencesPlugin" }, { - "pkg": "@capacitor/storage", - "classpath": "com.capacitorjs.plugins.storage.StoragePlugin" + "pkg": "@capacitor/status-bar", + "classpath": "com.capacitorjs.plugins.statusbar.StatusBarPlugin" } ] 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 8e2e0bae..f0bf5f18 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/MainActivity.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/MainActivity.kt @@ -13,7 +13,7 @@ import androidx.core.app.ActivityCompat import com.anggrayudi.storage.SimpleStorage import com.anggrayudi.storage.SimpleStorageHelper import com.audiobookshelf.app.data.AbsDatabase -import com.audiobookshelf.app.data.DbManager +import com.audiobookshelf.app.managers.DbManager import com.audiobookshelf.app.player.PlayerNotificationService import com.audiobookshelf.app.plugins.AbsAudioPlayer import com.audiobookshelf.app.plugins.AbsDownloader @@ -51,11 +51,17 @@ class MainActivity : BridgeActivity() { // .detectLeakedClosableObjects() // .penaltyLog() // .build()) + DbManager.initialize(applicationContext) + + registerPlugin(AbsAudioPlayer::class.java) + registerPlugin(AbsDownloader::class.java) + registerPlugin(AbsFileSystem::class.java) + registerPlugin(AbsDatabase::class.java) super.onCreate(savedInstanceState) Log.d(tag, "onCreate") - DbManager.initialize(applicationContext) + val permission = ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) if (permission != PackageManager.PERMISSION_GRANTED) { @@ -63,11 +69,6 @@ class MainActivity : BridgeActivity() { PERMISSIONS_ALL, REQUEST_PERMISSIONS) } - - registerPlugin(AbsAudioPlayer::class.java) - registerPlugin(AbsDownloader::class.java) - registerPlugin(AbsFileSystem::class.java) - registerPlugin(AbsDatabase::class.java) } override fun onDestroy() { diff --git a/android/app/src/main/java/com/audiobookshelf/app/device/DeviceManager.kt b/android/app/src/main/java/com/audiobookshelf/app/device/DeviceManager.kt index ef6b65cd..84a5c6a5 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/device/DeviceManager.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/device/DeviceManager.kt @@ -2,6 +2,7 @@ package com.audiobookshelf.app.device import android.util.Log import com.audiobookshelf.app.data.* +import com.audiobookshelf.app.managers.DbManager import com.audiobookshelf.app.player.PlayerNotificationService interface WidgetEventEmitter { @@ -11,7 +12,7 @@ interface WidgetEventEmitter { object DeviceManager { const val tag = "DeviceManager" - val dbManager:DbManager = DbManager() + val dbManager: DbManager = DbManager() var deviceData:DeviceData = dbManager.getDeviceData() var serverConnectionConfig: ServerConnectionConfig? = null diff --git a/android/app/src/main/java/com/audiobookshelf/app/device/FolderScanner.kt b/android/app/src/main/java/com/audiobookshelf/app/device/FolderScanner.kt index 02e8850b..4cefaa04 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/device/FolderScanner.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/device/FolderScanner.kt @@ -9,6 +9,7 @@ import com.arthenica.ffmpegkit.FFmpegKitConfig import com.arthenica.ffmpegkit.FFprobeKit import com.arthenica.ffmpegkit.Level import com.audiobookshelf.app.data.* +import com.audiobookshelf.app.models.DownloadItem import com.audiobookshelf.app.plugins.AbsDownloader import com.fasterxml.jackson.core.json.JsonReadFeature import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper @@ -83,7 +84,7 @@ class FolderScanner(var ctx: Context) { Log.d(tag, "Folder $${localFolder.name} scan Results: $mediaItemsAdded Added | $mediaItemsUpdated Updated | $mediaItemsRemoved Removed | $mediaItemsUpToDate Up-to-date") return if (mediaItemsAdded > 0 || mediaItemsUpdated > 0 || mediaItemsRemoved > 0) { - var folderLibraryItems = DeviceManager.dbManager.getLocalLibraryItemsInFolder(localFolder.id) // Get all local media items + val folderLibraryItems = DeviceManager.dbManager.getLocalLibraryItemsInFolder(localFolder.id) // Get all local media items FolderScanResult(mediaItemsAdded, mediaItemsUpdated, mediaItemsRemoved, mediaItemsUpToDate, localFolder, folderLibraryItems) } else { Log.d(tag, "No Media Items to save") @@ -91,7 +92,7 @@ class FolderScanner(var ctx: Context) { } } - fun scanLibraryItemFolder(itemFolder:DocumentFile, localFolder:LocalFolder, existingItem:LocalLibraryItem?, forceAudioProbe:Boolean):ItemScanResult { + private fun scanLibraryItemFolder(itemFolder:DocumentFile, localFolder:LocalFolder, existingItem:LocalLibraryItem?, forceAudioProbe:Boolean):ItemScanResult { val itemFolderName = itemFolder.name ?: "" val itemId = getLocalLibraryItemId(itemFolder.id) @@ -219,7 +220,7 @@ class FolderScanner(var ctx: Context) { } // Scan item after download and create local library item - fun scanDownloadItem(downloadItem: AbsDownloader.DownloadItem):DownloadItemScanResult? { + fun scanDownloadItem(downloadItem: DownloadItem):DownloadItemScanResult? { val folderDf = DocumentFileCompat.fromUri(ctx, Uri.parse(downloadItem.localFolder.contentUrl)) val foldersFound = folderDf?.search(false, DocumentFileType.FOLDER) ?: mutableListOf() @@ -268,49 +269,46 @@ class FolderScanner(var ctx: Context) { } } - val audioTracks:MutableList = mutableListOf() + val audioTracks:MutableList = mutableListOf() - filesFound.forEach { docFile -> - val itemPart = downloadItem.downloadItemParts.find { itemPart -> - itemPart.filename == docFile.name - } - if (itemPart == null) { - if (downloadItem.mediaType == "book") { // for books every download item should be a file found - Log.e(tag, "scanDownloadItem: Item part not found for doc file ${docFile.name} | ${docFile.getAbsolutePath(ctx)} | ${docFile.uri}") - } - } else if (itemPart.audioTrack != null) { // Is audio track - val audioTrackFromServer = itemPart.audioTrack - Log.d(tag, "scanDownloadItem: Audio Track from Server index = ${audioTrackFromServer?.index}") - - val localFileId = DeviceManager.getBase64Id(docFile.id) - val localFile = LocalFile(localFileId,docFile.name,docFile.uri.toString(),docFile.getBasePath(ctx),docFile.getAbsolutePath(ctx),docFile.getSimplePath(ctx),docFile.mimeType,docFile.length()) - localLibraryItem.localFiles.add(localFile) - - // TODO: Make asynchronous - val audioProbeResult = probeAudioFile(localFile.absolutePath) - - // Create new audio track - val track = AudioTrack(audioTrackFromServer.index, audioTrackFromServer.startOffset, audioProbeResult?.duration ?: 0.0, localFile.filename ?: "", localFile.contentUrl, localFile.mimeType ?: "", null, true, localFileId, audioProbeResult, audioTrackFromServer.index) - audioTracks.add(track) - - Log.d(tag, "scanDownloadItem: Created Audio Track with index ${track.index} from local file ${localFile.absolutePath}") - - // Add podcast episodes to library - itemPart.episode?.let { podcastEpisode -> - val podcast = localLibraryItem.media as Podcast - val newEpisode = podcast.addEpisode(track, podcastEpisode) - localEpisodeId = newEpisode.id - Log.d(tag, "scanDownloadItem: Added episode to podcast ${podcastEpisode.title} ${track.title} | Track index: ${podcastEpisode.audioTrack?.index}") - } - } else { // Cover image - val localFileId = DeviceManager.getBase64Id(docFile.id) - val localFile = LocalFile(localFileId,docFile.name,docFile.uri.toString(),docFile.getBasePath(ctx),docFile.getAbsolutePath(ctx),docFile.getSimplePath(ctx),docFile.mimeType,docFile.length()) - - localLibraryItem.coverAbsolutePath = localFile.absolutePath - localLibraryItem.coverContentUrl = localFile.contentUrl - localLibraryItem.localFiles.add(localFile) - } + filesFound.forEach { docFile -> + val itemPart = downloadItem.downloadItemParts.find { itemPart -> + itemPart.filename == docFile.name } + if (itemPart == null) { + if (downloadItem.mediaType == "book") { // for books every download item should be a file found + Log.e(tag, "scanDownloadItem: Item part not found for doc file ${docFile.name} | ${docFile.getAbsolutePath(ctx)} | ${docFile.uri}") + } + } else if (itemPart.audioTrack != null) { // Is audio track + val audioTrackFromServer = itemPart.audioTrack + Log.d(tag, "scanDownloadItem: Audio Track from Server index = ${audioTrackFromServer?.index}") + + val localFileId = DeviceManager.getBase64Id(docFile.id) + val localFile = LocalFile(localFileId,docFile.name,docFile.uri.toString(),docFile.getBasePath(ctx),docFile.getAbsolutePath(ctx),docFile.getSimplePath(ctx),docFile.mimeType,docFile.length()) + localLibraryItem.localFiles.add(localFile) + + // Create new audio track + val track = AudioTrack(audioTrackFromServer.index, audioTrackFromServer.startOffset, audioTrackFromServer.duration, localFile.filename ?: "", localFile.contentUrl, localFile.mimeType ?: "", null, true, localFileId, null, audioTrackFromServer.index) + audioTracks.add(track) + + Log.d(tag, "scanDownloadItem: Created Audio Track with index ${track.index} from local file ${localFile.absolutePath}") + + // Add podcast episodes to library + itemPart.episode?.let { podcastEpisode -> + val podcast = localLibraryItem.media as Podcast + val newEpisode = podcast.addEpisode(track, podcastEpisode) + localEpisodeId = newEpisode.id + Log.d(tag, "scanDownloadItem: Added episode to podcast ${podcastEpisode.title} ${track.title} | Track index: ${podcastEpisode.audioTrack?.index}") + } + } else { // Cover image + val localFileId = DeviceManager.getBase64Id(docFile.id) + val localFile = LocalFile(localFileId,docFile.name,docFile.uri.toString(),docFile.getBasePath(ctx),docFile.getAbsolutePath(ctx),docFile.getSimplePath(ctx),docFile.mimeType,docFile.length()) + + localLibraryItem.coverAbsolutePath = localFile.absolutePath + localLibraryItem.coverContentUrl = localFile.contentUrl + localLibraryItem.localFiles.add(localFile) + } + } if (audioTracks.isEmpty()) { Log.d(tag, "scanDownloadItem did not find any audio tracks in folder for ${downloadItem.itemFolderPath}") diff --git a/android/app/src/main/java/com/audiobookshelf/app/data/DbManager.kt b/android/app/src/main/java/com/audiobookshelf/app/managers/DbManager.kt similarity index 85% rename from android/app/src/main/java/com/audiobookshelf/app/data/DbManager.kt rename to android/app/src/main/java/com/audiobookshelf/app/managers/DbManager.kt index fa8ba35b..e0447961 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/data/DbManager.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/managers/DbManager.kt @@ -1,8 +1,9 @@ -package com.audiobookshelf.app.data +package com.audiobookshelf.app.managers import android.content.Context import android.util.Log -import com.audiobookshelf.app.plugins.AbsDownloader +import com.audiobookshelf.app.data.* +import com.audiobookshelf.app.models.DownloadItem import io.paperdb.Paper import java.io.File @@ -23,14 +24,14 @@ class DbManager { fun getDeviceData(): DeviceData { return Paper.book("device").read("data") ?: DeviceData(mutableListOf(), null, null, DeviceSettings.default()) } - fun saveDeviceData(deviceData:DeviceData) { + fun saveDeviceData(deviceData: DeviceData) { Paper.book("device").write("data", deviceData) } fun getLocalLibraryItems(mediaType:String? = null):MutableList { val localLibraryItems:MutableList = mutableListOf() Paper.book("localLibraryItems").allKeys.forEach { - val localLibraryItem:LocalLibraryItem? = Paper.book("localLibraryItems").read(it) + val localLibraryItem: LocalLibraryItem? = Paper.book("localLibraryItems").read(it) if (localLibraryItem != null && (mediaType.isNullOrEmpty() || mediaType == localLibraryItem.mediaType)) { localLibraryItems.add(localLibraryItem) } @@ -45,16 +46,16 @@ class DbManager { } } - fun getLocalLibraryItemByLId(libraryItemId:String):LocalLibraryItem? { + fun getLocalLibraryItemByLId(libraryItemId:String): LocalLibraryItem? { return getLocalLibraryItems().find { it.libraryItemId == libraryItemId } } - fun getLocalLibraryItem(localLibraryItemId:String):LocalLibraryItem? { + fun getLocalLibraryItem(localLibraryItemId:String): LocalLibraryItem? { return Paper.book("localLibraryItems").read(localLibraryItemId) } - fun getLocalLibraryItemWithEpisode(podcastEpisodeId:String):LibraryItemWithEpisode? { - var podcastEpisode:PodcastEpisode? = null + fun getLocalLibraryItemWithEpisode(podcastEpisodeId:String): LibraryItemWithEpisode? { + var podcastEpisode: PodcastEpisode? = null val localLibraryItem = getLocalLibraryItems("podcast").find { localLibraryItem -> val podcast = localLibraryItem.media as Podcast podcastEpisode = podcast.episodes?.find { it.id == podcastEpisodeId } @@ -77,15 +78,15 @@ class DbManager { } } - fun saveLocalLibraryItem(localLibraryItem:LocalLibraryItem) { + fun saveLocalLibraryItem(localLibraryItem: LocalLibraryItem) { Paper.book("localLibraryItems").write(localLibraryItem.id, localLibraryItem) } - fun saveLocalFolder(localFolder:LocalFolder) { + fun saveLocalFolder(localFolder: LocalFolder) { Paper.book("localFolders").write(localFolder.id,localFolder) } - fun getLocalFolder(folderId:String):LocalFolder? { + fun getLocalFolder(folderId:String): LocalFolder? { return Paper.book("localFolders").read(folderId) } @@ -107,7 +108,7 @@ class DbManager { Paper.book("localFolders").delete(folderId) } - fun saveDownloadItem(downloadItem: AbsDownloader.DownloadItem) { + fun saveDownloadItem(downloadItem: DownloadItem) { Paper.book("downloadItems").write(downloadItem.id, downloadItem) } @@ -115,21 +116,21 @@ class DbManager { Paper.book("downloadItems").delete(downloadItemId) } - fun getDownloadItems():List { - val downloadItems:MutableList = mutableListOf() + fun getDownloadItems():List { + val downloadItems:MutableList = mutableListOf() Paper.book("downloadItems").allKeys.forEach { downloadItemId -> - Paper.book("downloadItems").read(downloadItemId)?.let { + Paper.book("downloadItems").read(downloadItemId)?.let { downloadItems.add(it) } } return downloadItems } - fun saveLocalMediaProgress(mediaProgress:LocalMediaProgress) { + fun saveLocalMediaProgress(mediaProgress: LocalMediaProgress) { Paper.book("localMediaProgress").write(mediaProgress.id,mediaProgress) } // For books this will just be the localLibraryItemId for podcast episodes this will be "{localLibraryItemId}-{episodeId}" - fun getLocalMediaProgress(localMediaProgressId:String):LocalMediaProgress? { + fun getLocalMediaProgress(localMediaProgressId:String): LocalMediaProgress? { return Paper.book("localMediaProgress").read(localMediaProgressId) } fun getAllLocalMediaProgress():List { @@ -236,18 +237,18 @@ class DbManager { } } - fun saveLocalPlaybackSession(playbackSession:PlaybackSession) { + fun saveLocalPlaybackSession(playbackSession: PlaybackSession) { Paper.book("localPlaybackSession").write(playbackSession.id,playbackSession) } - fun getLocalPlaybackSession(playbackSessionId:String):PlaybackSession? { + fun getLocalPlaybackSession(playbackSessionId:String): PlaybackSession? { return Paper.book("localPlaybackSession").read(playbackSessionId) } - fun saveMediaItemHistory(mediaItemHistory:MediaItemHistory) { + fun saveMediaItemHistory(mediaItemHistory: MediaItemHistory) { Paper.book("mediaItemHistory").write(mediaItemHistory.id,mediaItemHistory) } - fun getMediaItemHistory(id:String):MediaItemHistory? { + fun getMediaItemHistory(id:String): MediaItemHistory? { return Paper.book("mediaItemHistory").read(id) } } diff --git a/android/app/src/main/java/com/audiobookshelf/app/managers/DownloadItemManager.kt b/android/app/src/main/java/com/audiobookshelf/app/managers/DownloadItemManager.kt new file mode 100644 index 00000000..f51cbece --- /dev/null +++ b/android/app/src/main/java/com/audiobookshelf/app/managers/DownloadItemManager.kt @@ -0,0 +1,234 @@ +package com.audiobookshelf.app.managers + +import android.app.DownloadManager +import android.net.Uri +import android.util.Log +import androidx.documentfile.provider.DocumentFile +import com.anggrayudi.storage.callback.FileCallback +import com.anggrayudi.storage.file.DocumentFileCompat +import com.anggrayudi.storage.file.MimeType +import com.anggrayudi.storage.file.getAbsolutePath +import com.anggrayudi.storage.file.moveFileTo +import com.anggrayudi.storage.media.FileDescription +import com.audiobookshelf.app.MainActivity +import com.audiobookshelf.app.device.DeviceManager +import com.audiobookshelf.app.device.FolderScanner +import com.audiobookshelf.app.models.DownloadItem +import com.audiobookshelf.app.models.DownloadItemPart +import com.fasterxml.jackson.core.json.JsonReadFeature +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import com.getcapacitor.JSObject +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +class DownloadItemManager(var downloadManager:DownloadManager, var folderScanner: FolderScanner, var mainActivity: MainActivity, var clientEventEmitter:DownloadEventEmitter) { + val tag = "DownloadItemManager" + private val maxSimultaneousDownloads = 5 + private var jacksonMapper = jacksonObjectMapper().enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature()) + + enum class DownloadCheckStatus { + InProgress, + Successful, + Failed + } + + var downloadItemQueue: MutableList = mutableListOf() + var currentDownloadItemParts: MutableList = mutableListOf() + + interface DownloadEventEmitter { + fun onDownloadItem(downloadItem:DownloadItem) + fun onDownloadItemPartUpdate(downloadItemPart:DownloadItemPart) + fun onDownloadItemComplete(jsobj:JSObject) + } + + companion object { + var isDownloading:Boolean = false + } + + fun addDownloadItem(downloadItem:DownloadItem) { + DeviceManager.dbManager.saveDownloadItem(downloadItem) + Log.i(tag, "Add download item ${downloadItem.media.metadata.title}") + + downloadItemQueue.add(downloadItem) + clientEventEmitter.onDownloadItem(downloadItem) + checkUpdateDownloadQueue() + } + + private fun checkUpdateDownloadQueue() { + for (downloadItem in downloadItemQueue) { + val numPartsToGet = maxSimultaneousDownloads - currentDownloadItemParts.size + val nextDownloadItemParts = downloadItem.getNextDownloadItemParts(numPartsToGet) + Log.d(tag, "checkUpdateDownloadQueue: numPartsToGet=$numPartsToGet, nextDownloadItemParts=${nextDownloadItemParts.size}") + + if (nextDownloadItemParts.size > 0) { + nextDownloadItemParts.forEach { + val dlRequest = it.getDownloadRequest() + val downloadId = downloadManager.enqueue(dlRequest) + it.downloadId = downloadId + Log.d(tag, "checkUpdateDownloadQueue: Starting download item part, downloadId=$downloadId") + currentDownloadItemParts.add(it) + } + } + + if (currentDownloadItemParts.size >= maxSimultaneousDownloads) { + break + } + } + + if (currentDownloadItemParts.size > 0) startWatchingDownloads() + } + + private fun startWatchingDownloads() { + if (isDownloading) return // Already watching + + GlobalScope.launch(Dispatchers.IO) { + Log.d(tag, "Starting watching downloads") + isDownloading = true + + while (currentDownloadItemParts.size > 0) { + + val itemParts = currentDownloadItemParts.filter { !it.isMoving }.map { it } + for (downloadItemPart in itemParts) { + val downloadCheckStatus = checkDownloadItemPart(downloadItemPart) + clientEventEmitter.onDownloadItemPartUpdate(downloadItemPart) + + // Will move to final destination, remove current item parts, and check if download item is finished + handleDownloadItemPartCheck(downloadCheckStatus, downloadItemPart) + } + + if (currentDownloadItemParts.size < maxSimultaneousDownloads) { + checkUpdateDownloadQueue() + } + + delay(500) + } + + Log.d(tag, "Finished watching downloads") + isDownloading = false + } + } + + private fun checkDownloadItemPart(downloadItemPart:DownloadItemPart):DownloadCheckStatus { + val downloadId = downloadItemPart.downloadId ?: return DownloadCheckStatus.Failed + + val query = DownloadManager.Query().setFilterById(downloadId) + downloadManager.query(query).use { + if (it.moveToFirst()) { + val bytesColumnIndex = it.getColumnIndex(DownloadManager.COLUMN_TOTAL_SIZE_BYTES) + val statusColumnIndex = it.getColumnIndex(DownloadManager.COLUMN_STATUS) + val bytesDownloadedColumnIndex = it.getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR) + + val totalBytes = if (bytesColumnIndex >= 0) it.getInt(bytesColumnIndex) else 0 + val downloadStatus = if (statusColumnIndex >= 0) it.getInt(statusColumnIndex) else 0 + val bytesDownloadedSoFar = if (bytesDownloadedColumnIndex >= 0) it.getInt(bytesDownloadedColumnIndex) else 0 + Log.d(tag, "checkDownloads Download ${downloadItemPart.filename} bytes $totalBytes | bytes dled $bytesDownloadedSoFar | downloadStatus $downloadStatus") + + if (downloadStatus == DownloadManager.STATUS_SUCCESSFUL) { + Log.d(tag, "checkDownloads Download ${downloadItemPart.filename} Successful") + downloadItemPart.completed = true + return DownloadCheckStatus.Successful + } else if (downloadStatus == DownloadManager.STATUS_FAILED) { + Log.d(tag, "checkDownloads Download ${downloadItemPart.filename} Failed") + downloadItemPart.completed = true + downloadItemPart.failed = true + return DownloadCheckStatus.Failed + } else { + //update progress + val percentProgress = if (totalBytes > 0) ((bytesDownloadedSoFar * 100L) / totalBytes) else 0 + Log.d(tag, "checkDownloads Download ${downloadItemPart.filename} Progress = $percentProgress%") + downloadItemPart.progress = percentProgress + return DownloadCheckStatus.InProgress + } + } else { + Log.d(tag, "Download ${downloadItemPart.filename} not found in dlmanager") + downloadItemPart.completed = true + downloadItemPart.failed = true + return DownloadCheckStatus.Failed + } + } + } + + private fun handleDownloadItemPartCheck(downloadCheckStatus:DownloadCheckStatus, downloadItemPart:DownloadItemPart) { + val downloadItem = downloadItemQueue.find { it.id == downloadItemPart.downloadItemId } + if (downloadItem == null) { + Log.e(tag, "Download item part finished but download item not found ${downloadItemPart.filename}") + currentDownloadItemParts.remove(downloadItemPart) + } else if (downloadCheckStatus == DownloadCheckStatus.Successful) { + + val file = DocumentFileCompat.fromUri(mainActivity, downloadItemPart.destinationUri) + Log.d(tag, "DOWNLOAD: DESTINATION URI ${downloadItemPart.destinationUri}") + + val fcb = object : FileCallback() { + override fun onPrepare() { + Log.d(tag, "DOWNLOAD: PREPARING MOVE FILE") + } + override fun onFailed(errorCode: ErrorCode) { + Log.e(tag, "DOWNLOAD: FAILED TO MOVE FILE $errorCode") + downloadItemPart.failed = true + downloadItemPart.isMoving = false + file?.delete() + checkDownloadItemFinished(downloadItem) + currentDownloadItemParts.remove(downloadItemPart) + } + override fun onCompleted(result:Any) { + Log.d(tag, "DOWNLOAD: FILE MOVE COMPLETED") + val resultDocFile = result as DocumentFile + Log.d(tag, "DOWNLOAD: COMPLETED FILE INFO ${resultDocFile.getAbsolutePath(mainActivity)}") + + // Rename to fix appended .mp4 on m4b files + // REF: https://github.com/anggrayudi/SimpleStorage/issues/94 + resultDocFile.renameTo(downloadItemPart.filename) + + downloadItemPart.moved = true + downloadItemPart.isMoving = false + checkDownloadItemFinished(downloadItem) + currentDownloadItemParts.remove(downloadItemPart) + } + } + + val localFolderFile = DocumentFileCompat.fromUri(mainActivity, Uri.parse(downloadItemPart.localFolderUrl)) + if (localFolderFile == null) { + // fAILED + downloadItemPart.failed = true + Log.e(tag, "Local Folder File from uri is null") + checkDownloadItemFinished(downloadItem) + currentDownloadItemParts.remove(downloadItemPart) + } else { + downloadItemPart.isMoving = true + val mimetype = if (downloadItemPart.audioTrack != null) MimeType.AUDIO else MimeType.IMAGE + val fileDescription = FileDescription(downloadItemPart.filename, downloadItemPart.itemTitle, mimetype) + file?.moveFileTo(mainActivity, localFolderFile, fileDescription, fcb) + } + + } else if (downloadCheckStatus != DownloadCheckStatus.InProgress) { + checkDownloadItemFinished(downloadItem) + currentDownloadItemParts.remove(downloadItemPart) + } + } + + private fun checkDownloadItemFinished(downloadItem:DownloadItem) { + if (downloadItem.isDownloadFinished) { + Log.i(tag, "Download Item finished ${downloadItem.media.metadata.title}") + + val downloadItemScanResult = folderScanner.scanDownloadItem(downloadItem) + Log.d(tag, "Item download complete ${downloadItem.itemTitle} | local library item id: ${downloadItemScanResult?.localLibraryItem?.id}") + + val jsobj = JSObject() + jsobj.put("libraryItemId", downloadItem.id) + jsobj.put("localFolderId", downloadItem.localFolder.id) + + downloadItemScanResult?.localLibraryItem?.let { localLibraryItem -> + jsobj.put("localLibraryItem", JSObject(jacksonMapper.writeValueAsString(localLibraryItem))) + } + downloadItemScanResult?.localMediaProgress?.let { localMediaProgress -> + jsobj.put("localMediaProgress", JSObject(jacksonMapper.writeValueAsString(localMediaProgress))) + } + + clientEventEmitter.onDownloadItemComplete(jsobj) + downloadItemQueue.remove(downloadItem) + DeviceManager.dbManager.removeDownloadItem(downloadItem.id) + } + } +} diff --git a/android/app/src/main/java/com/audiobookshelf/app/models/DownloadItem.kt b/android/app/src/main/java/com/audiobookshelf/app/models/DownloadItem.kt new file mode 100644 index 00000000..dd1812dd --- /dev/null +++ b/android/app/src/main/java/com/audiobookshelf/app/models/DownloadItem.kt @@ -0,0 +1,47 @@ +package com.audiobookshelf.app.models + +import com.audiobookshelf.app.data.LocalFolder +import com.audiobookshelf.app.data.MediaProgress +import com.audiobookshelf.app.data.MediaType +import com.fasterxml.jackson.annotation.JsonIgnore + +data class DownloadItem( + val id: String, + val libraryItemId:String, + val episodeId:String?, + val userMediaProgress: MediaProgress?, + val serverConnectionConfigId:String, + val serverAddress:String, + val serverUserId:String, + val mediaType: String, + val itemFolderPath:String, + val localFolder: LocalFolder, + val itemTitle: String, + val media: MediaType, + val downloadItemParts: MutableList +) { + @get:JsonIgnore + val isDownloadFinished get() = !downloadItemParts.any { !it.completed || it.isMoving } + + @JsonIgnore + fun getTotalFileSize(): Long { + var totalSize = 0L + downloadItemParts.forEach { totalSize += it.fileSize } + return totalSize + } + + @JsonIgnore + fun getNextDownloadItemParts(limit:Int): MutableList { + val itemParts = mutableListOf() + if (limit == 0) return itemParts + + for (it in downloadItemParts) { + if (!it.completed && it.downloadId == null) { + itemParts.add(it) + if (itemParts.size > limit) break + } + } + + return itemParts + } +} diff --git a/android/app/src/main/java/com/audiobookshelf/app/models/DownloadItemPart.kt b/android/app/src/main/java/com/audiobookshelf/app/models/DownloadItemPart.kt new file mode 100644 index 00000000..f1872e2a --- /dev/null +++ b/android/app/src/main/java/com/audiobookshelf/app/models/DownloadItemPart.kt @@ -0,0 +1,82 @@ +package com.audiobookshelf.app.models + +import android.app.DownloadManager +import android.net.Uri +import android.os.Environment +import android.util.Log +import com.audiobookshelf.app.data.AudioTrack +import com.audiobookshelf.app.data.LocalFolder +import com.audiobookshelf.app.data.PodcastEpisode +import com.audiobookshelf.app.device.DeviceManager +import com.fasterxml.jackson.annotation.JsonIgnore +import java.io.File + +data class DownloadItemPart( + val id: String, + val downloadItemId: String, + val filename: String, + val finalDestinationPath:String, + val itemTitle: String, + val serverPath: String, + val localFolderName: String, + val localFolderUrl: String, + val localFolderId: String, + val audioTrack: AudioTrack?, + val episode: PodcastEpisode?, + var completed:Boolean, + var moved:Boolean, + var isMoving:Boolean, + var failed:Boolean, + @JsonIgnore val uri: Uri, + @JsonIgnore val destinationUri: Uri, + @JsonIgnore val finalDestinationUri: Uri, + var downloadId: Long?, + var progress: Long +) { + companion object { + fun make(downloadItemId:String, filename:String, destinationFile: File, finalDestinationFile: File, itemTitle:String, serverPath:String, localFolder: LocalFolder, audioTrack: AudioTrack?, episode: PodcastEpisode?) :DownloadItemPart { + val destinationUri = Uri.fromFile(destinationFile) + val finalDestinationUri = Uri.fromFile(finalDestinationFile) + + var downloadUrl = "${DeviceManager.serverAddress}${serverPath}?token=${DeviceManager.token}" + if (serverPath.endsWith("/cover")) downloadUrl += "&format=jpeg" // For cover images force to jpeg + val downloadUri = Uri.parse(downloadUrl) + Log.d("DownloadItemPart", "Audio File Destination Uri: $destinationUri | Final Destination Uri: $finalDestinationUri | Download URI $downloadUri") + return DownloadItemPart( + id = DeviceManager.getBase64Id(finalDestinationFile.absolutePath), + downloadItemId, + filename = filename, + finalDestinationPath = finalDestinationFile.absolutePath, + itemTitle = itemTitle, + serverPath = serverPath, + localFolderName = localFolder.name, + localFolderUrl = localFolder.contentUrl, + localFolderId = localFolder.id, + audioTrack = audioTrack, + episode = episode, + completed = false, + moved = false, + isMoving = false, + failed = false, + uri = downloadUri, + destinationUri = destinationUri, + finalDestinationUri = finalDestinationUri, + downloadId = null, + progress = 0 + ) + } + } + + @get:JsonIgnore + val fileSize get() = audioTrack?.metadata?.size ?: 0 + + @JsonIgnore + fun getDownloadRequest(): DownloadManager.Request { + val dlRequest = DownloadManager.Request(uri) + dlRequest.setTitle(filename) + dlRequest.setDescription("Downloading to $localFolderName for book $itemTitle") + dlRequest.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE) + dlRequest.setDestinationUri(destinationUri) + return dlRequest + } +} diff --git a/android/app/src/main/java/com/audiobookshelf/app/player/PlayerNotificationService.kt b/android/app/src/main/java/com/audiobookshelf/app/player/PlayerNotificationService.kt index 1975fd66..b8dc19e7 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/player/PlayerNotificationService.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/player/PlayerNotificationService.kt @@ -31,6 +31,7 @@ import com.audiobookshelf.app.R import com.audiobookshelf.app.data.* import com.audiobookshelf.app.data.DeviceInfo import com.audiobookshelf.app.device.DeviceManager +import com.audiobookshelf.app.managers.DbManager import com.audiobookshelf.app.media.MediaManager import com.audiobookshelf.app.server.ApiHandler import com.fasterxml.jackson.annotation.JsonIgnore @@ -49,8 +50,8 @@ import kotlin.concurrent.schedule const val SLEEP_TIMER_WAKE_UP_EXPIRATION = 120000L // 2m -const val PLAYER_CAST = "cast-player"; -const val PLAYER_EXO = "exo-player"; +const val PLAYER_CAST = "cast-player" +const val PLAYER_EXO = "exo-player" class PlayerNotificationService : MediaBrowserServiceCompat() { @@ -61,6 +62,8 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { var isSwitchingPlayer = false // Used when switching between cast player and exoplayer } + private val tag = "PlayerNotificationService" + interface ClientEventEmitter { fun onPlaybackSession(playbackSession:PlaybackSession) fun onPlaybackClosed() @@ -76,8 +79,6 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { fun onNetworkMeteredChanged(isUnmetered:Boolean) fun onMediaItemHistoryUpdated(mediaItemHistory:MediaItemHistory) } - - private val tag = "PlayerService" private val binder = LocalBinder() var clientEventEmitter:ClientEventEmitter? = null diff --git a/android/app/src/main/java/com/audiobookshelf/app/plugins/AbsDownloader.kt b/android/app/src/main/java/com/audiobookshelf/app/plugins/AbsDownloader.kt index 78c45d3f..9c501b90 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/plugins/AbsDownloader.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/plugins/AbsDownloader.kt @@ -10,12 +10,15 @@ import androidx.documentfile.provider.DocumentFile import com.anggrayudi.storage.callback.FileCallback import com.anggrayudi.storage.file.* import com.anggrayudi.storage.media.FileDescription +import com.anggrayudi.storage.media.MediaStoreCompat import com.audiobookshelf.app.MainActivity import com.audiobookshelf.app.data.* import com.audiobookshelf.app.device.DeviceManager import com.audiobookshelf.app.device.FolderScanner +import com.audiobookshelf.app.models.DownloadItem +import com.audiobookshelf.app.models.DownloadItemPart import com.audiobookshelf.app.server.ApiHandler -import com.fasterxml.jackson.annotation.JsonIgnore +import com.audiobookshelf.app.managers.DownloadItemManager import com.fasterxml.jackson.core.json.JsonReadFeature import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.getcapacitor.JSObject @@ -32,99 +35,34 @@ import java.io.File @CapacitorPlugin(name = "AbsDownloader") class AbsDownloader : Plugin() { private val tag = "AbsDownloader" - var jacksonMapper = jacksonObjectMapper().enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature()) + private var jacksonMapper = jacksonObjectMapper().enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature()) lateinit var mainActivity: MainActivity lateinit var downloadManager: DownloadManager lateinit var apiHandler: ApiHandler lateinit var folderScanner: FolderScanner + lateinit var downloadItemManager: DownloadItemManager - data class DownloadItemPart( - val id: String, - val filename: String, - val finalDestinationPath:String, - val itemTitle: String, - val serverPath: String, - val localFolderName: String, - val localFolderUrl: String, - val localFolderId: String, - val audioTrack: AudioTrack?, - val episode:PodcastEpisode?, - var completed:Boolean, - var moved:Boolean, - var failed:Boolean, - @JsonIgnore val uri: Uri, - @JsonIgnore val destinationUri: Uri, - @JsonIgnore val finalDestinationUri: Uri, - var downloadId: Long?, - var progress: Long - ) { - companion object { - fun make(filename:String, destinationFile:File, finalDestinationFile:File, itemTitle:String, serverPath:String, localFolder:LocalFolder, audioTrack:AudioTrack?, episode:PodcastEpisode?) :DownloadItemPart { - val destinationUri = Uri.fromFile(destinationFile) - val finalDestinationUri = Uri.fromFile(finalDestinationFile) + private var downloadQueue: MutableList = mutableListOf() - var downloadUrl = "${DeviceManager.serverAddress}${serverPath}?token=${DeviceManager.token}" - if (serverPath.endsWith("/cover")) downloadUrl += "&format=jpeg" // For cover images force to jpeg - val downloadUri = Uri.parse(downloadUrl) - Log.d("DownloadItemPart", "Audio File Destination Uri: $destinationUri | Final Destination Uri: $finalDestinationUri | Download URI $downloadUri") - return DownloadItemPart( - id = DeviceManager.getBase64Id(finalDestinationFile.absolutePath), - filename = filename, finalDestinationFile.absolutePath, - itemTitle = itemTitle, - serverPath = serverPath, - localFolderName = localFolder.name, - localFolderUrl = localFolder.contentUrl, - localFolderId = localFolder.id, - audioTrack = audioTrack, - episode = episode, - completed = false, - moved = false, - failed = false, - uri = downloadUri, - destinationUri = destinationUri, - finalDestinationUri = finalDestinationUri, - downloadId = null, - progress = 0 - ) - } + val clientEventEmitter = (object : DownloadItemManager.DownloadEventEmitter { + override fun onDownloadItem(downloadItem:DownloadItem) { + notifyListeners("onDownloadItem", JSObject(jacksonMapper.writeValueAsString(downloadItem))) } - - @JsonIgnore - fun getDownloadRequest(): DownloadManager.Request { - val dlRequest = DownloadManager.Request(uri) - dlRequest.setTitle(filename) - dlRequest.setDescription("Downloading to $localFolderName for book $itemTitle") - dlRequest.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE) - dlRequest.setDestinationUri(destinationUri) - return dlRequest + override fun onDownloadItemPartUpdate(downloadItemPart:DownloadItemPart) { + notifyListeners("onDownloadItemPartUpdate", JSObject(jacksonMapper.writeValueAsString(downloadItemPart))) } - } - - data class DownloadItem( - val id: String, - val libraryItemId:String, - val episodeId:String?, - val userMediaProgress:MediaProgress?, - val serverConnectionConfigId:String, - val serverAddress:String, - val serverUserId:String, - val mediaType: String, - val itemFolderPath:String, - val localFolder: LocalFolder, - val itemTitle: String, - val media:MediaType, - val downloadItemParts: MutableList - ) - - var downloadQueue: MutableList = mutableListOf() + override fun onDownloadItemComplete(jsobj:JSObject) { + notifyListeners("onItemDownloadComplete", jsobj) + } + }) override fun load() { mainActivity = (activity as MainActivity) downloadManager = activity.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager folderScanner = FolderScanner(mainActivity) apiHandler = ApiHandler(mainActivity) - + downloadItemManager = DownloadItemManager(downloadManager, folderScanner, mainActivity, clientEventEmitter) Log.d(tag, "Build SDK ${Build.VERSION.SDK_INT}") } @@ -204,13 +142,16 @@ class AbsDownloader : Plugin() { private fun startLibraryItemDownload(libraryItem: LibraryItem, localFolder: LocalFolder, episode:PodcastEpisode?) { val tempFolderPath = mainActivity.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS) +// val tempFolderPath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) + + Log.d(tag, "downloadCacheDirectory=$tempFolderPath") if (libraryItem.mediaType == "book") { val bookTitle = cleanStringForFileSystem(libraryItem.media.metadata.title) val tracks = libraryItem.media.getAudioTracks() Log.d(tag, "Starting library item download with ${tracks.size} tracks") - val itemFolderPath = localFolder.absolutePath + "/" + bookTitle + val itemFolderPath = "${localFolder.absolutePath}/$bookTitle" val downloadItem = DownloadItem(libraryItem.id, libraryItem.id, null, libraryItem.userMediaProgress,DeviceManager.serverConnectionConfig?.id ?: "", DeviceManager.serverAddress, DeviceManager.serverUserId, libraryItem.mediaType, itemFolderPath, localFolder, bookTitle, libraryItem.media, mutableListOf()) // Create download item part for each audio track @@ -222,43 +163,54 @@ class AbsDownloader : Plugin() { val finalDestinationFile = File("$itemFolderPath/$destinationFilename") val destinationFile = File("$tempFolderPath/$destinationFilename") + if (destinationFile.exists()) { + Log.d(tag, "TEMP Audio file already exists, removing it from ${destinationFile.absolutePath}") + destinationFile.delete() + } + if (finalDestinationFile.exists()) { Log.d(tag, "Audio file already exists, removing it from ${finalDestinationFile.absolutePath}") finalDestinationFile.delete() } - val downloadItemPart = DownloadItemPart.make(destinationFilename,destinationFile,finalDestinationFile,bookTitle,serverPath,localFolder,audioTrack,null) + val downloadItemPart = DownloadItemPart.make(downloadItem.id, destinationFilename,destinationFile,finalDestinationFile,bookTitle,serverPath,localFolder,audioTrack,null) downloadItem.downloadItemParts.add(downloadItemPart) - val dlRequest = downloadItemPart.getDownloadRequest() - val downloadId = downloadManager.enqueue(dlRequest) - downloadItemPart.downloadId = downloadId +// val dlRequest = downloadItemPart.getDownloadRequest() +// val downloadId = downloadManager.enqueue(dlRequest) +// downloadItemPart.downloadId = downloadId } if (downloadItem.downloadItemParts.isNotEmpty()) { // Add cover download item if (libraryItem.media.coverPath != null && libraryItem.media.coverPath?.isNotEmpty() == true) { val serverPath = "/api/items/${libraryItem.id}/cover" - val destinationFilename = "cover.jpg" + val destinationFilename = "cover-${libraryItem.id}.jpg" val destinationFile = File("$tempFolderPath/$destinationFilename") val finalDestinationFile = File("$itemFolderPath/$destinationFilename") + if (destinationFile.exists()) { + Log.d(tag, "TEMP Audio file already exists, removing it from ${destinationFile.absolutePath}") + destinationFile.delete() + } + if (finalDestinationFile.exists()) { Log.d(tag, "Cover already exists, removing it from ${finalDestinationFile.absolutePath}") finalDestinationFile.delete() } - val downloadItemPart = DownloadItemPart.make(destinationFilename,destinationFile,finalDestinationFile,bookTitle,serverPath,localFolder,null,null) + val downloadItemPart = DownloadItemPart.make(downloadItem.id, destinationFilename,destinationFile,finalDestinationFile,bookTitle,serverPath,localFolder,null,null) downloadItem.downloadItemParts.add(downloadItemPart) - val dlRequest = downloadItemPart.getDownloadRequest() - val downloadId = downloadManager.enqueue(dlRequest) - downloadItemPart.downloadId = downloadId +// val dlRequest = downloadItemPart.getDownloadRequest() +// val downloadId = downloadManager.enqueue(dlRequest) +// downloadItemPart.downloadId = downloadId } - downloadQueue.add(downloadItem) - startWatchingDownloads(downloadItem) - DeviceManager.dbManager.saveDownloadItem(downloadItem) +// downloadQueue.add(downloadItem) +// startWatchingDownloads(downloadItem) +// DeviceManager.dbManager.saveDownloadItem(downloadItem) + downloadItemManager.addDownloadItem(downloadItem) } } else { // Podcast episode download @@ -281,7 +233,7 @@ class AbsDownloader : Plugin() { finalDestinationFile.delete() } - var downloadItemPart = DownloadItemPart.make(destinationFilename,destinationFile,finalDestinationFile,podcastTitle,serverPath,localFolder,audioTrack,episode) + var downloadItemPart = DownloadItemPart.make(downloadItem.id, destinationFilename,destinationFile,finalDestinationFile,podcastTitle,serverPath,localFolder,audioTrack,episode) downloadItem.downloadItemParts.add(downloadItemPart) var dlRequest = downloadItemPart.getDownloadRequest() @@ -298,7 +250,7 @@ class AbsDownloader : Plugin() { if (finalDestinationFile.exists()) { Log.d(tag, "Podcast cover already exists - not downloading cover again") } else { - downloadItemPart = DownloadItemPart.make(destinationFilename,destinationFile,finalDestinationFile,podcastTitle,serverPath,localFolder,null,null) + downloadItemPart = DownloadItemPart.make(downloadItem.id, destinationFilename,destinationFile,finalDestinationFile,podcastTitle,serverPath,localFolder,null,null) downloadItem.downloadItemParts.add(downloadItemPart) dlRequest = downloadItemPart.getDownloadRequest() diff --git a/android/app/src/main/java/com/audiobookshelf/app/plugins/AbsFileSystem.kt b/android/app/src/main/java/com/audiobookshelf/app/plugins/AbsFileSystem.kt index 3d58cb41..c8219738 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/plugins/AbsFileSystem.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/plugins/AbsFileSystem.kt @@ -85,21 +85,28 @@ class AbsFileSystem : Plugin() { call.resolve(JSObject(jacksonMapper.writeValueAsString(localFolder))) } - override fun onStorageAccessDenied(requestCode: Int, folder: DocumentFile?, storageType: StorageType) { + override fun onStorageAccessDenied( + requestCode: Int, + folder: DocumentFile?, + storageType: StorageType, + storageId: String + ) { + Log.e(tag, "Storage Access Denied ${folder?.getAbsolutePath(mainActivity)}") + val jsobj = JSObject() if (requestCode == REQUEST_CODE_SELECT_FOLDER) { val builder: AlertDialog.Builder = AlertDialog.Builder(mainActivity) builder.setMessage( "You have no write access to this storage, thus selecting this folder is useless." + - "\nWould you like to grant access to this folder?") + "\nWould you like to grant access to this folder?") builder.setNegativeButton("Dont Allow") { _, _ -> run { jsobj.put("error", "User Canceled, Access Denied") call.resolve(jsobj) } } - builder.setPositiveButton("Allow.") { _, _ -> mainActivity.storageHelper.requestStorageAccess(REQUEST_CODE_SDCARD_ACCESS, storageType) } + builder.setPositiveButton("Allow.") { _, _ -> mainActivity.storageHelper.requestStorageAccess(REQUEST_CODE_SDCARD_ACCESS, initialPath = FileFullPath(mainActivity, storageId, "")) } builder.show() } else { Log.d(TAG, "STORAGE ACCESS DENIED $requestCode") diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml index da1cf625..713e8445 100644 --- a/android/app/src/main/res/values/styles.xml +++ b/android/app/src/main/res/values/styles.xml @@ -9,7 +9,7 @@ @color/colorAccent - -