Skip to content

Commit

Permalink
Add support to uninstall & sort extensions
Browse files Browse the repository at this point in the history
  • Loading branch information
brahmkshatriya committed Oct 17, 2024
1 parent a62e8a2 commit 2e4b9ea
Show file tree
Hide file tree
Showing 27 changed files with 405 additions and 213 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,30 +3,18 @@ package dev.brahmkshatriya.echo
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.content.pm.PackageInfo
import android.net.Uri
import android.widget.Toast
import androidx.activity.viewModels
import androidx.core.content.FileProvider
import androidx.core.net.toFile
import androidx.core.net.toUri
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.lifecycleScope
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.extensions.plugger.FileChangeListener
import dev.brahmkshatriya.echo.ui.extension.ExtensionInstallerBottomSheet
import dev.brahmkshatriya.echo.viewmodels.LoginUserViewModel
import dev.brahmkshatriya.echo.viewmodels.SnackBar
import dev.brahmkshatriya.echo.viewmodels.SnackBar.Companion.createSnack
import dev.brahmkshatriya.echo.viewmodels.ExtensionViewModel
import kotlinx.coroutines.launch
import java.io.File


class ExtensionOpenerActivity : Activity() {
override fun onStart() {
super.onStart()
Expand Down Expand Up @@ -75,61 +63,13 @@ class ExtensionOpenerActivity : Activity() {
val install = b.getBoolean("install")
val installAsApk = b.getBoolean("installAsApk")
val context = this
if (install && file != null) lifecycleScope.launch {
val installation = if (installAsApk) openApk(context, file)
else {
val viewModel by viewModels<LoginUserViewModel>()
val extensionLoader = viewModel.extensionLoader
installAsFile(context, file, extensionLoader.fileListener)
if (install && file != null) {
val extensionViewModel by viewModels<ExtensionViewModel>()
lifecycleScope.launch {
extensionViewModel.install(context, file, installAsApk)
}
val exception = installation.exceptionOrNull()
if (exception != null) {
val viewModel by viewModels<SnackBar>()
viewModel.throwableFlow.emit(exception)
} else if (!installAsApk)
createSnack(getString(R.string.extension_installed_successfully))
}
}
}

private fun openApk(context: Context, file: File) = runCatching {
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)
data = contentUri
}
context.startActivity(installIntent)
}

private suspend fun installAsFile(
context: Context, file: File, fileChangeListener: FileChangeListener
) = runCatching {
val packageInfo = context.packageManager.getPackageArchiveInfo(
file.path, ApkPluginSource.PACKAGE_FLAGS
)
val type = getType(packageInfo!!)
val metadata = ApkManifestParser(ImportType.File)
.parseManifest(packageInfo.applicationInfo!!)
val dir = context.getPluginFileDir(type)
dir.setWritable(true)
val newFile = File(dir, "${metadata.id}.apk")
val flow = fileChangeListener.getFlow(type)
flow.emit(newFile)
newFile.setWritable(true)
if (newFile.exists()) newFile.delete()
file.copyTo(newFile, true)
dir.setReadOnly()
flow.emit(null)
}

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}")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,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
Expand Down Expand Up @@ -68,6 +69,12 @@ class ExtensionLoader(
val current = extensionFlow
val currentWithUser = MutableStateFlow<Pair<MusicExtension?, UserEntity?>>(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)
Expand Down Expand Up @@ -199,17 +206,23 @@ class ExtensionLoader(

private suspend fun <T : ExtensionClient> ExtensionRepo<T>.getPlugins(
collector: FlowCollector<List<Pair<Metadata, Lazy<Result<T>>>>>
) = 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)
) {
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<Extension<*>>.setExtensions() = coroutineScope {
map {
Expand All @@ -229,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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ 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.lazily
import dev.brahmkshatriya.echo.extensions.plugger.catchLazy
import dev.brahmkshatriya.echo.utils.getSettings
import tel.jeelpa.plugger.utils.mapState
import java.io.File

Expand Down Expand Up @@ -49,9 +50,9 @@ sealed class ExtensionRepo<T : ExtensionClient>(
list.map {
runCatching {
val plugin = it.getOrThrow()
val metadata = plugin.first
metadata to lazily {
val instance = plugin.second.value.getOrThrow()
val (metadata, resultLazy) = plugin
metadata to catchLazy {
val instance = resultLazy.value.getOrThrow()
//Injection
instance.setSettings(getSettings(context, type, metadata))

Expand Down
Original file line number Diff line number Diff line change
@@ -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<ExtensionViewModel>()
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<ExtensionViewModel>()
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}")
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ class AndroidPluginLoader<TPlugin>(
) : PluginLoader<Metadata, TPlugin> {
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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,4 @@ class LazyRepoComposer<TMetadata, TPlugin>(
.reduce { a, b -> combineStates(a, b) { x, y -> x + y } }
}

fun <T> lazily(function: () -> T) = lazy { runCatching { function() } }
fun <T> catchLazy(function: () -> T) = lazy { runCatching { function() } }
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ data class LazyPluginRepoImpl<TSourceInput, TMetadata, TPlugin : Any>(
}.mapState { metadata ->
metadata.map { resultMetadata ->
resultMetadata.mapCatching {
it to lazily { pluginLoader.loadPlugin(it) }
it to catchLazy { pluginLoader.loadPlugin(it) }
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ 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 BuiltInExtensionRepo(
Expand All @@ -18,5 +18,5 @@ class BuiltInExtensionRepo(
)

private fun getLazy(metadata: Metadata, extension: ExtensionClient) =
Result.success(Pair(metadata, lazily { extension }))
Result.success(Pair(metadata, catchLazy { extension }))
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import dev.brahmkshatriya.echo.databinding.FragmentEditPlaylistBinding
import dev.brahmkshatriya.echo.playback.MediaItemUtils
import dev.brahmkshatriya.echo.extensions.getExtension
import dev.brahmkshatriya.echo.extensions.isClient
import dev.brahmkshatriya.echo.ui.adapter.PlaylistAdapter
import dev.brahmkshatriya.echo.ui.player.PlaylistAdapter
import dev.brahmkshatriya.echo.ui.common.openFragment
import dev.brahmkshatriya.echo.ui.editplaylist.EditPlaylistViewModel.ListAction.Add
import dev.brahmkshatriya.echo.ui.editplaylist.EditPlaylistViewModel.ListAction.Move
Expand Down
Loading

0 comments on commit 2e4b9ea

Please sign in to comment.