Skip to content

Commit

Permalink
Add support to open .eapk files
Browse files Browse the repository at this point in the history
  • Loading branch information
brahmkshatriya committed Oct 15, 2024
1 parent be26dd3 commit a62e8a2
Show file tree
Hide file tree
Showing 23 changed files with 538 additions and 142 deletions.
14 changes: 12 additions & 2 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,8 @@
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="file" />
<data android:scheme="content" />
<data android:mimeType="*/*" />
<data android:mimeType="application/octet-stream" />
</intent-filter>
</activity>

Expand Down Expand Up @@ -112,6 +111,17 @@
<activity
android:name=".ExceptionActivity"
android:exported="true" />

<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.provider"
android:enabled="true"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths" />
</provider>
</application>

</manifest>
2 changes: 2 additions & 0 deletions app/src/main/java/dev/brahmkshatriya/echo/EchoApplication.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -39,6 +40,7 @@ class EchoApplication : Application() {
//UI
applyLocale(settings)
applyUiChanges(this, settings)
cleanupTempApks()

//Crash Handling
Thread.setDefaultUncaughtExceptionHandler { _, exception ->
Expand Down
101 changes: 92 additions & 9 deletions app/src/main/java/dev/brahmkshatriya/echo/ExtensionOpenerActivity.kt
Original file line number Diff line number Diff line change
@@ -1,27 +1,47 @@
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 dev.brahmkshatriya.echo.ui.extension.ApkLinkParser
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 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()
if (file == null)
Toast.makeText(this, "Could not find a file.", Toast.LENGTH_SHORT).show()
val startIntent = Intent(this, MainActivity::class.java)
startIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
startIntent.data = file?.let { Uri.fromFile(it) }
Expand All @@ -31,22 +51,85 @@ class ExtensionOpenerActivity : Activity() {
private fun getTempFile(uri: Uri): File? {
val stream = contentResolver.openInputStream(uri) ?: return null
val bytes = stream.readBytes()
val tempFile = File.createTempFile("temp", ".apk", cacheDir)
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) {
val apk = uri.toFile()
val supportedLinks = ApkLinkParser.getSupportedLinks(apk)

ExtensionInstallerBottomSheet.newInstance(uri.toString())
.show(supportFragmentManager, null)

supportFragmentManager.setFragmentResultListener(EXTENSION_INSTALLER, this) { _, bundle ->
val file = bundle.getString("file")?.toUri()?.toFile()
file?.delete()
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) lifecycleScope.launch {
val installation = if (installAsApk) openApk(context, file)
else {
val viewModel by viewModels<LoginUserViewModel>()
val extensionLoader = viewModel.extensionLoader
installAsFile(context, file, extensionLoader.fileListener)
}
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}")
}
}
3 changes: 1 addition & 2 deletions app/src/main/java/dev/brahmkshatriya/echo/PlayerService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,6 @@ class PlayerService : MediaLibraryService() {
}
}

//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)
Expand All @@ -157,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()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -46,7 +39,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,
Expand All @@ -63,40 +55,13 @@ class ExtensionLoader(
private val extensionFlow: MutableStateFlow<MusicExtension?>,
) {
private val scope = MainScope() + CoroutineName("ExtensionLoader")

private fun Context.getPluginFileDir() = File(filesDir, "extensions").apply { mkdirs() }
private val listener = PackageChangeListener(context)
private fun <T : Any> getComposed(
context: Context,
suffix: String,
vararg repo: LazyPluginRepo<Metadata, T>
): LazyPluginRepo<Metadata, T> {
val loader = AndroidPluginLoader<T>(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))
)

private val trackerExtensionRepo = TrackerExtensionRepo(
context, getComposed(context, "tracker")
)
val fileListener = FileChangeListener(scope)
private val builtIn = BuiltInExtensionRepo(offlineExtension)

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
Expand Down Expand Up @@ -192,7 +157,7 @@ class ExtensionLoader(
val lyrics = MutableStateFlow<Unit?>(null)
val music = MutableStateFlow<Unit?>(null)
scope.launch {
trackerExtensionRepo.getPlugins(ExtensionType.TRACKER) { list ->
trackerExtensionRepo.getPlugins { list ->
val trackerExtensions = list.map { (metadata, client) ->
TrackerExtension(metadata, client)
}
Expand All @@ -202,7 +167,7 @@ class ExtensionLoader(
}
}
scope.launch {
lyricsExtensionRepo.getPlugins(ExtensionType.LYRICS) { list ->
lyricsExtensionRepo.getPlugins { list ->
val lyricsExtensions = list.map { (metadata, client) ->
LyricsExtension(metadata, client)
}
Expand All @@ -215,7 +180,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)
}
Expand All @@ -232,8 +197,8 @@ class ExtensionLoader(
music.first { it != null }
}

private suspend fun <T : Any> LazyPluginRepo<Metadata, T>.getPlugins(
type: ExtensionType, collector: FlowCollector<List<Pair<Metadata, Lazy<Result<T>>>>>
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 {
Expand Down
Loading

0 comments on commit a62e8a2

Please sign in to comment.