From 96fdd52ef4c23cdd09a7bf7757c506e1054713af Mon Sep 17 00:00:00 2001 From: shabinder Date: Fri, 14 May 2021 00:47:54 +0530 Subject: [PATCH 1/2] (WIP)Android SAF --- android/build.gradle.kts | 8 +- .../main/java/com/shabinder/spotiflyer/App.kt | 21 ++ .../com/shabinder/spotiflyer/MainActivity.kt | 167 ++++++--- .../spotiflyer/ui/PermissionDialog.kt | 60 +++- android/src/main/res/values/res.xml | 62 ---- .../common/uikit/SpotiFlyerMainUi.kt | 21 +- common/data-models/build.gradle.kts | 6 + .../AndroidMediaFile.kt | 8 + .../AndroidPlatformActions.kt | 4 +- .../SpotiFlyerBaseDir.kt | 18 + .../shabinder/common/models/DownloadObject.kt | 4 +- .../com/shabinder/common/models/File.kt | 3 + .../common/models/DesktopMediaFile.kt | 3 + .../IOSMediaFile.kt | 5 + .../WebMediaFile.kt | 5 + common/dependency-injection/build.gradle.kts | 1 - .../com/shabinder/common/di/AndroidDir.kt | 329 +++++++++++++++--- .../com/shabinder/common/di/AudioTagging.kt | 10 +- .../common/di/worker/ForegroundService.kt | 28 +- .../com/shabinder/common/di/worker/Utils.kt | 47 +++ .../kotlin/com/shabinder/common/di/DI.kt | 9 +- .../kotlin/com/shabinder/common/di/Dir.kt | 41 ++- .../common/di/FetchPlatformQueryResult.kt | 5 +- .../common/di/providers/GaanaProvider.kt | 9 +- .../common/di/providers/SpotifyProvider.kt | 20 +- .../di/{ => providers}/YoutubeProvider.kt | 22 +- .../com/shabinder/common/di/utils/Utils.kt | 1 + .../com/shabinder/common/di/DesktopActual.kt | 1 + .../com/shabinder/common/di/DesktopDir.kt | 7 + .../com.shabinder.common.di/IOSActual.kt | 1 + .../kotlin/com.shabinder.common.di/IOSDir.kt | 8 +- .../kotlin/com/shabinder/common/di/WebDir.kt | 9 + .../shabinder/common/list/SpotiFlyerList.kt | 5 + .../shabinder/common/main/SpotiFlyerMain.kt | 8 +- .../main/integration/SpotiFlyerMainImpl.kt | 8 +- .../shabinder/common/root/SpotiFlyerRoot.kt | 8 + .../root/integration/SpotiFlyerRootImpl.kt | 22 +- 37 files changed, 716 insertions(+), 278 deletions(-) delete mode 100644 android/src/main/res/values/res.xml create mode 100644 common/data-models/src/androidMain/kotlin/com.shabinder.common.models/AndroidMediaFile.kt create mode 100644 common/data-models/src/androidMain/kotlin/com.shabinder.common.models/SpotiFlyerBaseDir.kt create mode 100644 common/data-models/src/commonMain/kotlin/com/shabinder/common/models/File.kt create mode 100644 common/data-models/src/desktopMain/kotlin/com/shabinder/common/models/DesktopMediaFile.kt create mode 100644 common/data-models/src/iosMain/kotlin/com.shabinder.common.models/IOSMediaFile.kt create mode 100644 common/data-models/src/jsMain/kotlin/com.shabinder.common.models/WebMediaFile.kt create mode 100644 common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/di/worker/Utils.kt rename common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/{ => providers}/YoutubeProvider.kt (89%) diff --git a/android/build.gradle.kts b/android/build.gradle.kts index e9a0a68a..061fa7a4 100644 --- a/android/build.gradle.kts +++ b/android/build.gradle.kts @@ -120,12 +120,6 @@ dependencies { implementation(MVIKotlin.mvikotlinLogging) implementation(MVIKotlin.mvikotlinTimeTravel) - // Firebase - implementation(platform("com.google.firebase:firebase-bom:27.1.0")) - implementation("com.google.firebase:firebase-analytics-ktx") - implementation("com.google.firebase:firebase-crashlytics-ktx") - implementation("com.google.firebase:firebase-perf-ktx") - // Extras Extras.Android.apply { implementation(Acra.notification) @@ -134,8 +128,8 @@ dependencies { implementation(matomo) } + implementation("com.jakewharton.timber:timber:4.7.1") implementation("dev.icerock.moko:parcelize:0.6.1") - implementation("com.github.shabinder:storage-chooser:2.0.4.45") implementation("com.google.accompanist:accompanist-insets:0.9.1") // Test diff --git a/android/src/main/java/com/shabinder/spotiflyer/App.kt b/android/src/main/java/com/shabinder/spotiflyer/App.kt index 36ffd831..d1158f23 100644 --- a/android/src/main/java/com/shabinder/spotiflyer/App.kt +++ b/android/src/main/java/com/shabinder/spotiflyer/App.kt @@ -29,8 +29,29 @@ import org.koin.android.ext.koin.androidContext import org.koin.android.ext.koin.androidLogger import org.koin.core.component.KoinComponent import org.koin.core.logger.Level +import org.matomo.sdk.Matomo +import org.matomo.sdk.Tracker +import org.matomo.sdk.TrackerBuilder +import timber.log.Timber +import timber.log.Timber.DebugTree + class App: Application(), KoinComponent { + + val tracker: Tracker by lazy { + TrackerBuilder.createDefault( + "https://kind-grasshopper-73.telebit.io/matomo/matomo.php", 1) + .build(Matomo.getInstance(this)).apply { + if (BuildConfig.DEBUG) { + Timber.plant(DebugTree()) + addTrackingCallback { + Timber.d(it.toMap().toString()) + it + } + } + } + } + override fun onCreate() { super.onCreate() diff --git a/android/src/main/java/com/shabinder/spotiflyer/MainActivity.kt b/android/src/main/java/com/shabinder/spotiflyer/MainActivity.kt index 72b148e7..e9e70c0f 100644 --- a/android/src/main/java/com/shabinder/spotiflyer/MainActivity.kt +++ b/android/src/main/java/com/shabinder/spotiflyer/MainActivity.kt @@ -39,13 +39,15 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalView import androidx.core.content.ContextCompat import androidx.core.view.WindowCompat +import androidx.documentfile.provider.DocumentFile import androidx.lifecycle.lifecycleScope import com.arkivanov.decompose.ComponentContext import com.arkivanov.decompose.extensions.compose.jetbrains.rememberRootComponent import com.arkivanov.mvikotlin.logging.store.LoggingStoreFactory import com.arkivanov.mvikotlin.main.store.DefaultStoreFactory -import com.codekidlabs.storagechooser.R -import com.codekidlabs.storagechooser.StorageChooser +import com.github.k1rakishou.fsaf.FileChooser +import com.github.k1rakishou.fsaf.FileManager +import com.github.k1rakishou.fsaf.callback.directory.DirectoryChooserCallback import com.google.accompanist.insets.ProvideWindowInsets import com.google.accompanist.insets.navigationBarsPadding import com.google.accompanist.insets.statusBarsHeight @@ -56,22 +58,23 @@ import com.shabinder.common.models.Actions import com.shabinder.common.models.DownloadStatus import com.shabinder.common.models.PlatformActions import com.shabinder.common.models.PlatformActions.Companion.SharedPreferencesKey +import com.shabinder.common.models.SpotiFlyerBaseDir +import com.shabinder.common.models.Status import com.shabinder.common.models.TrackDetails +import com.shabinder.common.models.methods import com.shabinder.common.root.SpotiFlyerRoot +import com.shabinder.common.root.SpotiFlyerRoot.Analytics import com.shabinder.common.root.callbacks.SpotiFlyerRootCallBacks import com.shabinder.common.uikit.* -import com.shabinder.spotiflyer.utils.* -import com.shabinder.common.models.Status -import com.shabinder.common.models.methods import com.shabinder.spotiflyer.ui.NetworkDialog import com.shabinder.spotiflyer.ui.PermissionDialog +import com.shabinder.spotiflyer.utils.* import kotlinx.coroutines.* import kotlinx.coroutines.flow.* import org.koin.android.ext.android.inject +import org.matomo.sdk.extra.TrackHelper import java.io.File -const val disableDozeCode = 1223 - @ExperimentalAnimationApi class MainActivity : ComponentActivity() { @@ -85,6 +88,7 @@ class MainActivity : ComponentActivity() { private lateinit var updateUIReceiver: BroadcastReceiver private lateinit var queryReceiver: BroadcastReceiver private val internetAvailability by lazy { ConnectionLiveData(applicationContext) } + private val tracker get() = (application as App).tracker override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -118,7 +122,8 @@ class MainActivity : ComponentActivity() { PermissionDialog( permissionGranted.value, { requestStoragePermission() }, - { disableDozeMode(disableDozeCode) } + { disableDozeMode(disableDozeCode) }, + dir::enableAnalytics ) } } @@ -130,6 +135,10 @@ class MainActivity : ComponentActivity() { private fun initialise() { checkIfLatestVersion() handleIntentFromExternalActivity() + if(dir.isAnalyticsEnabled){ + // Download/App Install Event + TrackHelper.track().download().with(tracker) + } } @Composable @@ -139,38 +148,34 @@ class MainActivity : ComponentActivity() { @Suppress("DEPRECATION") private fun setUpOnPrefClickListener() { - // Initialize Builder - val chooser = StorageChooser.Builder() - .withActivity(this) - .withFragmentManager(fragmentManager) - .withMemoryBar(true) - .setTheme(StorageChooser.Theme(applicationContext).apply { - scheme = applicationContext.resources.getIntArray(R.array.default_dark) - }) - .setDialogTitle("Set Download Directory") - .allowCustomPath(true) - .setType(StorageChooser.DIRECTORY_CHOOSER) - .build() - - // get path that the user has chosen - chooser.setOnSelectListener { path -> - Log.d("Setting Base Path", path) - val f = File(path) - if (f.canWrite()) { - // hell yeah :) - dir.setDownloadDirectory(path) - showPopUpMessage( - "Download Directory Set to:\n${dir.defaultDir()} " - ) - }else{ - showPopUpMessage( - "NO WRITE ACCESS on \n$path ,\nReverting Back to Previous" - ) - } + /*Get User Permission to access External SD*//* + val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).apply { + addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION) + addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION) } + startActivityForResult(intent, externalSDWriteAccess)*/ + val fileChooser = FileChooser(applicationContext) + fileChooser.openChooseDirectoryDialog(object : DirectoryChooserCallback() { + override fun onResult(uri: Uri) { + println("treeUri = $uri") + // Can be only used using SAF + contentResolver.takePersistableUriPermission(uri, + Intent.FLAG_GRANT_READ_URI_PERMISSION or + Intent.FLAG_GRANT_WRITE_URI_PERMISSION) + val treeDocumentFile = DocumentFile.fromTreeUri(applicationContext, uri) - // Show dialog whenever you want by - chooser.show() + dir.setDownloadDirectory(uri) + showPopUpMessage("New Download Directory Set") + GlobalScope.launch { + dir.createDirectories() + } + } + + override fun onCancel(reason: String) { + println("Canceled by user") + } + }) } private fun showPopUpMessage(string: String, long: Boolean = false) { @@ -193,12 +198,13 @@ class MainActivity : ComponentActivity() { override val storeFactory = LoggingStoreFactory(DefaultStoreFactory) override val database = this@MainActivity.dir.db override val fetchPlatformQueryResult = this@MainActivity.fetcher + @SuppressLint("StaticFieldLeak") override val directories: Dir = this@MainActivity.dir override val downloadProgressReport: MutableSharedFlow> = trackStatusFlow override val actions = object: Actions { override val platformActions = object : PlatformActions { - override val imageCacheDir: String = applicationContext.cacheDir.absolutePath + File.separator + override val imageCacheDir: File = applicationContext.cacheDir override val sharedPreferences = applicationContext.getSharedPreferences(SharedPreferencesKey, MODE_PRIVATE ) @@ -264,6 +270,37 @@ class MainActivity : ComponentActivity() { override val isInternetAvailable get() = internetAvailability.value ?: true } + override val analytics = object: Analytics { + override fun appLaunchEvent() { + TrackHelper.track() + .event("events","App_Launch") + .name("App Launch").with(tracker) + } + + override fun homeScreenVisit() { + if(dir.isAnalyticsEnabled){ + // HomeScreen Visit Event + TrackHelper.track().screen("/main_activity/home_screen") + .title("HomeScreen").with(tracker) + } + } + + override fun listScreenVisit() { + if(dir.isAnalyticsEnabled){ + // ListScreen Visit Event + TrackHelper.track().screen("/main_activity/list_screen") + .title("ListScreen").with(tracker) + } + } + + override fun donationDialogVisit() { + if (dir.isAnalyticsEnabled) { + // Donation Dialog Open Event + TrackHelper.track().screen("/main_activity/donation_dialog") + .title("DonationDialog").with(tracker) + } + } + } } ) @@ -271,19 +308,44 @@ class MainActivity : ComponentActivity() { @SuppressLint("ObsoleteSdkInt") override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) - if (requestCode == disableDozeCode) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - val pm = - getSystemService(Context.POWER_SERVICE) as PowerManager - val isIgnoringBatteryOptimizations = - pm.isIgnoringBatteryOptimizations(packageName) - if (isIgnoringBatteryOptimizations) { - // Ignoring battery optimization - permissionGranted.value = true - } else { - disableDozeMode(disableDozeCode)//Again Ask For Permission!! + when(requestCode) { + disableDozeCode -> { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + val pm = + getSystemService(Context.POWER_SERVICE) as PowerManager + val isIgnoringBatteryOptimizations = + pm.isIgnoringBatteryOptimizations(packageName) + if (isIgnoringBatteryOptimizations) { + // Already Ignoring battery optimization + permissionGranted.value = true + } else { + //Again Ask For Permission!! + disableDozeMode(disableDozeCode) + } } } + + externalSDWriteAccess -> { + // Can be only used using SAF + /*if (resultCode == RESULT_OK) { + val treeUri: Uri? = data?.data + if (treeUri == null){ + showPopUpMessage("Some Error Occurred While Setting New Download Directory") + }else { + // Persistently save READ & WRITE Access to whole Selected Directory Tree + contentResolver.takePersistableUriPermission(treeUri, + Intent.FLAG_GRANT_READ_URI_PERMISSION or + Intent.FLAG_GRANT_WRITE_URI_PERMISSION) + dir.setDownloadDirectory(com.shabinder.common.models.File( + DocumentFile.fromTreeUri(applicationContext,treeUri)?.createDirectory("SpotiFlyer")!!) + ) + showPopUpMessage("New Download Directory Set") + GlobalScope.launch { + dir.createDirectories() + } + } + }*/ + } } } @@ -380,4 +442,9 @@ class MainActivity : ComponentActivity() { } } } + + companion object { + const val disableDozeCode = 1223 + const val externalSDWriteAccess = 1224 + } } diff --git a/android/src/main/java/com/shabinder/spotiflyer/ui/PermissionDialog.kt b/android/src/main/java/com/shabinder/spotiflyer/ui/PermissionDialog.kt index a84efaca..41c6877b 100644 --- a/android/src/main/java/com/shabinder/spotiflyer/ui/PermissionDialog.kt +++ b/android/src/main/java/com/shabinder/spotiflyer/ui/PermissionDialog.kt @@ -8,11 +8,13 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.material.AlertDialog import androidx.compose.material.Icon import androidx.compose.material.Text import androidx.compose.material.TextButton import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Insights import androidx.compose.material.icons.rounded.SdStorage import androidx.compose.material.icons.rounded.SystemSecurityUpdate import androidx.compose.runtime.Composable @@ -28,6 +30,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.DialogProperties import com.shabinder.common.uikit.SpotiFlyerShapes import com.shabinder.common.uikit.SpotiFlyerTypography import com.shabinder.common.uikit.colorPrimary @@ -38,13 +41,50 @@ import kotlinx.coroutines.delay fun PermissionDialog( permissionGranted: Boolean, requestStoragePermission:() -> Unit, - disableDozeMode:() -> Unit + disableDozeMode:() -> Unit, + enableAnalytics:() -> Unit ){ var askForPermission by remember { mutableStateOf(false) } LaunchedEffect(Unit) { delay(2000) askForPermission = true } + + // Analytics Permission Dialog + var askForAnalyticsPermission by remember { mutableStateOf(false) } + AnimatedVisibility(askForAnalyticsPermission) { + AlertDialog( + onDismissRequest = { + askForAnalyticsPermission = false + }, + title = { + Row(verticalAlignment = Alignment.CenterVertically) { + Icon(Icons.Rounded.Insights,"Analytics",Modifier.size(52.dp)) + Spacer(Modifier.padding(horizontal = 4.dp)) + Text("Grant Analytics Access",style = SpotiFlyerTypography.h5,textAlign = TextAlign.Center) + } + }, + backgroundColor = Color.DarkGray, + buttons = { + TextButton( + { + askForAnalyticsPermission = false + enableAnalytics() + }, + Modifier.padding(bottom = 16.dp, start = 16.dp, end = 16.dp).fillMaxWidth() + .background(colorPrimary, shape = SpotiFlyerShapes.medium) + .padding(horizontal = 8.dp), + ) { + Text("Sure!",color = Color.Black,fontSize = 18.sp,textAlign = TextAlign.Center) + } + }, + text = { + Text("Your Data is Anonymized and will never be shared with any 3rd party service",style = SpotiFlyerTypography.body2,textAlign = TextAlign.Center) + }, + properties = DialogProperties(dismissOnBackPress = true,dismissOnClickOutside = false) + ) + } + AnimatedVisibility( askForPermission && !permissionGranted ) { @@ -55,6 +95,7 @@ fun PermissionDialog( { requestStoragePermission() disableDozeMode() + askForAnalyticsPermission = true }, Modifier.padding(bottom = 16.dp, start = 16.dp, end = 16.dp).fillMaxWidth() .background(colorPrimary, shape = SpotiFlyerShapes.medium) @@ -100,6 +141,23 @@ fun PermissionDialog( ) } } + Row( + modifier = Modifier.fillMaxWidth().padding(top = 6.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon(Icons.Rounded.Insights,"Analytics") + Spacer(modifier = Modifier.padding(start = 16.dp)) + Column { + Text( + text = "Analytics", + style = SpotiFlyerTypography.h6.copy(fontWeight = FontWeight.SemiBold) + ) + Text( + text = "Share Analytics Data (optional) with App Devs (Self-Hosted), It will never be used/shared/sold to any third party service.", + style = SpotiFlyerTypography.subtitle2, + ) + } + } } } ) diff --git a/android/src/main/res/values/res.xml b/android/src/main/res/values/res.xml deleted file mode 100644 index 81dab6b7..00000000 --- a/android/src/main/res/values/res.xml +++ /dev/null @@ -1,62 +0,0 @@ - - - - - - - @color/colorPrimary - @android:color/white - @android:color/white - @android:color/black - @color/colorPrimary - @color/colorAccent - - - @color/colorPrimary - @android:color/white - @android:color/black - @android:color/white - @color/chevronBgColor - #da6c6c - #da6c6c - #da6c6c - @color/colorPrimary - - - - - @color/colorPrimary - @android:color/white - @android:color/black - @android:color/white - #da6c6c - @color/colorPrimary - - - @color/colorPrimary - @android:color/black - @android:color/white - @android:color/white - @color/grey - #da6c6c - #da6c6c - #da6c6c - #da6c6c - - \ No newline at end of file diff --git a/common/compose/src/commonMain/kotlin/com/shabinder/common/uikit/SpotiFlyerMainUi.kt b/common/compose/src/commonMain/kotlin/com/shabinder/common/uikit/SpotiFlyerMainUi.kt index c6a3fa54..6dd8ec37 100644 --- a/common/compose/src/commonMain/kotlin/com/shabinder/common/uikit/SpotiFlyerMainUi.kt +++ b/common/compose/src/commonMain/kotlin/com/shabinder/common/uikit/SpotiFlyerMainUi.kt @@ -102,7 +102,7 @@ fun SpotiFlyerMainContent(component: SpotiFlyerMain) { ) when (model.selectedCategory) { - HomeCategory.About -> AboutColumn() + HomeCategory.About -> AboutColumn { component.analytics.donationDialogVisit() } HomeCategory.History -> HistoryColumn( model.records.sortedByDescending { it.id }, component::loadImage, @@ -221,7 +221,10 @@ fun SearchPanel( } @Composable -fun AboutColumn(modifier: Modifier = Modifier) { +fun AboutColumn( + modifier: Modifier = Modifier, + donationDialogOpenEvent:() -> Unit +) { Box { val stateVertical = rememberScrollState(0) @@ -331,14 +334,18 @@ fun AboutColumn(modifier: Modifier = Modifier) { var isDonationDialogVisible by remember { mutableStateOf(false) } DonationDialog( - isDonationDialogVisible - ) { - isDonationDialogVisible = false - } + isDonationDialogVisible, + onDismiss = { + isDonationDialogVisible = false + } + ) Row( modifier = modifier.fillMaxWidth().padding(vertical = 6.dp) - .clickable(onClick = { isDonationDialogVisible = true }), + .clickable(onClick = { + isDonationDialogVisible = true + donationDialogOpenEvent() + }), verticalAlignment = Alignment.CenterVertically ) { Icon(Icons.Rounded.CardGiftcard, "Support Developer") diff --git a/common/data-models/build.gradle.kts b/common/data-models/build.gradle.kts index 178552a7..967da38e 100644 --- a/common/data-models/build.gradle.kts +++ b/common/data-models/build.gradle.kts @@ -45,5 +45,11 @@ kotlin { implementation("co.touchlab:stately-iso-collections:$statelyIsoVersion") } } + androidMain { + dependencies { + api("com.github.K1rakishou:Fuck-Storage-Access-Framework:v1.1") + api("androidx.documentfile:documentfile:1.0.1") + } + } } } diff --git a/common/data-models/src/androidMain/kotlin/com.shabinder.common.models/AndroidMediaFile.kt b/common/data-models/src/androidMain/kotlin/com.shabinder.common.models/AndroidMediaFile.kt new file mode 100644 index 00000000..e0752650 --- /dev/null +++ b/common/data-models/src/androidMain/kotlin/com.shabinder.common.models/AndroidMediaFile.kt @@ -0,0 +1,8 @@ +package com.shabinder.common.models + +import com.github.k1rakishou.fsaf.file.AbstractFile + +// Use Storage Access Framework `SAF` +actual data class File( + val documentFile: AbstractFile? +) \ No newline at end of file diff --git a/common/data-models/src/androidMain/kotlin/com.shabinder.common.models/AndroidPlatformActions.kt b/common/data-models/src/androidMain/kotlin/com.shabinder.common.models/AndroidPlatformActions.kt index 21131944..46732ac7 100644 --- a/common/data-models/src/androidMain/kotlin/com.shabinder.common.models/AndroidPlatformActions.kt +++ b/common/data-models/src/androidMain/kotlin/com.shabinder.common.models/AndroidPlatformActions.kt @@ -8,7 +8,7 @@ actual interface PlatformActions { const val SharedPreferencesKey = "configurations" } - val imageCacheDir: String + val imageCacheDir: java.io.File val sharedPreferences: SharedPreferences? @@ -18,7 +18,7 @@ actual interface PlatformActions { } actual val StubPlatformActions = object: PlatformActions { - override val imageCacheDir: String = "" + override val imageCacheDir = java.io.File("/") override val sharedPreferences: SharedPreferences? = null diff --git a/common/data-models/src/androidMain/kotlin/com.shabinder.common.models/SpotiFlyerBaseDir.kt b/common/data-models/src/androidMain/kotlin/com.shabinder.common.models/SpotiFlyerBaseDir.kt new file mode 100644 index 00000000..6603b994 --- /dev/null +++ b/common/data-models/src/androidMain/kotlin/com.shabinder.common.models/SpotiFlyerBaseDir.kt @@ -0,0 +1,18 @@ +package com.shabinder.common.models + +import android.net.Uri +import com.github.k1rakishou.fsaf.manager.base_directory.BaseDirectory +import java.io.File + +class SpotiFlyerBaseDir( + private val getDirType: ()-> ActiveBaseDirType, + private val getJavaFile: ()-> File?, + private val getSAFUri: ()-> Uri? +): BaseDirectory() { + + override fun currentActiveBaseDirType(): ActiveBaseDirType = getDirType() + + override fun getDirFile(): File? = getJavaFile() + + override fun getDirUri(): Uri? = getSAFUri() +} \ No newline at end of file diff --git a/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/DownloadObject.kt b/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/DownloadObject.kt index 7d004ed6..080c790d 100644 --- a/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/DownloadObject.kt +++ b/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/DownloadObject.kt @@ -32,12 +32,12 @@ data class TrackDetails( var comment: String? = null, var lyrics: String? = null, var trackUrl: String? = null, - var albumArtPath: String, + var albumArtPath: String, // UriString in Android var albumArtURL: String, var source: Source, val progress: Int = 2, val downloaded: DownloadStatus = DownloadStatus.NotDownloaded, - var outputFilePath: String, + var outputFilePath: String, // UriString in Android var videoID: String? = null, ) : Parcelable diff --git a/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/File.kt b/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/File.kt new file mode 100644 index 00000000..995a30ed --- /dev/null +++ b/common/data-models/src/commonMain/kotlin/com/shabinder/common/models/File.kt @@ -0,0 +1,3 @@ +package com.shabinder.common.models + +expect class File \ No newline at end of file diff --git a/common/data-models/src/desktopMain/kotlin/com/shabinder/common/models/DesktopMediaFile.kt b/common/data-models/src/desktopMain/kotlin/com/shabinder/common/models/DesktopMediaFile.kt new file mode 100644 index 00000000..9fc04540 --- /dev/null +++ b/common/data-models/src/desktopMain/kotlin/com/shabinder/common/models/DesktopMediaFile.kt @@ -0,0 +1,3 @@ +package com.shabinder.common.models + +actual typealias File = java.io.File \ No newline at end of file diff --git a/common/data-models/src/iosMain/kotlin/com.shabinder.common.models/IOSMediaFile.kt b/common/data-models/src/iosMain/kotlin/com.shabinder.common.models/IOSMediaFile.kt new file mode 100644 index 00000000..3b6f93f3 --- /dev/null +++ b/common/data-models/src/iosMain/kotlin/com.shabinder.common.models/IOSMediaFile.kt @@ -0,0 +1,5 @@ +package com.shabinder.common.models + +actual data class File( + val path: String +) \ No newline at end of file diff --git a/common/data-models/src/jsMain/kotlin/com.shabinder.common.models/WebMediaFile.kt b/common/data-models/src/jsMain/kotlin/com.shabinder.common.models/WebMediaFile.kt new file mode 100644 index 00000000..3b6f93f3 --- /dev/null +++ b/common/data-models/src/jsMain/kotlin/com.shabinder.common.models/WebMediaFile.kt @@ -0,0 +1,5 @@ +package com.shabinder.common.models + +actual data class File( + val path: String +) \ No newline at end of file diff --git a/common/dependency-injection/build.gradle.kts b/common/dependency-injection/build.gradle.kts index d030b2d9..7724fa72 100644 --- a/common/dependency-injection/build.gradle.kts +++ b/common/dependency-injection/build.gradle.kts @@ -41,7 +41,6 @@ kotlin { dependencies { implementation(compose.materialIconsExtended) implementation(Extras.mp3agic) - implementation("com.github.shabinder:storage-chooser:2.0.4.45") // implementation(files("$rootDir/libs/mobile-ffmpeg.aar")) } } diff --git a/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/di/AndroidDir.kt b/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/di/AndroidDir.kt index b077dd23..e40ebd4a 100644 --- a/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/di/AndroidDir.kt +++ b/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/di/AndroidDir.kt @@ -16,14 +16,26 @@ package com.shabinder.common.di +import android.content.Context import android.graphics.Bitmap import android.graphics.BitmapFactory +import android.net.Uri import android.os.Environment +import android.util.Log import androidx.compose.ui.graphics.asImageBitmap +import androidx.documentfile.provider.DocumentFile import co.touchlab.kermit.Kermit +import com.github.k1rakishou.fsaf.FileManager +import com.github.k1rakishou.fsaf.file.AbstractFile +import com.github.k1rakishou.fsaf.file.DirectorySegment +import com.github.k1rakishou.fsaf.file.FileSegment +import com.github.k1rakishou.fsaf.manager.base_directory.BaseDirectory import com.mpatric.mp3agic.Mp3File import com.russhwolf.settings.Settings import com.shabinder.common.database.SpotiFlyerDatabase +import com.shabinder.common.di.utils.removeIllegalChars +import com.shabinder.common.models.File +import com.shabinder.common.models.SpotiFlyerBaseDir import com.shabinder.common.models.TrackDetails import com.shabinder.common.models.methods import com.shabinder.database.Database @@ -31,81 +43,225 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import java.io.File +import org.koin.core.component.KoinComponent +import org.koin.core.component.get +import java.io.FileInputStream import java.io.FileOutputStream import java.io.IOException import java.io.InputStream import java.net.HttpURLConnection import java.net.URL +/* +* Ignore Deprecation +* Deprecation is only a Suggestion P-) +* */ +@Suppress("DEPRECATION") actual class Dir actual constructor( private val logger: Kermit, private val settings: Settings, private val spotiFlyerDatabase: SpotiFlyerDatabase, -) { - companion object { - const val DirKey = "downloadDir" +): KoinComponent { + + private val context: Context = get() + val fileManager = FileManager(context) + + init { + fileManager.registerBaseDir(SpotiFlyerBaseDir({ getDirType() }, + getJavaFile = { + java.io.File( + Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC).toString() + + "/SpotiFlyer/" + ) + }, + getSAFUri = { null } + )) } - actual fun setDownloadDirectory(newBasePath:String) = settings.putString(DirKey,newBasePath) + companion object { + const val DirKey = "downloadDir" + const val AnalyticsKey = "analytics" + } - @Suppress("DEPRECATION") - private val defaultBaseDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC).toString() + /* + * Do we have Analytics Permission? + * - Defaults to `False` + * */ + actual val isAnalyticsEnabled get() = settings.getBooleanOrNull(AnalyticsKey) ?: false - actual fun fileSeparator(): String = File.separator + actual fun enableAnalytics() { + settings.putBoolean(AnalyticsKey,true) + } - actual fun imageCacheDir(): String = methods.value.platformActions.imageCacheDir - - // fun call in order to always access Updated Value - actual fun defaultDir(): String = (settings.getStringOrNull(DirKey) ?: defaultBaseDir) + - File.separator + "SpotiFlyer" + File.separator - - actual fun isPresent(path: String): Boolean = File(path).exists() - - actual fun createDirectory(dirPath: String) { - val yourAppDir = File(dirPath) - - if (!yourAppDir.exists() && !yourAppDir.isDirectory) { // create empty directory - if (yourAppDir.mkdirs()) { logger.i { "$dirPath created" } } else { - logger.e { "Unable to create Dir: $dirPath!" } - } - } else { - logger.i { "$dirPath already exists" } + private fun getDirType() :BaseDirectory.ActiveBaseDirType{ + return if(settings.getStringOrNull(DirKey) == null) { + // Default Dir + BaseDirectory.ActiveBaseDirType.JavaFileBaseDir + }else { + // User Updated Dir + BaseDirectory.ActiveBaseDirType.SafBaseDir } } + actual fun setDownloadDirectory(newBasePath:File) = settings.putString( + DirKey, + newBasePath.documentFile?.getFullPath()!! + ) + + fun setDownloadDirectory(treeUri:Uri) { + fileManager.registerBaseDir(SpotiFlyerBaseDir( + { getDirType() }, + getJavaFile = { + null + }, + getSAFUri = { + treeUri + } + )) + } + + @Suppress("DEPRECATION")// By Default Save Files to /Music/SpotiFlyer/ + private val defaultBaseDir = SpotiFlyerBaseDir({ getDirType() }, + getJavaFile = {java.io.File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC).toString() + "/SpotiFlyer/")}, + getSAFUri = { null } + ) + + // Image Cache Path + // We Will Handling Image relating operations using java.io.File (reason: Faster) + actual val imageCachePath: String get() = methods.value.platformActions.imageCacheDir.absolutePath + "/" + + actual fun imageCacheDir(): File = File(fileManager.fromPath(imageCachePath)) + + // fun call in order to always access Updated Value + actual fun defaultDir(): File = File(fileManager.newBaseDirectoryFile()) + + actual fun isPresent(file: File): Boolean = file.documentFile?.let { fileManager.exists(it) } ?: false + + actual fun createDirectory(dirPath: File , subDirectory:String?) { + if(dirPath.documentFile != null) { + if (subDirectory != null) { + fileManager.createDir(dirPath.documentFile!!,subDirectory) + }else { + fileManager.create(dirPath.documentFile!!) + } + } + + /*try { + val yourAppDir = File(dirPath) + + if (!yourAppDir.exists() && !yourAppDir.isDirectory) { // create empty directory + if (yourAppDir.mkdirs()) { logger.i { "$dirPath created" } } else { + logger.e { "Unable to create Dir: $dirPath!" } + } + } else { + logger.i { "$dirPath already exists" } + } + } catch (e: SecurityException) { + //TRY USING SAF + Log.d("Directory","USING SAF to create $dirPath") + val file = DocumentFile.fromTreeUri(context, Uri.parse(defaultDir())) + DocumentFile.fromFile() + }*/ + } + actual suspend fun clearCache(): Unit = withContext(dispatcherIO) { - File(imageCacheDir()).deleteRecursively() + try { + java.io.File(imageCachePath).deleteRecursively() + }catch (e:Exception){ + e.printStackTrace() + } } @Suppress("BlockingMethodInNonBlockingContext") actual suspend fun saveFileWithMetadata( mp3ByteArray: ByteArray, trackDetails: TrackDetails, - postProcess:(track: TrackDetails)->Unit - ) = withContext(dispatcherIO) { - val songFile = File(trackDetails.outputFilePath) + postProcess: (track: TrackDetails) -> Unit, + ): Unit = withContext(dispatcherIO) { + val songFile = java.io.File(imageCachePath+"Tracks/"+ removeIllegalChars(trackDetails.title) + ".mp3") try { - /* - * Check , if Fetch was Used, File is saved Already, else write byteArray we Received - * */ + + /*Make intermediate Dirs if they don't exist yet*/ if(!songFile.exists()) { - /*Make intermediate Dirs if they don't exist yet*/ - songFile.parentFile.mkdirs() + songFile.parentFile?.mkdirs() } if(mp3ByteArray.isNotEmpty()) songFile.writeBytes(mp3ByteArray) - when (trackDetails.outputFilePath.substringAfterLast('.')) { + Mp3File(songFile) + .removeAllTags() + .setId3v1Tags(trackDetails) + .setId3v2TagsAndSaveFile(trackDetails,songFile.absolutePath) + + // Copy File to Desired Location + val documentFile = when(getDirType()){ + BaseDirectory.ActiveBaseDirType.SafBaseDir -> { + fileManager.fromUri(Uri.parse(trackDetails.outputFilePath)) + } + BaseDirectory.ActiveBaseDirType.JavaFileBaseDir -> { + fileManager.fromPath(trackDetails.outputFilePath) + } + }.also { fileManager.create(it!!) } + + try { + fileManager.copyFileContents( + fileManager.fromRawFile(songFile), + documentFile!! + ) + songFile.deleteOnExit() + /*val inStream = FileInputStream(songFile) + + val buffer = ByteArray(1024) + var readLen: Int + while (inStream.read(buffer).also { readLen = it } != -1) { + outStream?.write(buffer, 0, readLen) + } + inStream.close() + // write the output file (You have now copied the file) + outStream?.flush() + outStream?.close()*/ + + }catch (e:Exception) { + e.printStackTrace() + } + + documentFile?.let { + addToLibrary(File(it)) + } + + /*when (trackDetails.outputFilePath.substringAfterLast('.')) { ".mp3" -> { - Mp3File(File(songFile.absolutePath)) + Mp3File(songFile) .removeAllTags() .setId3v1Tags(trackDetails) - .setId3v2TagsAndSaveFile(trackDetails) - addToLibrary(songFile.absolutePath) + .setId3v2TagsAndSaveFile(trackDetails,songFile.absolutePath) + + // Copy File to DocumentUri + val documentFile = DocumentFile.fromSingleUri(context,Uri.parse(trackDetails.outputFilePath)) + try { + val outStream = context.contentResolver.openOutputStream(documentFile?.uri!!) + val inStream = FileInputStream(songFile) + + val buffer = ByteArray(1024) + var readLen: Int + while (inStream.read(buffer).also { readLen = it } != -1) { + outStream?.write(buffer, 0, readLen) + } + inStream.close() + // write the output file (You have now copied the file) + outStream?.flush() + outStream?.close() + + }catch (e:Exception) { + e.printStackTrace() + } + + documentFile?.let { + addToLibrary(File(it)) + } } ".m4a" -> { - /*FFmpeg.executeAsync( + *//*FFmpeg.executeAsync( "-i ${m4aFile.absolutePath} -y -b:a 160k -acodec libmp3lame -vn ${m4aFile.absolutePath.substringBeforeLast('.') + ".mp3"}" ){ _, returnCode -> when (returnCode) { @@ -127,18 +283,12 @@ actual class Dir actual constructor( logger.d { "Async command execution failed with rc=$returnCode" } } } - }*/ + }*//* } else -> { - try { - Mp3File(File(songFile.absolutePath)) - .removeAllTags() - .setId3v1Tags(trackDetails) - .setId3v2TagsAndSaveFile(trackDetails) - addToLibrary(songFile.absolutePath) - } catch (e: Exception) { e.printStackTrace() } + // TODO } - } + }*/ }catch (e:Exception){ withContext(Dispatchers.Main){ //Toast.makeText(appContext,"Could Not Create File:\n${songFile.absolutePath}",Toast.LENGTH_SHORT).show() @@ -148,10 +298,12 @@ actual class Dir actual constructor( } } - actual fun addToLibrary(path: String) = methods.value.platformActions.addToLibrary(path) + actual fun addToLibrary(file: File) { +// methods.value.platformActions.addToLibrary(path) + } actual suspend fun loadImage(url: String): Picture = withContext(dispatcherIO){ - val cachePath = imageCacheDir() + getNameURL(url) + val cachePath = imageCachePath + getNameURL(url) Picture(image = (loadCachedImage(cachePath) ?: freshImage(url))?.asImageBitmap()) } @@ -169,6 +321,7 @@ actual class Dir actual constructor( actual suspend fun cacheImage(image: Any, path: String):Unit = withContext(dispatcherIO) { try { + java.io.File(path).parentFile?.mkdirs() FileOutputStream(path).use { out -> (image as? Bitmap)?.compress(Bitmap.CompressFormat.JPEG, 100, out) } @@ -189,7 +342,7 @@ actual class Dir actual constructor( if (result != null) { GlobalScope.launch(Dispatchers.IO) { - cacheImage(result, imageCacheDir() + getNameURL(url)) + cacheImage(result, imageCachePath + getNameURL(url)) } result } else null @@ -200,4 +353,78 @@ actual class Dir actual constructor( } actual val db: Database? = spotiFlyerDatabase.instance + + actual fun finalOutputPath( + itemName: String, + type: String, + subFolder: String, + extension: String, + ):String = finalOutputFile( + itemName, + type, + subFolder, + extension, + ).documentFile?.getFullPath() ?: throw(Exception("no path for $itemName")) + + actual fun finalOutputFile( + itemName: String, + type: String, + subFolder: String, + extension: String, + ):File { + val file = fileManager.create( + defaultDir().documentFile!!, //Base Dir + DirectorySegment(removeIllegalChars(type)), + DirectorySegment(removeIllegalChars(subFolder)), + + ) + return File(file?.clone(FileSegment(removeIllegalChars(itemName) + extension)))/*.also { + if(fileManager.getLength(it.documentFile!!) == 0L){ + fileManager.delete(it.documentFile!!) + } + }*/ + /*GlobalScope.launch { + // Create Intermediate Directories + var file = defaultDir().documentFile + file = file.findFile(removeIllegalChars(type)) + ?: file.createDirectory(removeIllegalChars(type)) + ?: throw Exception("Couldn't Find/Create $type Directory") + + if (subFolder.isNotEmpty()) file.findFile(removeIllegalChars(subFolder)) + ?: file.createDirectory(removeIllegalChars(subFolder)) + ?: throw Exception("Couldn't Find/Create $subFolder Directory") + + } + val sep = "%2F" + val finalUri = defaultDir().documentFile.uri.toString() + sep + + removeIllegalChars(type) + sep + + removeIllegalChars(subFolder) + sep + + removeIllegalChars(itemName) + extension + return File( + DocumentFile.fromSingleUri(context,Uri.parse(finalUri))!! + ).also { + Log.d("Final Output File",it.documentFile.uri.toString()) + }*/ + + + /*file = file?.findFile(removeIllegalChars(type)) + ?: file?.createDirectory(removeIllegalChars(type)) + ?: throw Exception("Couldn't Find/Create $type Directory") + + if (subFolder.isNotEmpty()) file = file.findFile(removeIllegalChars(subFolder)) + ?: file.createDirectory(removeIllegalChars(subFolder)) + ?: throw Exception("Couldn't Find/Create $subFolder Directory") + + // TODO check Mime + file = file.findFile(removeIllegalChars(itemName)) + ?: file.createFile("audio/mpeg",removeIllegalChars(itemName)) + ?: throw Exception("Couldn't Find/Create ${removeIllegalChars(itemName) + extension} File") + Log.d("Final Output File",file.uri.toString()) + + return File(file).also { + val size = it.documentFile.length() + Log.d("File size", size.toString()) + if(size == 0L) it.documentFile.delete() + }*/ + } } diff --git a/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/di/AudioTagging.kt b/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/di/AudioTagging.kt index 042d26b4..5bb3fbe3 100644 --- a/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/di/AudioTagging.kt +++ b/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/di/AudioTagging.kt @@ -99,9 +99,8 @@ fun Mp3File.setId3v1Tags(track: TrackDetails): Mp3File { } @Suppress("BlockingMethodInNonBlockingContext") -suspend fun Mp3File.setId3v2TagsAndSaveFile(track: TrackDetails) { +suspend fun Mp3File.setId3v2TagsAndSaveFile(track: TrackDetails,tempMp3Path:String) { val id3v2Tag = ID3v24Tag().apply { - artist = track.artists.joinToString(", ") title = track.title album = track.albumName @@ -118,9 +117,9 @@ suspend fun Mp3File.setId3v2TagsAndSaveFile(track: TrackDetails) { fis.close() id3v2Tag.setAlbumImage(bytesArray, "image/jpeg") this.id3v2Tag = id3v2Tag - saveFile(track.outputFilePath) + saveFile(tempMp3Path) } catch (e: java.io.FileNotFoundException) { - Log.e("Error", "Couldn't Write Cached Mp3 Album Art, error: ${e.stackTrace}") + Log.e("Error", "Couldn't Write Cached Mp3 Album Art, Downloading And Trying Again, error: ${e.message}") try { // Image Still Not Downloaded! // Lets Download Now and Write it into Album Art @@ -130,7 +129,7 @@ suspend fun Mp3File.setId3v2TagsAndSaveFile(track: TrackDetails) { is DownloadResult.Success -> { id3v2Tag.setAlbumImage(it.byteArray, "image/jpeg") this.id3v2Tag = id3v2Tag - saveFile(track.outputFilePath) + saveFile(tempMp3Path) } is DownloadResult.Progress -> {} // Nothing for Now , no progress bar to show } @@ -143,6 +142,7 @@ suspend fun Mp3File.setId3v2TagsAndSaveFile(track: TrackDetails) { } fun Mp3File.saveFile(filePath: String) { + Log.d("Mp3 File Save",filePath) save(filePath.substringBeforeLast('.') + ".new.mp3") val m4aFile = File(filePath) m4aFile.delete() diff --git a/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/di/worker/ForegroundService.kt b/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/di/worker/ForegroundService.kt index 8576851d..58414e5a 100644 --- a/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/di/worker/ForegroundService.kt +++ b/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/di/worker/ForegroundService.kt @@ -38,7 +38,10 @@ import androidx.annotation.RequiresApi import androidx.core.app.NotificationCompat import androidx.core.net.toUri import co.touchlab.kermit.Kermit +import com.github.k1rakishou.fsaf.FileManager +import com.github.k1rakishou.fsaf.file.AbstractFile import com.shabinder.common.di.* +import com.shabinder.common.di.providers.getData import com.shabinder.common.di.utils.ParallelExecutor import com.shabinder.common.models.DownloadResult import com.shabinder.common.models.DownloadStatus @@ -47,6 +50,7 @@ import com.shabinder.downloader.models.formats.Format import com.shabinder.common.models.Status import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.Job import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.flow.collect @@ -337,34 +341,14 @@ class ForegroundService : Service(), CoroutineScope { service.createNotificationChannel(channel) } - /** - * Cleaning All Residual Files except Mp3 Files - **/ - private fun cleanFiles(dir: File) { - logger.d(tag) { "Starting Cleaning in ${dir.path} " } - val fList = dir.listFiles() - fList?.let { - for (file in fList) { - if (file.isDirectory) { - cleanFiles(file) - } else if (file.isFile) { - if (file.path.toString().substringAfterLast(".") != "mp3") { - logger.d(tag) { "Cleaning ${file.path}" } - file.delete() - } - } - } - } - } - private fun killService() { launch { logger.d(tag) { "Killing Self" } messageList = mutableListOf("Cleaning And Exiting", "", "", "", "") downloadService.close() updateNotification() - cleanFiles(File(dir.defaultDir())) - // TODO cleanFiles(File(dir.imageCacheDir())) + // dir.defaultDir().documentFile?.let { cleanFiles(it,dir.fileManager,logger) } + cleanFiles(File(dir.imageCachePath + "Tracks/"),logger) messageList = mutableListOf("", "", "", "", "") releaseWakeLock() serviceJob.cancel() diff --git a/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/di/worker/Utils.kt b/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/di/worker/Utils.kt new file mode 100644 index 00000000..43b6830d --- /dev/null +++ b/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/di/worker/Utils.kt @@ -0,0 +1,47 @@ +package com.shabinder.common.di.worker + +import co.touchlab.kermit.Kermit +import com.github.k1rakishou.fsaf.FileManager +import com.github.k1rakishou.fsaf.file.AbstractFile +import java.io.File + +/** + * Cleaning All Residual Files except Mp3 Files + **/ +fun cleanFiles(dir: File,logger: Kermit) { + try { + logger.d("File Cleaning") { "Starting Cleaning in ${dir.path} " } + val fList = dir.listFiles() + fList?.let { + for (file in fList) { + if (file.isDirectory) { + cleanFiles(file, logger) + } else if (file.isFile) { + if (file.path.toString().substringAfterLast(".") != "mp3") { + logger.d("Files Cleaning") { "Cleaning ${file.path}" } + file.delete() + } + } + } + } + } catch (e:Exception) { e.printStackTrace() } +} +/** + * Cleaning All Residual Files except Mp3 Files + **/ +fun cleanFiles(directory: AbstractFile,fm: FileManager,logger: Kermit) { + try { + logger.d("Files Cleaning") { "Starting Cleaning in ${directory.getFullPath()} " } + val fList = fm.listFiles(directory) + for (file in fList) { + if (fm.isDirectory(file)) { + cleanFiles(file, fm, logger) + } else if (fm.isFile(file)) { + if (file.getFullPath().substringAfterLast(".") != "mp3") { + logger.d("Files Cleaning") { "Cleaning ${file.getFullPath()}" } + fm.delete(file) + } + } + } + } catch (e:Exception) { e.printStackTrace() } +} \ No newline at end of file diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/DI.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/DI.kt index 7416fcf6..8aab25df 100644 --- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/DI.kt +++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/DI.kt @@ -17,7 +17,6 @@ package com.shabinder.common.di import co.touchlab.kermit.Kermit -import co.touchlab.stately.ensureNeverFrozen import com.russhwolf.settings.Settings import com.shabinder.common.database.databaseModule import com.shabinder.common.database.getLogger @@ -25,7 +24,7 @@ import com.shabinder.common.di.providers.GaanaProvider import com.shabinder.common.di.providers.SpotifyProvider import com.shabinder.common.di.providers.YoutubeMp3 import com.shabinder.common.di.providers.YoutubeMusic -import com.shabinder.common.models.NativeAtomicReference +import com.shabinder.common.di.providers.YoutubeProvider import io.ktor.client.HttpClient import io.ktor.client.features.HttpTimeout import io.ktor.client.features.json.JsonFeature @@ -87,12 +86,6 @@ fun createHttpClient(enableNetworkLogs: Boolean = false) = HttpClient { } ) }*/ - // Timeout - install(HttpTimeout) { - // requestTimeoutMillis = 20000L - connectTimeoutMillis = 15000L - socketTimeoutMillis = 15000L - } if (enableNetworkLogs) { install(Logging) { logger = Logger.DEFAULT diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/Dir.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/Dir.kt index 49ae8f9c..ef7475c9 100644 --- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/Dir.kt +++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/Dir.kt @@ -18,10 +18,10 @@ package com.shabinder.common.di import co.touchlab.kermit.Kermit import com.russhwolf.settings.Settings -import com.russhwolf.settings.SettingsListener import com.shabinder.common.database.SpotiFlyerDatabase import com.shabinder.common.di.utils.removeIllegalChars import com.shabinder.common.models.DownloadResult +import com.shabinder.common.models.File import com.shabinder.common.models.TrackDetails import com.shabinder.database.Database import io.ktor.client.request.* @@ -38,17 +38,27 @@ expect class Dir ( spotiFlyerDatabase: SpotiFlyerDatabase, ) { val db: Database? - fun isPresent(path: String): Boolean - fun fileSeparator(): String - fun defaultDir(): String - fun imageCacheDir(): String - fun createDirectory(dirPath: String) - fun setDownloadDirectory(newBasePath:String) + val isAnalyticsEnabled:Boolean + fun enableAnalytics() + fun isPresent(file: File): Boolean + //fun fileSeparator(): String + fun defaultDir(): File + fun imageCacheDir(): File + val imageCachePath: String + + /* + * on Android dirPath represents Directory Name + * */ + fun createDirectory(dirPath: File , subDirectory:String? = null) + + fun setDownloadDirectory(newBasePath:File) suspend fun cacheImage(image: Any, path: String) // in Android = ImageBitmap, Desktop = BufferedImage suspend fun loadImage(url: String): Picture suspend fun clearCache() suspend fun saveFileWithMetadata(mp3ByteArray: ByteArray, trackDetails: TrackDetails,postProcess:(track: TrackDetails)->Unit = {}) - fun addToLibrary(path: String) + fun addToLibrary(file: File) + fun finalOutputFile(itemName: String, type: String, subFolder: String, extension: String = ".mp3"): File + fun finalOutputPath(itemName: String, type: String, subFolder: String, extension: String = ".mp3"): String } suspend fun downloadFile(url: String): Flow { @@ -92,8 +102,9 @@ suspend fun downloadByteArray( return response } +// Function to get cache Image Name fun getNameURL(url: String): String { - return url.substring(url.lastIndexOf('/', url.lastIndexOf('/') - 1) + 1, url.length).replace('/', '_') + return url.substring(url.lastIndexOf('/', url.lastIndexOf('/') - 1) + 1, url.length).replace('/', '_') + ".jpeg" } /* @@ -102,13 +113,13 @@ fun getNameURL(url: String): String { fun Dir.createDirectories() { createDirectory(defaultDir()) createDirectory(imageCacheDir()) - createDirectory(defaultDir() + "Tracks/") - createDirectory(defaultDir() + "Albums/") - createDirectory(defaultDir() + "Playlists/") - createDirectory(defaultDir() + "YT_Downloads/") + createDirectory(defaultDir(),"Tracks/") + createDirectory(defaultDir(),"Albums/") + createDirectory(defaultDir(),"Playlists/") + createDirectory(defaultDir(),"YT_Downloads/") } -fun Dir.finalOutputDir(itemName: String, type: String, subFolder: String, defaultDir: String, extension: String = ".mp3"): String = +/*fun Dir.finalOutputDir(itemName: String, type: String, subFolder: String, defaultDir: String, extension: String = ".mp3"): String = defaultDir + removeIllegalChars(type) + this.fileSeparator() + if (subFolder.isEmpty())"" else { removeIllegalChars(subFolder) + this.fileSeparator() } + - removeIllegalChars(itemName) + extension + removeIllegalChars(itemName) + extension*/ diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/FetchPlatformQueryResult.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/FetchPlatformQueryResult.kt index ef1b6f27..63110d0a 100644 --- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/FetchPlatformQueryResult.kt +++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/FetchPlatformQueryResult.kt @@ -21,13 +21,10 @@ import com.shabinder.common.di.providers.GaanaProvider import com.shabinder.common.di.providers.SpotifyProvider import com.shabinder.common.di.providers.YoutubeMp3 import com.shabinder.common.di.providers.YoutubeMusic +import com.shabinder.common.di.providers.YoutubeProvider import com.shabinder.common.models.PlatformQueryResult -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.async -import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext class FetchPlatformQueryResult( val gaanaProvider: GaanaProvider, diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/GaanaProvider.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/GaanaProvider.kt index 69959a54..fbebe579 100644 --- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/GaanaProvider.kt +++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/GaanaProvider.kt @@ -18,8 +18,8 @@ package com.shabinder.common.di.providers import co.touchlab.kermit.Kermit import com.shabinder.common.di.Dir -import com.shabinder.common.di.finalOutputDir import com.shabinder.common.di.gaana.GaanaRequests +import com.shabinder.common.di.getNameURL import com.shabinder.common.models.DownloadStatus import com.shabinder.common.models.PlatformQueryResult import com.shabinder.common.models.TrackDetails @@ -136,7 +136,7 @@ class GaanaProvider( title = it.track_title, artists = it.artist.map { artist -> artist?.name.toString() }, durationSec = it.duration, - albumArtPath = dir.imageCacheDir() + (it.artworkLink.substringBeforeLast('/').substringAfterLast('/')) + ".jpeg", + albumArtPath = dir.imageCachePath + getNameURL(it.artworkLink), albumName = it.album_title, year = it.release_date, comment = "Genres:${it.genre?.map { genre -> genre?.name }?.reduceOrNull { acc, s -> acc + s }}", @@ -144,16 +144,15 @@ class GaanaProvider( downloaded = it.downloaded ?: DownloadStatus.NotDownloaded, source = Source.Gaana, albumArtURL = it.artworkLink.replace("http:","https:"), - outputFilePath = dir.finalOutputDir(it.track_title, type, subFolder, dir.defaultDir()/*,".m4a"*/) + outputFilePath = dir.finalOutputPath(it.track_title, type, subFolder /*,".m4a"*/) ) } private fun GaanaTrack.updateStatusIfPresent(folderType: String, subFolder: String) { if (dir.isPresent( - dir.finalOutputDir( + dir.finalOutputFile( track_title, folderType, subFolder, - dir.defaultDir() ) ) ) { // Download Already Present!! diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/SpotifyProvider.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/SpotifyProvider.kt index ab40a511..ad029703 100644 --- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/SpotifyProvider.kt +++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/SpotifyProvider.kt @@ -17,14 +17,10 @@ package com.shabinder.common.di.providers import co.touchlab.kermit.Kermit -import co.touchlab.stately.ensureNeverFrozen -import co.touchlab.stately.freeze import com.shabinder.common.di.Dir import com.shabinder.common.di.TokenStore import com.shabinder.common.di.createHttpClient -import com.shabinder.common.di.finalOutputDir -import com.shabinder.common.di.kotlinxSerializer -import com.shabinder.common.di.ktorHttpClient +import com.shabinder.common.di.getNameURL import com.shabinder.common.di.spotify.SpotifyRequests import com.shabinder.common.di.spotify.authenticateSpotify import com.shabinder.common.models.NativeAtomicReference @@ -35,12 +31,8 @@ import com.shabinder.common.models.spotify.Image import com.shabinder.common.models.spotify.Source import com.shabinder.common.models.spotify.Track import io.ktor.client.HttpClient -import io.ktor.client.features.auth.* -import io.ktor.client.features.auth.providers.* import io.ktor.client.features.defaultRequest -import io.ktor.client.features.json.JsonFeature import io.ktor.client.request.header -import kotlin.native.concurrent.SharedImmutable class SpotifyProvider( private val tokenStore: TokenStore, @@ -224,28 +216,28 @@ class SpotifyProvider( } private fun List.toTrackDetailsList(type: String, subFolder: String) = this.map { + val albumArtLink = it.album?.images?.firstOrNull()?.url.toString() TrackDetails( title = it.name.toString(), artists = it.artists?.map { artist -> artist?.name.toString() } ?: listOf(), durationSec = (it.duration_ms / 1000).toInt(), - albumArtPath = dir.imageCacheDir() + (it.album?.images?.firstOrNull()?.url.toString()).substringAfterLast('/') + ".jpeg", + albumArtPath = dir.imageCachePath + getNameURL(albumArtLink), albumName = it.album?.name, year = it.album?.release_date, comment = "Genres:${it.album?.genres?.joinToString()}", trackUrl = it.href, downloaded = it.downloaded, source = Source.Spotify, - albumArtURL = it.album?.images?.firstOrNull()?.url.toString(), - outputFilePath = dir.finalOutputDir(it.name.toString(), type, subFolder, dir.defaultDir()/*,".m4a"*/) + albumArtURL = albumArtLink, + outputFilePath = dir.finalOutputPath(it.name.toString(), type, subFolder/*,".m4a"*/) ) } private fun Track.updateStatusIfPresent(folderType: String, subFolder: String) { if (dir.isPresent( - dir.finalOutputDir( + dir.finalOutputFile( name.toString(), folderType, subFolder, - dir.defaultDir() ) ) ) { // Download Already Present!! diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/YoutubeProvider.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/YoutubeProvider.kt similarity index 89% rename from common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/YoutubeProvider.kt rename to common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/YoutubeProvider.kt index 37573ce5..9d8af706 100644 --- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/YoutubeProvider.kt +++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/providers/YoutubeProvider.kt @@ -14,9 +14,11 @@ * * along with this program. If not, see . */ -package com.shabinder.common.di +package com.shabinder.common.di.providers import co.touchlab.kermit.Kermit +import com.shabinder.common.di.Dir +import com.shabinder.common.di.getNameURL import com.shabinder.common.di.utils.removeIllegalChars import com.shabinder.common.models.DownloadStatus import com.shabinder.common.models.PlatformQueryResult @@ -106,15 +108,14 @@ class YoutubeProvider( title = it.title ?: "N/A", artists = listOf(it.author ?: "N/A"), durationSec = it.lengthSeconds, - albumArtPath = dir.imageCacheDir() + it.videoId + ".jpeg", + albumArtPath = dir.imageCachePath + getNameURL(coverUrl), source = Source.YouTube, - albumArtURL = "https://i.ytimg.com/vi/${it.videoId}/hqdefault.jpg", + albumArtURL = coverUrl, downloaded = if (dir.isPresent( - dir.finalOutputDir( + dir.finalOutputFile( itemName = it.title ?: "N/A", type = folderType, subFolder = subFolder, - dir.defaultDir() ) ) ) @@ -122,7 +123,7 @@ class YoutubeProvider( else { DownloadStatus.NotDownloaded }, - outputFilePath = dir.finalOutputDir(it.title ?: "N/A", folderType, subFolder, dir.defaultDir()/*,".m4a"*/), + outputFilePath = dir.finalOutputPath(it.title ?: "N/A", folderType, subFolder/*,".m4a"*/), videoID = it.videoId ) } @@ -160,15 +161,14 @@ class YoutubeProvider( title = name, artists = listOf(detail.author ?: "N/A"), durationSec = detail.lengthSeconds, - albumArtPath = dir.imageCacheDir() + "$searchId.jpeg", + albumArtPath = dir.imageCachePath + getNameURL(coverUrl), source = Source.YouTube, - albumArtURL = "https://i.ytimg.com/vi/$searchId/hqdefault.jpg", + albumArtURL = coverUrl, downloaded = if (dir.isPresent( - dir.finalOutputDir( + dir.finalOutputFile( itemName = name, type = folderType, subFolder = subFolder, - defaultDir = dir.defaultDir() ) ) ) @@ -176,7 +176,7 @@ class YoutubeProvider( else { DownloadStatus.NotDownloaded }, - outputFilePath = dir.finalOutputDir(name, folderType, subFolder, dir.defaultDir()/*,".m4a"*/), + outputFilePath = dir.finalOutputPath(name, folderType, subFolder /*,".m4a"*/), videoID = searchId ) ) diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/utils/Utils.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/utils/Utils.kt index 88fcc775..f1c7734f 100644 --- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/utils/Utils.kt +++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/utils/Utils.kt @@ -90,6 +90,7 @@ fun removeIllegalChars(fileName: String): String { name = fileName.replace(c, '_') } name = name.replace("\\s".toRegex(), "_") + name = name.replace("/".toRegex(), "_") name = name.replace("\\)".toRegex(), "") name = name.replace("\\(".toRegex(), "") name = name.replace("\\[".toRegex(), "") diff --git a/common/dependency-injection/src/desktopMain/kotlin/com/shabinder/common/di/DesktopActual.kt b/common/dependency-injection/src/desktopMain/kotlin/com/shabinder/common/di/DesktopActual.kt index f03e6fcd..788aed87 100644 --- a/common/dependency-injection/src/desktopMain/kotlin/com/shabinder/common/di/DesktopActual.kt +++ b/common/dependency-injection/src/desktopMain/kotlin/com/shabinder/common/di/DesktopActual.kt @@ -17,6 +17,7 @@ package com.shabinder.common.di import com.shabinder.common.di.providers.YoutubeMp3 +import com.shabinder.common.di.providers.getData import com.shabinder.common.di.utils.ParallelExecutor import com.shabinder.common.models.AllPlatforms import com.shabinder.common.models.DownloadResult diff --git a/common/dependency-injection/src/desktopMain/kotlin/com/shabinder/common/di/DesktopDir.kt b/common/dependency-injection/src/desktopMain/kotlin/com/shabinder/common/di/DesktopDir.kt index 58bea06e..594142ac 100644 --- a/common/dependency-injection/src/desktopMain/kotlin/com/shabinder/common/di/DesktopDir.kt +++ b/common/dependency-injection/src/desktopMain/kotlin/com/shabinder/common/di/DesktopDir.kt @@ -45,6 +45,13 @@ actual class Dir actual constructor( ) { companion object { const val DirKey = "downloadDir" + const val AnalyticsKey = "analytics" + } + + actual val isAnalyticsEnabled get() = settings.getBooleanOrNull(AnalyticsKey) ?: false + + actual fun enableAnalytics() { + settings.putBoolean(AnalyticsKey,true) } init { diff --git a/common/dependency-injection/src/iosMain/kotlin/com.shabinder.common.di/IOSActual.kt b/common/dependency-injection/src/iosMain/kotlin/com.shabinder.common.di/IOSActual.kt index 41a8475e..a4dee917 100644 --- a/common/dependency-injection/src/iosMain/kotlin/com.shabinder.common.di/IOSActual.kt +++ b/common/dependency-injection/src/iosMain/kotlin/com.shabinder.common.di/IOSActual.kt @@ -1,5 +1,6 @@ package com.shabinder.common.di +import com.shabinder.common.di.providers.getData import com.shabinder.common.di.utils.ParallelExecutor import com.shabinder.common.models.AllPlatforms import com.shabinder.common.models.DownloadResult diff --git a/common/dependency-injection/src/iosMain/kotlin/com.shabinder.common.di/IOSDir.kt b/common/dependency-injection/src/iosMain/kotlin/com.shabinder.common.di/IOSDir.kt index 056870f5..6e710ce6 100644 --- a/common/dependency-injection/src/iosMain/kotlin/com.shabinder.common.di/IOSDir.kt +++ b/common/dependency-injection/src/iosMain/kotlin/com.shabinder.common.di/IOSDir.kt @@ -21,7 +21,6 @@ import platform.Foundation.sendSynchronousRequest import platform.Foundation.writeToFile import platform.UIKit.UIImage import platform.UIKit.UIImageJPEGRepresentation -import java.lang.System actual class Dir actual constructor( val logger: Kermit, @@ -30,6 +29,13 @@ actual class Dir actual constructor( ) { companion object { const val DirKey = "downloadDir" + const val AnalyticsKey = "analytics" + } + + actual val isAnalyticsEnabled get() = settings.getBooleanOrNull(AnalyticsKey) ?: false + + actual fun enableAnalytics() { + settings.putBoolean(AnalyticsKey,true) } actual fun isPresent(path: String): Boolean = NSFileManager.defaultManager.fileExistsAtPath(path) diff --git a/common/dependency-injection/src/jsMain/kotlin/com/shabinder/common/di/WebDir.kt b/common/dependency-injection/src/jsMain/kotlin/com/shabinder/common/di/WebDir.kt index c6ea859c..711a331b 100644 --- a/common/dependency-injection/src/jsMain/kotlin/com/shabinder/common/di/WebDir.kt +++ b/common/dependency-injection/src/jsMain/kotlin/com/shabinder/common/di/WebDir.kt @@ -39,8 +39,17 @@ actual class Dir actual constructor( ) { companion object { const val DirKey = "downloadDir" + const val AnalyticsKey = "analytics" } + actual val isAnalyticsEnabled get() = settings.getBooleanOrNull(AnalyticsKey) ?: false + + actual fun enableAnalytics() { + settings.putBoolean(AnalyticsKey,true) + } + + actual fun setDownloadDirectory(newBasePath:String) = settings.putString(DirKey,newBasePath) + /*init { createDirectories() }*/ diff --git a/common/list/src/commonMain/kotlin/com/shabinder/common/list/SpotiFlyerList.kt b/common/list/src/commonMain/kotlin/com/shabinder/common/list/SpotiFlyerList.kt index c36180b1..ff4e1980 100644 --- a/common/list/src/commonMain/kotlin/com/shabinder/common/list/SpotiFlyerList.kt +++ b/common/list/src/commonMain/kotlin/com/shabinder/common/list/SpotiFlyerList.kt @@ -65,6 +65,11 @@ interface SpotiFlyerList { val link: String val listOutput: Consumer val downloadProgressFlow: MutableSharedFlow> + val listAnalytics: Analytics + } + + interface Analytics { + } sealed class Output { diff --git a/common/main/src/commonMain/kotlin/com/shabinder/common/main/SpotiFlyerMain.kt b/common/main/src/commonMain/kotlin/com/shabinder/common/main/SpotiFlyerMain.kt index 57a8cefe..81df8e87 100644 --- a/common/main/src/commonMain/kotlin/com/shabinder/common/main/SpotiFlyerMain.kt +++ b/common/main/src/commonMain/kotlin/com/shabinder/common/main/SpotiFlyerMain.kt @@ -25,12 +25,13 @@ import com.shabinder.common.main.integration.SpotiFlyerMainImpl import com.shabinder.common.models.Consumer import com.shabinder.common.models.DownloadRecord import com.shabinder.database.Database -import kotlinx.coroutines.flow.Flow interface SpotiFlyerMain { val models: Value + val analytics: Analytics + /* * We Intend to Move to List Screen * Note: Implementation in Root @@ -57,6 +58,11 @@ interface SpotiFlyerMain { val storeFactory: StoreFactory val database: Database? val dir: Dir + val mainAnalytics: Analytics + } + + interface Analytics { + fun donationDialogVisit() } sealed class Output { diff --git a/common/main/src/commonMain/kotlin/com/shabinder/common/main/integration/SpotiFlyerMainImpl.kt b/common/main/src/commonMain/kotlin/com/shabinder/common/main/integration/SpotiFlyerMainImpl.kt index 554991a2..861aaf75 100644 --- a/common/main/src/commonMain/kotlin/com/shabinder/common/main/integration/SpotiFlyerMainImpl.kt +++ b/common/main/src/commonMain/kotlin/com/shabinder/common/main/integration/SpotiFlyerMainImpl.kt @@ -18,7 +18,6 @@ package com.shabinder.common.main.integration import co.touchlab.stately.ensureNeverFrozen import com.arkivanov.decompose.ComponentContext -import com.arkivanov.decompose.lifecycle.doOnDestroy import com.arkivanov.decompose.value.Value import com.shabinder.common.di.Picture import com.shabinder.common.di.utils.asValue @@ -40,9 +39,6 @@ internal class SpotiFlyerMainImpl( init { instanceKeeper.ensureNeverFrozen() - lifecycle.doOnDestroy { - cache.invalidateAll() - } } private val store = @@ -55,11 +51,13 @@ internal class SpotiFlyerMainImpl( private val cache = Cache.Builder .newBuilder() - .maximumCacheSize(20) + .maximumCacheSize(25) .build() override val models: Value = store.asValue() + override val analytics = mainAnalytics + override fun onLinkSearch(link: String) { if (methods.value.isInternetAvailable) mainOutput.callback(Output.Search(link = link)) else methods.value.showPopUpMessage("Check Network Connection Please") diff --git a/common/root/src/commonMain/kotlin/com/shabinder/common/root/SpotiFlyerRoot.kt b/common/root/src/commonMain/kotlin/com/shabinder/common/root/SpotiFlyerRoot.kt index 23d57065..74ede10e 100644 --- a/common/root/src/commonMain/kotlin/com/shabinder/common/root/SpotiFlyerRoot.kt +++ b/common/root/src/commonMain/kotlin/com/shabinder/common/root/SpotiFlyerRoot.kt @@ -54,6 +54,14 @@ interface SpotiFlyerRoot { val directories: Dir val downloadProgressReport: MutableSharedFlow> val actions:Actions + val analytics: Analytics + } + + interface Analytics { + fun appLaunchEvent() + fun homeScreenVisit() + fun listScreenVisit() + fun donationDialogVisit() } } diff --git a/common/root/src/commonMain/kotlin/com/shabinder/common/root/integration/SpotiFlyerRootImpl.kt b/common/root/src/commonMain/kotlin/com/shabinder/common/root/integration/SpotiFlyerRootImpl.kt index e093b6da..1354681f 100644 --- a/common/root/src/commonMain/kotlin/com/shabinder/common/root/integration/SpotiFlyerRootImpl.kt +++ b/common/root/src/commonMain/kotlin/com/shabinder/common/root/integration/SpotiFlyerRootImpl.kt @@ -37,6 +37,7 @@ import com.shabinder.common.models.AllPlatforms import com.shabinder.common.models.Consumer import com.shabinder.common.models.methods import com.shabinder.common.root.SpotiFlyerRoot +import com.shabinder.common.root.SpotiFlyerRoot.Analytics import com.shabinder.common.root.SpotiFlyerRoot.Child import com.shabinder.common.root.SpotiFlyerRoot.Dependencies import com.shabinder.common.root.callbacks.SpotiFlyerRootCallBacks @@ -49,7 +50,8 @@ internal class SpotiFlyerRootImpl( componentContext: ComponentContext, private val main: (ComponentContext, output:Consumer)->SpotiFlyerMain, private val list: (ComponentContext, link:String, output:Consumer)->SpotiFlyerList, - private val actions: Actions + private val actions: Actions, + private val analytics: Analytics ) : SpotiFlyerRoot, ComponentContext by componentContext { constructor( @@ -72,7 +74,8 @@ internal class SpotiFlyerRootImpl( dependencies ) }, - actions = dependencies.actions.freeze() + actions = dependencies.actions.freeze(), + analytics = dependencies.analytics ) { instanceKeeper.ensureNeverFrozen() methods.value = dependencies.actions.freeze() @@ -113,16 +116,23 @@ internal class SpotiFlyerRootImpl( private fun onMainOutput(output: SpotiFlyerMain.Output) = when (output) { - is SpotiFlyerMain.Output.Search -> router.push(Configuration.List(link = output.link)) + is SpotiFlyerMain.Output.Search -> { + router.push(Configuration.List(link = output.link)) + analytics.listScreenVisit() + } } private fun onListOutput(output: SpotiFlyerList.Output): Unit = when (output) { - is SpotiFlyerList.Output.Finished -> router.pop() + is SpotiFlyerList.Output.Finished -> { + router.pop() + analytics.homeScreenVisit() + } } private fun authenticateSpotify(spotifyProvider: SpotifyProvider, override:Boolean){ GlobalScope.launch(Dispatchers.Default) { + analytics.appLaunchEvent() /*Authenticate Spotify Client*/ spotifyProvider.authenticateSpotifyClient(override) } @@ -143,6 +153,9 @@ private fun spotiFlyerMain(componentContext: ComponentContext, output: Consumer< dependencies = object : SpotiFlyerMain.Dependencies, Dependencies by dependencies { override val mainOutput: Consumer = output override val dir: Dir = directories + override val mainAnalytics = object : SpotiFlyerMain.Analytics { + override fun donationDialogVisit() = analytics.donationDialogVisit() + } } ) @@ -155,5 +168,6 @@ private fun spotiFlyerList(componentContext: ComponentContext, link: String, out override val link: String = link override val listOutput: Consumer = output override val downloadProgressFlow = downloadProgressReport + override val listAnalytics = object : SpotiFlyerList.Analytics {} } ) From ea48d929a409a1184d8cbdae7672a149072e52e6 Mon Sep 17 00:00:00 2001 From: shabinder Date: Fri, 14 May 2021 02:51:33 +0530 Subject: [PATCH 2/2] (WIP)Android SAF --- .../com/shabinder/spotiflyer/MainActivity.kt | 40 +-- .../com/shabinder/common/di/AndroidDir.kt | 277 ++++++------------ .../common/di/worker/ForegroundService.kt | 93 ++---- .../com/shabinder/common/di/worker/Utils.kt | 5 +- .../kotlin/com/shabinder/common/di/Dir.kt | 2 +- 5 files changed, 125 insertions(+), 292 deletions(-) diff --git a/android/src/main/java/com/shabinder/spotiflyer/MainActivity.kt b/android/src/main/java/com/shabinder/spotiflyer/MainActivity.kt index e9e70c0f..14323d76 100644 --- a/android/src/main/java/com/shabinder/spotiflyer/MainActivity.kt +++ b/android/src/main/java/com/shabinder/spotiflyer/MainActivity.kt @@ -46,7 +46,6 @@ import com.arkivanov.decompose.extensions.compose.jetbrains.rememberRootComponen import com.arkivanov.mvikotlin.logging.store.LoggingStoreFactory import com.arkivanov.mvikotlin.main.store.DefaultStoreFactory import com.github.k1rakishou.fsaf.FileChooser -import com.github.k1rakishou.fsaf.FileManager import com.github.k1rakishou.fsaf.callback.directory.DirectoryChooserCallback import com.google.accompanist.insets.ProvideWindowInsets import com.google.accompanist.insets.navigationBarsPadding @@ -58,7 +57,6 @@ import com.shabinder.common.models.Actions import com.shabinder.common.models.DownloadStatus import com.shabinder.common.models.PlatformActions import com.shabinder.common.models.PlatformActions.Companion.SharedPreferencesKey -import com.shabinder.common.models.SpotiFlyerBaseDir import com.shabinder.common.models.Status import com.shabinder.common.models.TrackDetails import com.shabinder.common.models.methods @@ -148,34 +146,13 @@ class MainActivity : ComponentActivity() { @Suppress("DEPRECATION") private fun setUpOnPrefClickListener() { - /*Get User Permission to access External SD*//* + /*Get User Permission to access External SD*/ val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).apply { addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION) addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION) } - startActivityForResult(intent, externalSDWriteAccess)*/ - val fileChooser = FileChooser(applicationContext) - fileChooser.openChooseDirectoryDialog(object : DirectoryChooserCallback() { - override fun onResult(uri: Uri) { - println("treeUri = $uri") - // Can be only used using SAF - contentResolver.takePersistableUriPermission(uri, - Intent.FLAG_GRANT_READ_URI_PERMISSION or - Intent.FLAG_GRANT_WRITE_URI_PERMISSION) - val treeDocumentFile = DocumentFile.fromTreeUri(applicationContext, uri) - - dir.setDownloadDirectory(uri) - showPopUpMessage("New Download Directory Set") - GlobalScope.launch { - dir.createDirectories() - } - } - - override fun onCancel(reason: String) { - println("Canceled by user") - } - }) + startActivityForResult(intent, externalSDWriteAccess) } private fun showPopUpMessage(string: String, long: Boolean = false) { @@ -327,24 +304,19 @@ class MainActivity : ComponentActivity() { externalSDWriteAccess -> { // Can be only used using SAF - /*if (resultCode == RESULT_OK) { + if (resultCode == RESULT_OK) { val treeUri: Uri? = data?.data - if (treeUri == null){ + if (treeUri == null) { showPopUpMessage("Some Error Occurred While Setting New Download Directory") }else { // Persistently save READ & WRITE Access to whole Selected Directory Tree contentResolver.takePersistableUriPermission(treeUri, Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION) - dir.setDownloadDirectory(com.shabinder.common.models.File( - DocumentFile.fromTreeUri(applicationContext,treeUri)?.createDirectory("SpotiFlyer")!!) - ) + dir.setDownloadDirectory(treeUri) showPopUpMessage("New Download Directory Set") - GlobalScope.launch { - dir.createDirectories() - } } - }*/ + } } } } diff --git a/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/di/AndroidDir.kt b/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/di/AndroidDir.kt index e40ebd4a..d5512fe5 100644 --- a/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/di/AndroidDir.kt +++ b/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/di/AndroidDir.kt @@ -16,17 +16,16 @@ package com.shabinder.common.di +import android.content.ContentValues import android.content.Context import android.graphics.Bitmap import android.graphics.BitmapFactory import android.net.Uri import android.os.Environment -import android.util.Log +import android.provider.MediaStore import androidx.compose.ui.graphics.asImageBitmap -import androidx.documentfile.provider.DocumentFile import co.touchlab.kermit.Kermit import com.github.k1rakishou.fsaf.FileManager -import com.github.k1rakishou.fsaf.file.AbstractFile import com.github.k1rakishou.fsaf.file.DirectorySegment import com.github.k1rakishou.fsaf.file.FileSegment import com.github.k1rakishou.fsaf.manager.base_directory.BaseDirectory @@ -45,13 +44,13 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.koin.core.component.KoinComponent import org.koin.core.component.get -import java.io.FileInputStream import java.io.FileOutputStream import java.io.IOException import java.io.InputStream import java.net.HttpURLConnection import java.net.URL + /* * Ignore Deprecation * Deprecation is only a Suggestion P-) @@ -60,22 +59,32 @@ import java.net.URL actual class Dir actual constructor( private val logger: Kermit, private val settings: Settings, - private val spotiFlyerDatabase: SpotiFlyerDatabase, + spotiFlyerDatabase: SpotiFlyerDatabase, ): KoinComponent { private val context: Context = get() val fileManager = FileManager(context) init { - fileManager.registerBaseDir(SpotiFlyerBaseDir({ getDirType() }, - getJavaFile = { - java.io.File( - Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC).toString() - + "/SpotiFlyer/" - ) - }, - getSAFUri = { null } - )) + fileManager.apply { + registerBaseDir(SpotiFlyerBaseDir({ getDirType() }, + getJavaFile = { + java.io.File( + Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC) + .toString() + + "/SpotiFlyer/" + ) + }, + getSAFUri = { + settings.getStringOrNull(DirKey)?.let { + Uri.parse(it) + } + } + )) + defaultDir().documentFile?.let { + createSnapshot(it,true) + } + } } companion object { @@ -105,27 +114,32 @@ actual class Dir actual constructor( actual fun setDownloadDirectory(newBasePath:File) = settings.putString( DirKey, - newBasePath.documentFile?.getFullPath()!! + newBasePath.documentFile!!.getFullPath() ) fun setDownloadDirectory(treeUri:Uri) { - fileManager.registerBaseDir(SpotiFlyerBaseDir( - { getDirType() }, - getJavaFile = { - null - }, - getSAFUri = { - treeUri + try { + fileManager.apply { + registerBaseDir(SpotiFlyerBaseDir( + { getDirType() }, + getJavaFile = { + null + }, + getSAFUri = { + treeUri + } + )) + fromUri(treeUri)?.let { createSnapshot(it,true) } } - )) + } catch (e:IllegalArgumentException) { + methods.value.showPopUpMessage("This Directory is already set as Download Directory") + } + GlobalScope.launch { + setDownloadDirectory(File(fileManager.fromUri(treeUri))) + createDirectories() + } } - @Suppress("DEPRECATION")// By Default Save Files to /Music/SpotiFlyer/ - private val defaultBaseDir = SpotiFlyerBaseDir({ getDirType() }, - getJavaFile = {java.io.File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC).toString() + "/SpotiFlyer/")}, - getSAFUri = { null } - ) - // Image Cache Path // We Will Handling Image relating operations using java.io.File (reason: Faster) actual val imageCachePath: String get() = methods.value.platformActions.imageCacheDir.absolutePath + "/" @@ -145,25 +159,9 @@ actual class Dir actual constructor( fileManager.create(dirPath.documentFile!!) } } - - /*try { - val yourAppDir = File(dirPath) - - if (!yourAppDir.exists() && !yourAppDir.isDirectory) { // create empty directory - if (yourAppDir.mkdirs()) { logger.i { "$dirPath created" } } else { - logger.e { "Unable to create Dir: $dirPath!" } - } - } else { - logger.i { "$dirPath already exists" } - } - } catch (e: SecurityException) { - //TRY USING SAF - Log.d("Directory","USING SAF to create $dirPath") - val file = DocumentFile.fromTreeUri(context, Uri.parse(defaultDir())) - DocumentFile.fromFile() - }*/ } + @Suppress("unused") actual suspend fun clearCache(): Unit = withContext(dispatcherIO) { try { java.io.File(imageCachePath).deleteRecursively() @@ -178,20 +176,20 @@ actual class Dir actual constructor( trackDetails: TrackDetails, postProcess: (track: TrackDetails) -> Unit, ): Unit = withContext(dispatcherIO) { - val songFile = java.io.File(imageCachePath+"Tracks/"+ removeIllegalChars(trackDetails.title) + ".mp3") + val mediaFile = java.io.File(imageCachePath+"Tracks/"+ removeIllegalChars(trackDetails.title) + ".mp3") try { - /*Make intermediate Dirs if they don't exist yet*/ - if(!songFile.exists()) { - songFile.parentFile?.mkdirs() + if(!mediaFile.exists()) { + mediaFile.parentFile?.mkdirs() } + // Write Bytes to Media File + mediaFile.writeBytes(mp3ByteArray) - if(mp3ByteArray.isNotEmpty()) songFile.writeBytes(mp3ByteArray) - - Mp3File(songFile) + // Add Metadata to Media File + Mp3File(mediaFile) .removeAllTags() .setId3v1Tags(trackDetails) - .setId3v2TagsAndSaveFile(trackDetails,songFile.absolutePath) + .setId3v2TagsAndSaveFile(trackDetails,mediaFile.absolutePath) // Copy File to Desired Location val documentFile = when(getDirType()){ @@ -201,105 +199,52 @@ actual class Dir actual constructor( BaseDirectory.ActiveBaseDirType.JavaFileBaseDir -> { fileManager.fromPath(trackDetails.outputFilePath) } - }.also { fileManager.create(it!!) } + }.also { + // Create Desired File if it doesn't exists yet + fileManager.create(it!!) + } try { fileManager.copyFileContents( - fileManager.fromRawFile(songFile), + fileManager.fromRawFile(mediaFile), documentFile!! ) - songFile.deleteOnExit() - /*val inStream = FileInputStream(songFile) - - val buffer = ByteArray(1024) - var readLen: Int - while (inStream.read(buffer).also { readLen = it } != -1) { - outStream?.write(buffer, 0, readLen) - } - inStream.close() - // write the output file (You have now copied the file) - outStream?.flush() - outStream?.close()*/ - + mediaFile.deleteOnExit() }catch (e:Exception) { e.printStackTrace() } documentFile?.let { - addToLibrary(File(it)) + addToLibrary(File(it),trackDetails) } - - /*when (trackDetails.outputFilePath.substringAfterLast('.')) { - ".mp3" -> { - Mp3File(songFile) - .removeAllTags() - .setId3v1Tags(trackDetails) - .setId3v2TagsAndSaveFile(trackDetails,songFile.absolutePath) - - // Copy File to DocumentUri - val documentFile = DocumentFile.fromSingleUri(context,Uri.parse(trackDetails.outputFilePath)) - try { - val outStream = context.contentResolver.openOutputStream(documentFile?.uri!!) - val inStream = FileInputStream(songFile) - - val buffer = ByteArray(1024) - var readLen: Int - while (inStream.read(buffer).also { readLen = it } != -1) { - outStream?.write(buffer, 0, readLen) - } - inStream.close() - // write the output file (You have now copied the file) - outStream?.flush() - outStream?.close() - - }catch (e:Exception) { - e.printStackTrace() - } - - documentFile?.let { - addToLibrary(File(it)) - } - } - ".m4a" -> { - *//*FFmpeg.executeAsync( - "-i ${m4aFile.absolutePath} -y -b:a 160k -acodec libmp3lame -vn ${m4aFile.absolutePath.substringBeforeLast('.') + ".mp3"}" - ){ _, returnCode -> - when (returnCode) { - Config.RETURN_CODE_SUCCESS -> { - //FFMPEG task Completed - logger.d{ "Async command execution completed successfully." } - scope.launch { - Mp3File(File(m4aFile.absolutePath.substringBeforeLast('.') + ".mp3")) - .removeAllTags() - .setId3v1Tags(trackDetails) - .setId3v2TagsAndSaveFile(trackDetails) - addToLibrary(m4aFile.absolutePath.substringBeforeLast('.') + ".mp3") - } - } - Config.RETURN_CODE_CANCEL -> { - logger.d{"Async command execution cancelled by user."} - } - else -> { - logger.d { "Async command execution failed with rc=$returnCode" } - } - } - }*//* - } - else -> { - // TODO - } - }*/ }catch (e:Exception){ - withContext(Dispatchers.Main){ - //Toast.makeText(appContext,"Could Not Create File:\n${songFile.absolutePath}",Toast.LENGTH_SHORT).show() - } - if(songFile.exists()) songFile.delete() - logger.e { "${songFile.absolutePath} could not be created" } + e.printStackTrace() + if(mediaFile.exists()) mediaFile.delete() + logger.e { "${mediaFile.absolutePath} could not be created" } } } - actual fun addToLibrary(file: File) { -// methods.value.platformActions.addToLibrary(path) + actual fun addToLibrary(file: File,track: TrackDetails) { + try { + when (getDirType()) { + BaseDirectory.ActiveBaseDirType.SafBaseDir -> { + val values = ContentValues(4).apply { + put(MediaStore.Audio.Media.TITLE, track.title) + put(MediaStore.Audio.Media.DISPLAY_NAME, track.title) + put(MediaStore.Audio.Media.DATE_ADDED, + (System.currentTimeMillis() / 1000).toInt()) + put(MediaStore.Audio.Media.MIME_TYPE, "audio/mpeg") + } + context.contentResolver.insert(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, + values) + } + BaseDirectory.ActiveBaseDirType.JavaFileBaseDir -> { + file.documentFile?.getFullPath()?.let { + methods.value.platformActions.addToLibrary(it) + } + } + } + } catch (e:Exception) { e.printStackTrace() } } actual suspend fun loadImage(url: String): Picture = withContext(dispatcherIO){ @@ -319,6 +264,7 @@ actual class Dir actual constructor( } } + @Suppress("BlockingMethodInNonBlockingContext") actual suspend fun cacheImage(image: Any, path: String):Unit = withContext(dispatcherIO) { try { java.io.File(path).parentFile?.mkdirs() @@ -330,6 +276,7 @@ actual class Dir actual constructor( } } + @Suppress("BlockingMethodInNonBlockingContext") private suspend fun freshImage(url: String): Bitmap? = withContext(dispatcherIO) { try { val source = URL(url) @@ -372,59 +319,17 @@ actual class Dir actual constructor( subFolder: String, extension: String, ):File { + // Create Intermediate Directories val file = fileManager.create( defaultDir().documentFile!!, //Base Dir DirectorySegment(removeIllegalChars(type)), DirectorySegment(removeIllegalChars(subFolder)), - + FileSegment(removeIllegalChars(itemName) + extension) ) - return File(file?.clone(FileSegment(removeIllegalChars(itemName) + extension)))/*.also { - if(fileManager.getLength(it.documentFile!!) == 0L){ - fileManager.delete(it.documentFile!!) - } - }*/ - /*GlobalScope.launch { - // Create Intermediate Directories - var file = defaultDir().documentFile - file = file.findFile(removeIllegalChars(type)) - ?: file.createDirectory(removeIllegalChars(type)) - ?: throw Exception("Couldn't Find/Create $type Directory") - - if (subFolder.isNotEmpty()) file.findFile(removeIllegalChars(subFolder)) - ?: file.createDirectory(removeIllegalChars(subFolder)) - ?: throw Exception("Couldn't Find/Create $subFolder Directory") - - } - val sep = "%2F" - val finalUri = defaultDir().documentFile.uri.toString() + sep + - removeIllegalChars(type) + sep + - removeIllegalChars(subFolder) + sep + - removeIllegalChars(itemName) + extension - return File( - DocumentFile.fromSingleUri(context,Uri.parse(finalUri))!! - ).also { - Log.d("Final Output File",it.documentFile.uri.toString()) - }*/ - - - /*file = file?.findFile(removeIllegalChars(type)) - ?: file?.createDirectory(removeIllegalChars(type)) - ?: throw Exception("Couldn't Find/Create $type Directory") - - if (subFolder.isNotEmpty()) file = file.findFile(removeIllegalChars(subFolder)) - ?: file.createDirectory(removeIllegalChars(subFolder)) - ?: throw Exception("Couldn't Find/Create $subFolder Directory") - - // TODO check Mime - file = file.findFile(removeIllegalChars(itemName)) - ?: file.createFile("audio/mpeg",removeIllegalChars(itemName)) - ?: throw Exception("Couldn't Find/Create ${removeIllegalChars(itemName) + extension} File") - Log.d("Final Output File",file.uri.toString()) - return File(file).also { - val size = it.documentFile.length() - Log.d("File size", size.toString()) - if(size == 0L) it.documentFile.delete() - }*/ + if(fileManager.getLength(it.documentFile!!) == 0L) fileManager.delete(it.documentFile!!) + } + + //?.clone(FileSegment(removeIllegalChars(itemName) + extension))) } } diff --git a/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/di/worker/ForegroundService.kt b/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/di/worker/ForegroundService.kt index 58414e5a..78ff1919 100644 --- a/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/di/worker/ForegroundService.kt +++ b/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/di/worker/ForegroundService.kt @@ -18,39 +18,31 @@ package com.shabinder.common.di.worker import android.annotation.SuppressLint import android.app.DownloadManager -import android.app.DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED import android.app.Notification import android.app.NotificationChannel import android.app.NotificationManager import android.app.PendingIntent import android.app.PendingIntent.FLAG_CANCEL_CURRENT import android.app.Service -import android.content.BroadcastReceiver import android.content.Context import android.content.Intent -import android.content.IntentFilter -import android.net.Uri import android.os.Build import android.os.IBinder import android.os.PowerManager import android.util.Log import androidx.annotation.RequiresApi import androidx.core.app.NotificationCompat -import androidx.core.net.toUri import co.touchlab.kermit.Kermit -import com.github.k1rakishou.fsaf.FileManager -import com.github.k1rakishou.fsaf.file.AbstractFile import com.shabinder.common.di.* import com.shabinder.common.di.providers.getData import com.shabinder.common.di.utils.ParallelExecutor import com.shabinder.common.models.DownloadResult import com.shabinder.common.models.DownloadStatus +import com.shabinder.common.models.Status import com.shabinder.common.models.TrackDetails import com.shabinder.downloader.models.formats.Format -import com.shabinder.common.models.Status import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.Job import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.flow.collect @@ -128,8 +120,7 @@ class ForegroundService : Service(), CoroutineScope { val downloadObjects: ArrayList? = ( it.getParcelableArrayListExtra("object") ?: it.extras?.getParcelableArrayList( - "object" - ) + "object") ) downloadObjects?.let { list -> @@ -216,13 +207,11 @@ class ForegroundService : Service(), CoroutineScope { is DownloadResult.Error -> { launch { logger.d(tag) { it.message } - /*logger.d(tag) { "${track.title} Requesting Download thru Android DM" } - downloadUsingDM(url, track.outputFilePath, track)*/ removeFromNotification("Downloading ${track.title}") failed++ + updateNotification() + sendTrackBroadcast(Status.FAILED.name,track) } - updateNotification() - sendTrackBroadcast(Status.FAILED.name,track) } is DownloadResult.Progress -> { @@ -252,14 +241,10 @@ class ForegroundService : Service(), CoroutineScope { } logger.d(tag) { "${track.title} Download Completed" } downloaded++ - } catch ( - e: Exception - ) { - // Try downloading using android DM + } catch (e: Exception) { + // Download Failed logger.d(tag) { "${track.title} Download Failed! Error:Fetch!!!!" } failed++ - /*logger.d(tag) { "${track.title} Requesting Download thru Android DM" } - downloadUsingDM(url, track.outputFilePath, track)*/ } removeFromNotification("Downloading ${track.title}") } @@ -267,54 +252,6 @@ class ForegroundService : Service(), CoroutineScope { } } - /** - * If Custom Downloader Fails , Android Download Manager To RESCUE!! - **/ - private fun downloadUsingDM(url: String, outputDir: String, track: TrackDetails) { - launch { - val uri = Uri.parse(url) - val request = DownloadManager.Request(uri).apply { - setAllowedNetworkTypes( - DownloadManager.Request.NETWORK_WIFI or - DownloadManager.Request.NETWORK_MOBILE - ) - setAllowedOverRoaming(false) - setTitle(track.title) - setDescription("Spotify Downloader Working Up here...") - setDestinationUri(File(outputDir).toUri()) - setNotificationVisibility(VISIBILITY_VISIBLE_NOTIFY_COMPLETED) - } - - // Start Download - val downloadID = downloadManager.enqueue(request) - logger.d("DownloadManager") { "Download Request Sent" } - - val onDownloadComplete: BroadcastReceiver = object : BroadcastReceiver() { - override fun onReceive(context: Context, intent: Intent) { - // Fetching the download id received with the broadcast - val id = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1) - // Checking if the received broadcast is for our enqueued download by matching download id - if (downloadID == id) { - allTracksStatus[track.title] = DownloadStatus.Converting - launch { dir.saveFileWithMetadata(byteArrayOf(), track){}; converted++ } - // Unregister this broadcast Receiver - this@ForegroundService.unregisterReceiver(this) - } - } - } - registerReceiver(onDownloadComplete, IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE)) - } - } - - /** - * This is the method that can be called to update the Notification - */ - private fun updateNotification() { - val mNotificationManager: NotificationManager = - getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - mNotificationManager.notify(notificationId, getNotification()) - } - private fun releaseWakeLock() { logger.d(tag) { "Releasing Wake Lock" } try { @@ -341,13 +278,17 @@ class ForegroundService : Service(), CoroutineScope { service.createNotificationChannel(channel) } + /* + * Time To Wrap UP + * - `Clean Up` and `Stop this Foreground Service` + * */ private fun killService() { launch { logger.d(tag) { "Killing Self" } messageList = mutableListOf("Cleaning And Exiting", "", "", "", "") downloadService.close() updateNotification() - // dir.defaultDir().documentFile?.let { cleanFiles(it,dir.fileManager,logger) } + dir.defaultDir().documentFile?.let { cleanFiles(it,dir.fileManager,logger) } cleanFiles(File(dir.imageCachePath + "Tracks/"),logger) messageList = mutableListOf("", "", "", "", "") releaseWakeLock() @@ -375,6 +316,9 @@ class ForegroundService : Service(), CoroutineScope { } } + /* + * Create A New Notification with all the updated data + * */ private fun getNotification(): Notification = NotificationCompat.Builder(this, channelId).run { setSmallIcon(R.drawable.ic_download_arrow) setContentTitle("Total: $total Completed:$converted Failed:$failed") @@ -402,6 +346,15 @@ class ForegroundService : Service(), CoroutineScope { updateNotification() } + /** + * This is the method that can be called to update the Notification + */ + private fun updateNotification() { + val mNotificationManager: NotificationManager = + getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + mNotificationManager.notify(notificationId, getNotification()) + } + private fun sendTrackBroadcast(action: String, track: TrackDetails) { val intent = Intent().apply { setAction(action) diff --git a/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/di/worker/Utils.kt b/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/di/worker/Utils.kt index 43b6830d..e20f1de1 100644 --- a/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/di/worker/Utils.kt +++ b/common/dependency-injection/src/androidMain/kotlin/com/shabinder/common/di/worker/Utils.kt @@ -37,7 +37,10 @@ fun cleanFiles(directory: AbstractFile,fm: FileManager,logger: Kermit) { if (fm.isDirectory(file)) { cleanFiles(file, fm, logger) } else if (fm.isFile(file)) { - if (file.getFullPath().substringAfterLast(".") != "mp3") { + if (file.getFullPath().substringAfterLast(".") != "mp3" + || + fm.getLength(file) == 0L + ) { logger.d("Files Cleaning") { "Cleaning ${file.getFullPath()}" } fm.delete(file) } diff --git a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/Dir.kt b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/Dir.kt index ef7475c9..35840522 100644 --- a/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/Dir.kt +++ b/common/dependency-injection/src/commonMain/kotlin/com/shabinder/common/di/Dir.kt @@ -56,7 +56,7 @@ expect class Dir ( suspend fun loadImage(url: String): Picture suspend fun clearCache() suspend fun saveFileWithMetadata(mp3ByteArray: ByteArray, trackDetails: TrackDetails,postProcess:(track: TrackDetails)->Unit = {}) - fun addToLibrary(file: File) + fun addToLibrary(file: File,track: TrackDetails) fun finalOutputFile(itemName: String, type: String, subFolder: String, extension: String = ".mp3"): File fun finalOutputPath(itemName: String, type: String, subFolder: String, extension: String = ".mp3"): String }