diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 20e89529..a3538b5a 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -68,6 +68,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/java/dev/brahmkshatriya/echo/EchoApplication.kt b/app/src/main/java/dev/brahmkshatriya/echo/EchoApplication.kt
index 0159df94..335407e7 100644
--- a/app/src/main/java/dev/brahmkshatriya/echo/EchoApplication.kt
+++ b/app/src/main/java/dev/brahmkshatriya/echo/EchoApplication.kt
@@ -10,6 +10,7 @@ import com.google.android.material.color.DynamicColors
import com.google.android.material.color.DynamicColorsOptions
import com.google.android.material.color.ThemeUtils
import dagger.hilt.android.HiltAndroidApp
+import dev.brahmkshatriya.echo.ExtensionOpenerActivity.Companion.cleanupTempApks
import dev.brahmkshatriya.echo.ui.exception.ExceptionFragment.Companion.getDetails
import dev.brahmkshatriya.echo.ui.exception.ExceptionFragment.Companion.getTitle
import dev.brahmkshatriya.echo.ui.settings.LookFragment.Companion.AMOLED_KEY
@@ -39,6 +40,7 @@ class EchoApplication : Application() {
//UI
applyLocale(settings)
applyUiChanges(this, settings)
+ cleanupTempApks()
//Crash Handling
Thread.setDefaultUncaughtExceptionHandler { _, exception ->
diff --git a/app/src/main/java/dev/brahmkshatriya/echo/ExtensionOpenerActivity.kt b/app/src/main/java/dev/brahmkshatriya/echo/ExtensionOpenerActivity.kt
new file mode 100644
index 00000000..35b3c431
--- /dev/null
+++ b/app/src/main/java/dev/brahmkshatriya/echo/ExtensionOpenerActivity.kt
@@ -0,0 +1,75 @@
+package dev.brahmkshatriya.echo
+
+import android.app.Activity
+import android.content.Context
+import android.content.Intent
+import android.net.Uri
+import android.widget.Toast
+import androidx.activity.viewModels
+import androidx.core.net.toFile
+import androidx.core.net.toUri
+import androidx.fragment.app.FragmentActivity
+import androidx.lifecycle.lifecycleScope
+import dev.brahmkshatriya.echo.ui.extension.ExtensionInstallerBottomSheet
+import dev.brahmkshatriya.echo.viewmodels.ExtensionViewModel
+import kotlinx.coroutines.launch
+import java.io.File
+
+class ExtensionOpenerActivity : Activity() {
+ override fun onStart() {
+ super.onStart()
+ val uri = intent.data
+
+ val file = when (uri?.scheme) {
+ "content" -> getTempFile(uri)
+ else -> null
+ }
+
+ if (file == null) Toast.makeText(
+ this, getString(R.string.could_not_find_the_file), Toast.LENGTH_SHORT
+ ).show()
+
+ finish()
+ val startIntent = Intent(this, MainActivity::class.java)
+ startIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ startIntent.data = file?.let { Uri.fromFile(it) }
+ startActivity(startIntent)
+ }
+
+ private fun getTempFile(uri: Uri): File? {
+ val stream = contentResolver.openInputStream(uri) ?: return null
+ val bytes = stream.readBytes()
+ val tempFile = File.createTempFile("temp", ".apk", getTempApkDir())
+ tempFile.writeBytes(bytes)
+ return tempFile
+ }
+
+ companion object {
+ const val EXTENSION_INSTALLER = "extensionInstaller"
+
+ fun Context.getTempApkDir() = File(cacheDir, "apks").apply { mkdirs() }
+
+ fun Context.cleanupTempApks() {
+ getTempApkDir().deleteRecursively()
+ }
+
+ fun FragmentActivity.openExtensionInstaller(uri: Uri) {
+
+ ExtensionInstallerBottomSheet.newInstance(uri.toString())
+ .show(supportFragmentManager, null)
+
+ supportFragmentManager.setFragmentResultListener(EXTENSION_INSTALLER, this) { _, b ->
+ val file = b.getString("file")?.toUri()?.toFile()
+ val install = b.getBoolean("install")
+ val installAsApk = b.getBoolean("installAsApk")
+ val context = this
+ if (install && file != null) {
+ val extensionViewModel by viewModels()
+ lifecycleScope.launch {
+ extensionViewModel.install(context, file, installAsApk)
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/dev/brahmkshatriya/echo/MainActivity.kt b/app/src/main/java/dev/brahmkshatriya/echo/MainActivity.kt
index 5b206804..709090e4 100644
--- a/app/src/main/java/dev/brahmkshatriya/echo/MainActivity.kt
+++ b/app/src/main/java/dev/brahmkshatriya/echo/MainActivity.kt
@@ -18,16 +18,9 @@ import com.google.android.material.navigation.NavigationBarView
import com.google.android.material.navigationrail.NavigationRailView
import com.google.common.util.concurrent.ListenableFuture
import dagger.hilt.android.AndroidEntryPoint
-import dev.brahmkshatriya.echo.common.models.Album
-import dev.brahmkshatriya.echo.common.models.Artist
-import dev.brahmkshatriya.echo.common.models.EchoMediaItem
-import dev.brahmkshatriya.echo.common.models.EchoMediaItem.Companion.toMediaItem
-import dev.brahmkshatriya.echo.common.models.Playlist
-import dev.brahmkshatriya.echo.common.models.Track
-import dev.brahmkshatriya.echo.common.models.User
+import dev.brahmkshatriya.echo.ExtensionOpenerActivity.Companion.openExtensionInstaller
import dev.brahmkshatriya.echo.databinding.ActivityMainBinding
-import dev.brahmkshatriya.echo.ui.common.openFragment
-import dev.brahmkshatriya.echo.ui.item.ItemFragment
+import dev.brahmkshatriya.echo.ui.common.openItemFragmentFromUri
import dev.brahmkshatriya.echo.ui.settings.LookFragment.Companion.NAVBAR_GRADIENT
import dev.brahmkshatriya.echo.utils.animateTranslation
import dev.brahmkshatriya.echo.utils.checkAudioPermissions
@@ -38,7 +31,6 @@ import dev.brahmkshatriya.echo.utils.listenFuture
import dev.brahmkshatriya.echo.utils.observe
import dev.brahmkshatriya.echo.viewmodels.PlayerViewModel
import dev.brahmkshatriya.echo.viewmodels.PlayerViewModel.Companion.connectPlayerToUI
-import dev.brahmkshatriya.echo.viewmodels.SnackBar
import dev.brahmkshatriya.echo.viewmodels.SnackBar.Companion.configureSnackBar
import dev.brahmkshatriya.echo.viewmodels.UiViewModel
import dev.brahmkshatriya.echo.viewmodels.UiViewModel.Companion.isNightMode
@@ -83,9 +75,9 @@ class MainActivity : AppCompatActivity() {
ViewCompat.setOnApplyWindowInsetsListener(binding.root) { _, insets ->
uiViewModel.setSystemInsets(this, insets)
- val navBarSize = uiViewModel.systemInsets.value.bottom
+ val navBarSize = uiViewModel.systemInsets.value.bottom
val full = playerViewModel.settings.getBoolean(NAVBAR_GRADIENT, true)
- navView.createNavDrawable(isRail,navBarSize, !full)
+ navView.createNavDrawable(isRail, navBarSize, !full)
insets
}
@@ -152,47 +144,10 @@ class MainActivity : AppCompatActivity() {
return
}
val uri = data
- if (uri != null) {
- fun createSnack(id: Int) {
- val snackbar by viewModels()
- val message = getString(id)
- snackbar.create(SnackBar.Message(message))
- }
-
- val extensionType = uri.host
- when (extensionType) {
- "music" -> {
- val extensionId = uri.pathSegments.firstOrNull()
- if (extensionId == null) {
- createSnack(R.string.error_no_client)
- return
- }
- val type = uri.pathSegments.getOrNull(1)
- val id = uri.pathSegments.getOrNull(2)
- if (id == null) {
- createSnack(R.string.error_no_id)
- return
- }
- val name = uri.getQueryParameter("name").orEmpty()
- val item: EchoMediaItem? = when (type) {
- "user" -> User(id, name).toMediaItem()
- "artist" -> Artist(id, name).toMediaItem()
- "track" -> Track(id, name).toMediaItem()
- "album" -> Album(id, name).toMediaItem()
- "playlist" -> Playlist(id, name, false).toMediaItem()
- else -> null
- }
- if (item == null) {
- createSnack(R.string.error_invalid_type)
- return
- }
- openFragment(ItemFragment.newInstance(extensionId, item))
- }
-
- else -> {
- createSnack(R.string.invalid_extension_host)
- }
- }
+ println("URI: $uri")
+ when (uri?.scheme) {
+ "echo" -> openItemFragmentFromUri(uri)
+ "file" -> openExtensionInstaller(uri)
}
}
diff --git a/app/src/main/java/dev/brahmkshatriya/echo/PlayerService.kt b/app/src/main/java/dev/brahmkshatriya/echo/PlayerService.kt
index 7279b3c4..138878ec 100644
--- a/app/src/main/java/dev/brahmkshatriya/echo/PlayerService.kt
+++ b/app/src/main/java/dev/brahmkshatriya/echo/PlayerService.kt
@@ -137,8 +137,6 @@ class PlayerService : MediaLibraryService() {
}
}
- //TODO: Radio Item
- //TODO: Open .eapk files
//TODO: extension updater
//TODO: Spotify
//TODO: EQ, Pitch, Tempo, Reverb & Sleep Timer(5m, 10m, 15m, 30m, 45m, 1hr, End of track)
@@ -158,7 +156,7 @@ class PlayerService : MediaLibraryService() {
}
override fun onTaskRemoved(rootIntent: Intent?) {
- val stopPlayer = settings.getBoolean(CLOSE_PLAYER, false)
+ val stopPlayer = settings.getBoolean(CLOSE_PLAYER, true)
val player = mediaSession?.player ?: return stopSelf()
if (stopPlayer || !player.isPlaying) stopSelf()
}
diff --git a/app/src/main/java/dev/brahmkshatriya/echo/di/ExtensionModule.kt b/app/src/main/java/dev/brahmkshatriya/echo/di/ExtensionModule.kt
index e7881735..651a687d 100644
--- a/app/src/main/java/dev/brahmkshatriya/echo/di/ExtensionModule.kt
+++ b/app/src/main/java/dev/brahmkshatriya/echo/di/ExtensionModule.kt
@@ -2,6 +2,9 @@ package dev.brahmkshatriya.echo.di
import android.app.Application
import android.content.SharedPreferences
+import androidx.annotation.OptIn
+import androidx.media3.common.util.UnstableApi
+import androidx.media3.datasource.cache.SimpleCache
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@@ -21,9 +24,11 @@ import javax.inject.Singleton
@InstallIn(SingletonComponent::class)
class ExtensionModule {
+ @OptIn(UnstableApi::class)
@Provides
@Singleton
- fun provideOfflineExtension(context: Application) = OfflineExtension(context)
+ fun provideOfflineExtension(context: Application, cache: SimpleCache) =
+ OfflineExtension(context, cache)
@Provides
@Singleton
diff --git a/app/src/main/java/dev/brahmkshatriya/echo/extensions/ExtensionLoader.kt b/app/src/main/java/dev/brahmkshatriya/echo/extensions/ExtensionLoader.kt
index 7734cb11..e2669a93 100644
--- a/app/src/main/java/dev/brahmkshatriya/echo/extensions/ExtensionLoader.kt
+++ b/app/src/main/java/dev/brahmkshatriya/echo/extensions/ExtensionLoader.kt
@@ -6,9 +6,9 @@ import dev.brahmkshatriya.echo.common.Extension
import dev.brahmkshatriya.echo.common.LyricsExtension
import dev.brahmkshatriya.echo.common.MusicExtension
import dev.brahmkshatriya.echo.common.TrackerExtension
+import dev.brahmkshatriya.echo.common.clients.ExtensionClient
import dev.brahmkshatriya.echo.common.clients.LoginClient
import dev.brahmkshatriya.echo.common.helpers.ExtensionType
-import dev.brahmkshatriya.echo.common.helpers.ImportType
import dev.brahmkshatriya.echo.common.models.Metadata
import dev.brahmkshatriya.echo.common.providers.LyricsClientsProvider
import dev.brahmkshatriya.echo.common.providers.MusicClientsProvider
@@ -17,16 +17,9 @@ import dev.brahmkshatriya.echo.db.ExtensionDao
import dev.brahmkshatriya.echo.db.UserDao
import dev.brahmkshatriya.echo.db.models.UserEntity
import dev.brahmkshatriya.echo.db.models.UserEntity.Companion.toUser
-import dev.brahmkshatriya.echo.extensions.plugger.AndroidPluginLoader
-import dev.brahmkshatriya.echo.extensions.plugger.ApkFileManifestParser
-import dev.brahmkshatriya.echo.extensions.plugger.ApkManifestParser
-import dev.brahmkshatriya.echo.extensions.plugger.ApkPluginSource
-import dev.brahmkshatriya.echo.extensions.plugger.FilePluginSource
-import dev.brahmkshatriya.echo.extensions.plugger.LazyPluginRepo
-import dev.brahmkshatriya.echo.extensions.plugger.LazyPluginRepoImpl
-import dev.brahmkshatriya.echo.extensions.plugger.LazyRepoComposer
+import dev.brahmkshatriya.echo.extensions.plugger.FileChangeListener
import dev.brahmkshatriya.echo.extensions.plugger.PackageChangeListener
-import dev.brahmkshatriya.echo.offline.LocalExtensionRepo
+import dev.brahmkshatriya.echo.offline.BuiltInExtensionRepo
import dev.brahmkshatriya.echo.offline.OfflineExtension
import dev.brahmkshatriya.echo.utils.catchWith
import kotlinx.coroutines.CoroutineName
@@ -39,6 +32,7 @@ import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.FlowCollector
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.merge
@@ -46,7 +40,6 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.plus
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeout
-import java.io.File
class ExtensionLoader(
context: Context,
@@ -63,46 +56,25 @@ class ExtensionLoader(
private val extensionFlow: MutableStateFlow,
) {
private val scope = MainScope() + CoroutineName("ExtensionLoader")
-
- private fun Context.getPluginFileDir() = File(filesDir, "extensions").apply { mkdirs() }
private val listener = PackageChangeListener(context)
- private fun getComposed(
- context: Context,
- suffix: String,
- vararg repo: LazyPluginRepo
- ): LazyPluginRepo {
- val loader = AndroidPluginLoader(context)
- val apkFilePluginRepo = LazyPluginRepoImpl(
- FilePluginSource(context.getPluginFileDir(), ".eapk"),
- ApkFileManifestParser(context.packageManager, ApkManifestParser(ImportType.Apk)),
- loader,
- )
- val appPluginRepo = LazyPluginRepoImpl(
- ApkPluginSource(listener, context, "dev.brahmkshatriya.echo.$suffix"),
- ApkManifestParser(ImportType.App),
- loader
- )
- return LazyRepoComposer(appPluginRepo, apkFilePluginRepo, *repo)
- }
-
- private val musicExtensionRepo = MusicExtensionRepo(
- context,
- getComposed(context, "music", LocalExtensionRepo(offlineExtension))
- )
+ val fileListener = FileChangeListener(scope)
+ private val builtIn = BuiltInExtensionRepo(offlineExtension)
- private val trackerExtensionRepo = TrackerExtensionRepo(
- context, getComposed(context, "tracker")
- )
-
- private val lyricsExtensionRepo = LyricsExtensionRepo(
- context, getComposed(context, "lyrics")
- )
+ private val musicExtensionRepo = MusicExtensionRepo(context, listener, fileListener, builtIn)
+ private val trackerExtensionRepo = TrackerExtensionRepo(context, listener, fileListener)
+ private val lyricsExtensionRepo = LyricsExtensionRepo(context, listener, fileListener)
val trackers = trackerListFlow
val extensions = extensionListFlow
val current = extensionFlow
val currentWithUser = MutableStateFlow>(null to null)
+ val priorityMap = ExtensionType.entries.associateWith {
+ val key = it.priorityKey()
+ val list = settings.getString(key, null).orEmpty().split(',')
+ MutableStateFlow(list)
+ }
+
fun initialize() {
scope.launch {
getAllPlugins(scope)
@@ -192,7 +164,7 @@ class ExtensionLoader(
val lyrics = MutableStateFlow(null)
val music = MutableStateFlow(null)
scope.launch {
- trackerExtensionRepo.getPlugins(ExtensionType.TRACKER) { list ->
+ trackerExtensionRepo.getPlugins { list ->
val trackerExtensions = list.map { (metadata, client) ->
TrackerExtension(metadata, client)
}
@@ -202,7 +174,7 @@ class ExtensionLoader(
}
}
scope.launch {
- lyricsExtensionRepo.getPlugins(ExtensionType.LYRICS) { list ->
+ lyricsExtensionRepo.getPlugins { list ->
val lyricsExtensions = list.map { (metadata, client) ->
LyricsExtension(metadata, client)
}
@@ -215,7 +187,7 @@ class ExtensionLoader(
trackers.first { it != null }
scope.launch {
- musicExtensionRepo.getPlugins(ExtensionType.MUSIC) { list ->
+ musicExtensionRepo.getPlugins { list ->
val extensions = list.map { (metadata, client) ->
MusicExtension(metadata, client)
}
@@ -232,19 +204,25 @@ class ExtensionLoader(
music.first { it != null }
}
- private suspend fun LazyPluginRepo.getPlugins(
- type: ExtensionType, collector: FlowCollector>>>>
- ) = getAllPlugins().catchWith(throwableFlow).map { list ->
- list.mapNotNull { result ->
- val (metadata, client) = result.getOrElse {
- val error = it.cause ?: it
- throwableFlow.emit(ExtensionException(type, error))
- null
- } ?: return@mapNotNull null
- val metadataEnabled = isExtensionEnabled(type, metadata)
- Pair(metadataEnabled, client)
+ private suspend fun ExtensionRepo.getPlugins(
+ collector: FlowCollector>>>>
+ ) {
+ val pluginFlow = getAllPlugins().catchWith(throwableFlow).map { list ->
+ list.mapNotNull { result ->
+ val (metadata, client) = result.getOrElse {
+ val error = it.cause ?: it
+ throwableFlow.emit(ExtensionLoadingException(type, error))
+ null
+ } ?: return@mapNotNull null
+ val metadataEnabled = isExtensionEnabled(type, metadata)
+ Pair(metadataEnabled, client)
+ }
}
- }.collect(collector)
+ val priorityFlow = priorityMap[type]!!
+ pluginFlow.combine(priorityFlow) { list, set ->
+ list.sortedBy { set.indexOf(it.first.id) }
+ }.collect(collector)
+ }
private suspend fun List>.setExtensions() = coroutineScope {
map {
@@ -264,6 +242,8 @@ class ExtensionLoader(
const val LAST_EXTENSION_KEY = "last_extension"
private const val TIMEOUT = 5000L
+ fun ExtensionType.priorityKey() = "priority_$this"
+
fun setupMusicExtension(
scope: CoroutineScope,
settings: SharedPreferences,
diff --git a/app/src/main/java/dev/brahmkshatriya/echo/extensions/ExtensionException.kt b/app/src/main/java/dev/brahmkshatriya/echo/extensions/ExtensionLoadingException.kt
similarity index 87%
rename from app/src/main/java/dev/brahmkshatriya/echo/extensions/ExtensionException.kt
rename to app/src/main/java/dev/brahmkshatriya/echo/extensions/ExtensionLoadingException.kt
index 09f6e59d..eec87aec 100644
--- a/app/src/main/java/dev/brahmkshatriya/echo/extensions/ExtensionException.kt
+++ b/app/src/main/java/dev/brahmkshatriya/echo/extensions/ExtensionLoadingException.kt
@@ -2,7 +2,7 @@ package dev.brahmkshatriya.echo.extensions
import dev.brahmkshatriya.echo.common.helpers.ExtensionType
-class ExtensionException(
+class ExtensionLoadingException(
val type: ExtensionType,
override val cause: Throwable
) : Exception("Failed to load extension of type: $type")
\ No newline at end of file
diff --git a/app/src/main/java/dev/brahmkshatriya/echo/extensions/ExtensionRepo.kt b/app/src/main/java/dev/brahmkshatriya/echo/extensions/ExtensionRepo.kt
new file mode 100644
index 00000000..0726bf09
--- /dev/null
+++ b/app/src/main/java/dev/brahmkshatriya/echo/extensions/ExtensionRepo.kt
@@ -0,0 +1,99 @@
+package dev.brahmkshatriya.echo.extensions
+
+import android.content.Context
+import dev.brahmkshatriya.echo.common.clients.ExtensionClient
+import dev.brahmkshatriya.echo.common.clients.LyricsClient
+import dev.brahmkshatriya.echo.common.clients.TrackerClient
+import dev.brahmkshatriya.echo.common.helpers.ExtensionType
+import dev.brahmkshatriya.echo.common.helpers.ImportType
+import dev.brahmkshatriya.echo.common.models.Metadata
+import dev.brahmkshatriya.echo.extensions.plugger.AndroidPluginLoader
+import dev.brahmkshatriya.echo.extensions.plugger.ApkFileManifestParser
+import dev.brahmkshatriya.echo.extensions.plugger.ApkManifestParser
+import dev.brahmkshatriya.echo.extensions.plugger.ApkPluginSource
+import dev.brahmkshatriya.echo.extensions.plugger.FileChangeListener
+import dev.brahmkshatriya.echo.extensions.plugger.FilePluginSource
+import dev.brahmkshatriya.echo.extensions.plugger.LazyPluginRepo
+import dev.brahmkshatriya.echo.extensions.plugger.LazyPluginRepoImpl
+import dev.brahmkshatriya.echo.extensions.plugger.LazyRepoComposer
+import dev.brahmkshatriya.echo.extensions.plugger.PackageChangeListener
+import dev.brahmkshatriya.echo.extensions.plugger.catchLazy
+import dev.brahmkshatriya.echo.utils.getSettings
+import tel.jeelpa.plugger.utils.mapState
+import java.io.File
+
+sealed class ExtensionRepo(
+ private val context: Context,
+ private val listener: PackageChangeListener,
+ private val fileChangeListener: FileChangeListener,
+ private vararg val repo: LazyPluginRepo
+) : LazyPluginRepo {
+ abstract val type: ExtensionType
+
+ private val composed by lazy {
+ val loader = AndroidPluginLoader(context)
+ val dir = context.getPluginFileDir(type)
+ val apkFilePluginRepo = LazyPluginRepoImpl(
+ FilePluginSource(dir, fileChangeListener.scope, fileChangeListener.getFlow(type)),
+ ApkFileManifestParser(context.packageManager, ApkManifestParser(ImportType.File)),
+ loader,
+ )
+ val appPluginRepo = LazyPluginRepoImpl(
+ ApkPluginSource(listener, context, "$FEATURE${type.feature}"),
+ ApkManifestParser(ImportType.App),
+ loader
+ )
+ LazyRepoComposer(appPluginRepo, apkFilePluginRepo, *repo)
+ }
+
+ private fun injected() = composed.getAllPlugins().mapState { list ->
+ list.map {
+ runCatching {
+ val plugin = it.getOrThrow()
+ val (metadata, resultLazy) = plugin
+ metadata to catchLazy {
+ val instance = resultLazy.value.getOrThrow()
+ //Injection
+ instance.setSettings(getSettings(context, type, metadata))
+
+ instance
+ }
+ }
+ }
+ }
+
+ override fun getAllPlugins() = injected()
+
+ companion object {
+ const val FEATURE = "dev.brahmkshatriya.echo."
+ fun Context.getPluginFileDir(type: ExtensionType) =
+ File(filesDir, type.feature).apply { mkdirs() }
+ }
+}
+
+class MusicExtensionRepo(
+ context: Context,
+ listener: PackageChangeListener,
+ fileChangeListener: FileChangeListener,
+ vararg repo: LazyPluginRepo
+) : ExtensionRepo(context, listener, fileChangeListener, *repo) {
+ override val type = ExtensionType.MUSIC
+}
+
+class TrackerExtensionRepo(
+ context: Context,
+ listener: PackageChangeListener,
+ fileChangeListener: FileChangeListener,
+ vararg repo: LazyPluginRepo
+) : ExtensionRepo(context, listener, fileChangeListener, *repo) {
+ override val type = ExtensionType.TRACKER
+}
+
+class LyricsExtensionRepo(
+ context: Context,
+ listener: PackageChangeListener,
+ fileChangeListener: FileChangeListener,
+ vararg repo: LazyPluginRepo
+) : ExtensionRepo(context, listener, fileChangeListener, *repo) {
+ override val type = ExtensionType.LYRICS
+}
\ No newline at end of file
diff --git a/app/src/main/java/dev/brahmkshatriya/echo/extensions/ExtensionUtils.kt b/app/src/main/java/dev/brahmkshatriya/echo/extensions/ExtensionUtils.kt
index 53b34ec8..f7fc8bd3 100644
--- a/app/src/main/java/dev/brahmkshatriya/echo/extensions/ExtensionUtils.kt
+++ b/app/src/main/java/dev/brahmkshatriya/echo/extensions/ExtensionUtils.kt
@@ -1,13 +1,7 @@
package dev.brahmkshatriya.echo.extensions
-import android.content.Context
import dev.brahmkshatriya.echo.common.Extension
import dev.brahmkshatriya.echo.common.clients.ExtensionClient
-import dev.brahmkshatriya.echo.common.clients.LyricsClient
-import dev.brahmkshatriya.echo.common.clients.TrackerClient
-import dev.brahmkshatriya.echo.common.helpers.ExtensionType
-import dev.brahmkshatriya.echo.common.models.Metadata
-import dev.brahmkshatriya.echo.extensions.plugger.LazyPluginRepo
import dev.brahmkshatriya.echo.ui.exception.AppException.Companion.toAppException
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.StateFlow
@@ -38,29 +32,4 @@ suspend inline fun Extension<*>.get(
inline fun Extension<*>.isClient() = instance.value.getOrNull() is T
fun StateFlow>?>.getExtension(id: String?) =
- value?.find { it.metadata.id == id }
-
-class MusicExtensionRepo(
- private val context: Context,
- private val pluginRepo: LazyPluginRepo
-) : LazyPluginRepo {
-
- override fun getAllPlugins() = pluginRepo.getAllPlugins()
- .injectSettings(ExtensionType.MUSIC, context)
-}
-
-class TrackerExtensionRepo(
- private val context: Context,
- private val pluginRepo: LazyPluginRepo
-) : LazyPluginRepo {
- override fun getAllPlugins() = pluginRepo.getAllPlugins()
- .injectSettings(ExtensionType.TRACKER, context)
-}
-
-class LyricsExtensionRepo(
- private val context: Context,
- private val pluginRepo: LazyPluginRepo
-) : LazyPluginRepo {
- override fun getAllPlugins() = pluginRepo.getAllPlugins()
- .injectSettings(ExtensionType.LYRICS, context)
-}
\ No newline at end of file
+ value?.find { it.metadata.id == id }
\ No newline at end of file
diff --git a/app/src/main/java/dev/brahmkshatriya/echo/extensions/InstallationUtils.kt b/app/src/main/java/dev/brahmkshatriya/echo/extensions/InstallationUtils.kt
new file mode 100644
index 00000000..34f4085f
--- /dev/null
+++ b/app/src/main/java/dev/brahmkshatriya/echo/extensions/InstallationUtils.kt
@@ -0,0 +1,104 @@
+package dev.brahmkshatriya.echo.extensions
+
+import android.app.Activity.RESULT_OK
+import android.content.Intent
+import android.content.pm.PackageInfo
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.activity.viewModels
+import androidx.core.content.FileProvider
+import androidx.core.net.toUri
+import androidx.fragment.app.FragmentActivity
+import dev.brahmkshatriya.echo.common.Extension
+import dev.brahmkshatriya.echo.common.helpers.ExtensionType
+import dev.brahmkshatriya.echo.common.helpers.ImportType
+import dev.brahmkshatriya.echo.extensions.ExtensionRepo.Companion.FEATURE
+import dev.brahmkshatriya.echo.extensions.ExtensionRepo.Companion.getPluginFileDir
+import dev.brahmkshatriya.echo.extensions.plugger.ApkManifestParser
+import dev.brahmkshatriya.echo.extensions.plugger.ApkPluginSource
+import dev.brahmkshatriya.echo.utils.registerActivityResultLauncher
+import dev.brahmkshatriya.echo.viewmodels.ExtensionViewModel
+import java.io.File
+import kotlin.coroutines.resume
+import kotlin.coroutines.suspendCoroutine
+
+suspend fun installExtension(context: FragmentActivity, file: File, apk: Boolean) = runCatching {
+ if (apk) {
+ val contentUri = FileProvider.getUriForFile(
+ context, context.packageName + ".provider", file
+ )
+ val installIntent = Intent(Intent.ACTION_VIEW).apply {
+ addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
+ addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
+ putExtra(Intent.EXTRA_NOT_UNKNOWN_SOURCE, true)
+ putExtra(Intent.EXTRA_RETURN_RESULT, true)
+ data = contentUri
+ }
+ context.waitForResult(installIntent)
+ } else {
+ val viewModel by context.viewModels()
+ val extensionLoader = viewModel.extensionLoader
+ val fileChangeListener = extensionLoader.fileListener
+ val packageInfo = context.packageManager.getPackageArchiveInfo(
+ file.path, ApkPluginSource.PACKAGE_FLAGS
+ )
+ val type = getType(packageInfo!!)
+ val metadata = ApkManifestParser(ImportType.File)
+ .parseManifest(packageInfo.applicationInfo!!)
+ val flow = fileChangeListener.getFlow(type)
+ val dir = context.getPluginFileDir(type)
+ val newFile = File(dir, "${metadata.id}.apk")
+ flow.emit(newFile)
+ dir.setWritable(true)
+ newFile.setWritable(true)
+ if (newFile.exists()) newFile.delete()
+ file.copyTo(newFile, true)
+ dir.setReadOnly()
+ flow.emit(null)
+ true
+ }
+}
+
+suspend fun uninstallExtension(context: FragmentActivity, extension: Extension<*>) = runCatching {
+ when (extension.metadata.importType) {
+ ImportType.BuiltIn -> throw UnsupportedOperationException()
+ ImportType.File -> {
+ val file = File(extension.metadata.path)
+ val viewModel by context.viewModels()
+ val extensionLoader = viewModel.extensionLoader
+ val flow = extensionLoader.fileListener.getFlow(extension.type)
+ flow.emit(file)
+ file.parentFile!!.setWritable(true)
+ file.setWritable(true)
+ val delete = file.delete()
+ flow.emit(null)
+ delete
+ }
+
+ ImportType.App -> {
+ val packageInfo = context.packageManager.getPackageArchiveInfo(
+ extension.metadata.path, ApkPluginSource.PACKAGE_FLAGS
+ )!!
+ val packageName = packageInfo.packageName
+ val intent = Intent(Intent.ACTION_DELETE).apply {
+ data = "package:$packageName".toUri()
+ putExtra(Intent.EXTRA_RETURN_RESULT, true)
+ }
+ context.waitForResult(intent)
+ }
+ }
+}
+
+private suspend fun FragmentActivity.waitForResult(intent: Intent) = suspendCoroutine { cont ->
+ val contract = ActivityResultContracts.StartActivityForResult()
+ val activityResultLauncher = registerActivityResultLauncher(contract) {
+ cont.resume(it.resultCode == RESULT_OK)
+ }
+ activityResultLauncher.launch(intent)
+}
+
+
+fun getType(appInfo: PackageInfo) = appInfo.reqFeatures?.find { featureInfo ->
+ ExtensionType.entries.any { featureInfo.name == "$FEATURE${it.feature}" }
+}?.let { featureInfo ->
+ ExtensionType.entries.first { it.feature == featureInfo.name.removePrefix(FEATURE) }
+} ?: error("Extension type not found for ${appInfo.packageName}")
\ No newline at end of file
diff --git a/app/src/main/java/dev/brahmkshatriya/echo/extensions/plugger/AndroidPluginLoader.kt b/app/src/main/java/dev/brahmkshatriya/echo/extensions/plugger/AndroidPluginLoader.kt
index 89ccb1a0..27437dbc 100644
--- a/app/src/main/java/dev/brahmkshatriya/echo/extensions/plugger/AndroidPluginLoader.kt
+++ b/app/src/main/java/dev/brahmkshatriya/echo/extensions/plugger/AndroidPluginLoader.kt
@@ -10,7 +10,6 @@ class AndroidPluginLoader(
) : PluginLoader {
constructor(context: Context): this(GetClassLoaderWithPathUseCase(context.classLoader))
- // for loading classes.dex from dex, jar or apk
@Suppress("UNCHECKED_CAST")
override fun loadPlugin(pluginMetadata: Metadata): TPlugin {
return getClassLoader.getWithPath(pluginMetadata.path)
diff --git a/app/src/main/java/dev/brahmkshatriya/echo/extensions/plugger/ApkManifestParser.kt b/app/src/main/java/dev/brahmkshatriya/echo/extensions/plugger/ApkManifestParser.kt
index 125342b5..47a4ebe1 100644
--- a/app/src/main/java/dev/brahmkshatriya/echo/extensions/plugger/ApkManifestParser.kt
+++ b/app/src/main/java/dev/brahmkshatriya/echo/extensions/plugger/ApkManifestParser.kt
@@ -16,7 +16,7 @@ class ApkManifestParser(
path = data.sourceDir,
className = get("class"),
importType = importType,
- id = get("id"),
+ id = get("id") + importType.name,
name = get("name"),
version = get("version"),
description = get("description"),
diff --git a/app/src/main/java/dev/brahmkshatriya/echo/extensions/plugger/FileChangeListener.kt b/app/src/main/java/dev/brahmkshatriya/echo/extensions/plugger/FileChangeListener.kt
new file mode 100644
index 00000000..583ed1a2
--- /dev/null
+++ b/app/src/main/java/dev/brahmkshatriya/echo/extensions/plugger/FileChangeListener.kt
@@ -0,0 +1,13 @@
+package dev.brahmkshatriya.echo.extensions.plugger
+
+import dev.brahmkshatriya.echo.common.helpers.ExtensionType
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.MutableSharedFlow
+import java.io.File
+
+class FileChangeListener(
+ val scope: CoroutineScope,
+) {
+ val map = mutableMapOf>()
+ fun getFlow(type: ExtensionType) = map.getOrPut(type) { MutableSharedFlow() }
+}
\ No newline at end of file
diff --git a/app/src/main/java/dev/brahmkshatriya/echo/extensions/plugger/FilePluginSource.kt b/app/src/main/java/dev/brahmkshatriya/echo/extensions/plugger/FilePluginSource.kt
index 2f2aaab4..b63efeb2 100644
--- a/app/src/main/java/dev/brahmkshatriya/echo/extensions/plugger/FilePluginSource.kt
+++ b/app/src/main/java/dev/brahmkshatriya/echo/extensions/plugger/FilePluginSource.kt
@@ -1,27 +1,32 @@
package dev.brahmkshatriya.echo.extensions.plugger
-import android.os.Build
-import android.os.FileObserver
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.launch
import tel.jeelpa.plugger.PluginSource
import java.io.File
class FilePluginSource(
private val folder: File,
- private val extension: String,
+ scope: CoroutineScope,
+ fileIgnoreFlow: MutableSharedFlow
) : PluginSource {
- private fun loadAllPlugins() = folder.listFiles()!!.filter { it.path.endsWith(extension) }
+
+ private var ignoreFile : File? = null
+ private fun loadAllPlugins() = run {
+ folder.setReadOnly()
+ folder.listFiles()!!.filter { it != ignoreFile }.onEach { it.setWritable(false) }
+ }
private val pluginStateFlow = MutableStateFlow(loadAllPlugins())
init {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
- val fsEventsListener = object : FileObserver(folder) {
- override fun onEvent(event: Int, path: String?) {
- pluginStateFlow.value = loadAllPlugins()
- }
+ scope.launch {
+ fileIgnoreFlow.collect {
+ ignoreFile = it
+ pluginStateFlow.value = loadAllPlugins()
}
- fsEventsListener.startWatching()
}
}
diff --git a/app/src/main/java/dev/brahmkshatriya/echo/extensions/plugger/LazyPluginRepo.kt b/app/src/main/java/dev/brahmkshatriya/echo/extensions/plugger/LazyPluginRepo.kt
index 5298df19..87a016a7 100644
--- a/app/src/main/java/dev/brahmkshatriya/echo/extensions/plugger/LazyPluginRepo.kt
+++ b/app/src/main/java/dev/brahmkshatriya/echo/extensions/plugger/LazyPluginRepo.kt
@@ -12,4 +12,4 @@ class LazyRepoComposer(
.reduce { a, b -> combineStates(a, b) { x, y -> x + y } }
}
-fun lazily(value: T) = lazy { runCatching { value } }
\ No newline at end of file
+fun catchLazy(function: () -> T) = lazy { runCatching { function() } }
\ No newline at end of file
diff --git a/app/src/main/java/dev/brahmkshatriya/echo/extensions/plugger/LazyPluginRepoImpl.kt b/app/src/main/java/dev/brahmkshatriya/echo/extensions/plugger/LazyPluginRepoImpl.kt
index 9d6c7850..34cbf24f 100644
--- a/app/src/main/java/dev/brahmkshatriya/echo/extensions/plugger/LazyPluginRepoImpl.kt
+++ b/app/src/main/java/dev/brahmkshatriya/echo/extensions/plugger/LazyPluginRepoImpl.kt
@@ -18,7 +18,7 @@ data class LazyPluginRepoImpl(
}.mapState { metadata ->
metadata.map { resultMetadata ->
resultMetadata.mapCatching {
- Pair(it, lazy { runCatching { pluginLoader.loadPlugin(it) } })
+ it to catchLazy { pluginLoader.loadPlugin(it) }
}
}
}
diff --git a/app/src/main/java/dev/brahmkshatriya/echo/offline/LocalExtensionRepo.kt b/app/src/main/java/dev/brahmkshatriya/echo/offline/BuiltInExtensionRepo.kt
similarity index 81%
rename from app/src/main/java/dev/brahmkshatriya/echo/offline/LocalExtensionRepo.kt
rename to app/src/main/java/dev/brahmkshatriya/echo/offline/BuiltInExtensionRepo.kt
index 1a16d4c9..3098880f 100644
--- a/app/src/main/java/dev/brahmkshatriya/echo/offline/LocalExtensionRepo.kt
+++ b/app/src/main/java/dev/brahmkshatriya/echo/offline/BuiltInExtensionRepo.kt
@@ -3,10 +3,10 @@ package dev.brahmkshatriya.echo.offline
import dev.brahmkshatriya.echo.common.clients.ExtensionClient
import dev.brahmkshatriya.echo.common.models.Metadata
import dev.brahmkshatriya.echo.extensions.plugger.LazyPluginRepo
-import dev.brahmkshatriya.echo.extensions.plugger.lazily
+import dev.brahmkshatriya.echo.extensions.plugger.catchLazy
import kotlinx.coroutines.flow.MutableStateFlow
-class LocalExtensionRepo(
+class BuiltInExtensionRepo(
private val extension: OfflineExtension
) : LazyPluginRepo {
@@ -18,5 +18,5 @@ class LocalExtensionRepo(
)
private fun getLazy(metadata: Metadata, extension: ExtensionClient) =
- Result.success(Pair(metadata, lazily(extension)))
+ Result.success(Pair(metadata, catchLazy { extension }))
}
\ No newline at end of file
diff --git a/app/src/main/java/dev/brahmkshatriya/echo/offline/OfflineExtension.kt b/app/src/main/java/dev/brahmkshatriya/echo/offline/OfflineExtension.kt
index 6b0e19cd..5afa2025 100644
--- a/app/src/main/java/dev/brahmkshatriya/echo/offline/OfflineExtension.kt
+++ b/app/src/main/java/dev/brahmkshatriya/echo/offline/OfflineExtension.kt
@@ -1,6 +1,9 @@
package dev.brahmkshatriya.echo.offline
import android.content.Context
+import androidx.annotation.OptIn
+import androidx.media3.common.util.UnstableApi
+import androidx.media3.datasource.cache.SimpleCache
import dev.brahmkshatriya.echo.R
import dev.brahmkshatriya.echo.common.clients.AlbumClient
import dev.brahmkshatriya.echo.common.clients.ArtistClient
@@ -37,7 +40,7 @@ import dev.brahmkshatriya.echo.common.settings.SettingMultipleChoice
import dev.brahmkshatriya.echo.common.settings.SettingSlider
import dev.brahmkshatriya.echo.common.settings.SettingSwitch
import dev.brahmkshatriya.echo.common.settings.Settings
-import dev.brahmkshatriya.echo.extensions.getSettings
+import dev.brahmkshatriya.echo.utils.getSettings
import dev.brahmkshatriya.echo.offline.MediaStoreUtils.addSongToPlaylist
import dev.brahmkshatriya.echo.offline.MediaStoreUtils.createPlaylist
import dev.brahmkshatriya.echo.offline.MediaStoreUtils.deletePlaylist
@@ -45,14 +48,19 @@ import dev.brahmkshatriya.echo.offline.MediaStoreUtils.editPlaylist
import dev.brahmkshatriya.echo.offline.MediaStoreUtils.moveSongInPlaylist
import dev.brahmkshatriya.echo.offline.MediaStoreUtils.removeSongFromPlaylist
import dev.brahmkshatriya.echo.offline.MediaStoreUtils.searchBy
+import dev.brahmkshatriya.echo.playback.MediaItemUtils.toIdAndIsVideo
import dev.brahmkshatriya.echo.utils.getFromCache
import dev.brahmkshatriya.echo.utils.saveToCache
import dev.brahmkshatriya.echo.utils.toData
import dev.brahmkshatriya.echo.utils.toJson
-class OfflineExtension(val context: Context) : ExtensionClient, HomeFeedClient, TrackClient,
- AlbumClient, ArtistClient, PlaylistClient, RadioClient, SearchClient, LibraryClient,
- TrackLikeClient, PlaylistEditorListenerClient, SettingsChangeListenerClient {
+@OptIn(UnstableApi::class)
+class OfflineExtension(
+ val context: Context,
+ val cache: SimpleCache
+) : ExtensionClient, HomeFeedClient, TrackClient, AlbumClient, ArtistClient, PlaylistClient,
+ RadioClient, SearchClient, LibraryClient, TrackLikeClient, PlaylistEditorListenerClient,
+ SettingsChangeListenerClient {
companion object {
val metadata = Metadata(
@@ -107,8 +115,15 @@ class OfflineExtension(val context: Context) : ExtensionClient, HomeFeedClient,
var library = MediaStoreUtils.getAllSongs(context, settings)
private fun refreshLibrary() {
library = MediaStoreUtils.getAllSongs(context, settings)
+ cachedTracks = getCachedTracks()
}
+ @OptIn(UnstableApi::class)
+ private fun getCachedTracks() = cache.keys.mapNotNull { key ->
+ val (id, _) = key.toIdAndIsVideo() ?: return@mapNotNull null
+ context.getFromCache>(id, "track")
+ }.reversed()
+
override fun setSettings(settings: Settings) {}
private fun find(artist: Artist) =
@@ -132,7 +147,7 @@ class OfflineExtension(val context: Context) : ExtensionClient, HomeFeedClient,
override fun getHomeFeed(tab: Tab?): PagedData {
if (refreshLibrary) refreshLibrary()
fun List.sorted() = sortedBy { it.title.lowercase() }
- .map { it.toShelf() }.toPaged()
+ .map { it.toShelf(true) }.toPaged()
return when (tab?.id) {
"Songs" -> library.songList.map { it.toMediaItem() }.sorted()
"Albums" -> library.albumList.map { it.toAlbum().toMediaItem() }.sorted()
@@ -237,10 +252,11 @@ class OfflineExtension(val context: Context) : ExtensionClient, HomeFeedClient,
}
override suspend fun loadPlaylist(playlist: Playlist) =
- find(playlist)!!.toPlaylist()
+ if (playlist.id == "cached") playlist else find(playlist)!!.toPlaylist()
override fun loadTracks(playlist: Playlist): PagedData