feat: Final touch before merge

* Trust Extension rework
* Deep link
This commit is contained in:
ziro 2024-01-13 19:27:22 +07:00
parent 67f32d400d
commit addb84ee1e
Signed by: null2264
GPG key ID: BA64F8B60AF3EFB6
12 changed files with 100 additions and 48 deletions

View file

@ -60,6 +60,16 @@
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
<!-- Deep link to add repos -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="tachiyomi" />
<data android:host="add-repo" />
</intent-filter>
<meta-data android:name="android.app.searchable" android:resource="@xml/searchable"/> <meta-data android:name="android.app.searchable" android:resource="@xml/searchable"/>
</activity> </activity>
<activity <activity
@ -270,4 +280,4 @@
</application> </application>
</manifest> </manifest>

View file

@ -0,0 +1,29 @@
package dev.yokai.domain.extension
import android.content.pm.PackageInfo
import androidx.core.content.pm.PackageInfoCompat
import dev.yokai.domain.source.SourcePreferences
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class TrustExtension(
private val sourcePreferences: SourcePreferences = Injekt.get(),
) {
fun isTrusted(pkgInfo: PackageInfo, signatureHash: String): Boolean {
val key = "${pkgInfo.packageName}:${PackageInfoCompat.getLongVersionCode(pkgInfo)}:$signatureHash"
return key in sourcePreferences.trustedExtensions().get()
}
fun trust(pkgName: String, versionCode: Long, signatureHash: String) {
sourcePreferences.trustedExtensions().let { exts ->
val removed = exts.get().filterNot { it.startsWith("$pkgName:") }.toMutableSet()
removed += "$pkgName:$versionCode:$signatureHash"
exts.set(removed)
}
}
fun revokeAll() {
sourcePreferences.trustedExtensions().delete()
}
}

View file

@ -1,18 +1,23 @@
package dev.yokai.presentation.extension.repo package dev.yokai.presentation.extension.repo
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
import eu.kanade.tachiyomi.ui.base.controller.BaseComposeController import eu.kanade.tachiyomi.ui.base.controller.BaseComposeController
class ExtensionRepoController : class ExtensionRepoController() :
BaseComposeController() { BaseComposeController() {
@Preview private var repoUrl: String? = null
constructor(repoUrl: String) : this() {
this.repoUrl = repoUrl
}
@Composable @Composable
override fun ScreenContent() { override fun ScreenContent() {
ExtensionRepoScreen( ExtensionRepoScreen(
title = "Extension Repos", title = "Extension Repos",
onBackPress = router::handleBack, onBackPress = router::handleBack,
repoUrl = repoUrl,
) )
} }
} }

View file

@ -4,11 +4,7 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.ExtensionOff import androidx.compose.material.icons.filled.ExtensionOff
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
@ -31,6 +27,7 @@ fun ExtensionRepoScreen(
title: String, title: String,
onBackPress: () -> Unit, onBackPress: () -> Unit,
viewModel: ExtensionRepoViewModel = viewModel(), viewModel: ExtensionRepoViewModel = viewModel(),
repoUrl: String? = null,
) { ) {
val context = LocalContext.current val context = LocalContext.current
val repoState = viewModel.repoState.collectAsState() val repoState = viewModel.repoState.collectAsState()
@ -39,14 +36,6 @@ fun ExtensionRepoScreen(
YokaiScaffold( YokaiScaffold(
onNavigationIconClicked = onBackPress, onNavigationIconClicked = onBackPress,
title = title, title = title,
fab = {
FloatingActionButton(
containerColor = MaterialTheme.colorScheme.secondary,
onClick = { context.toast("Test") },
) {
Icon(Icons.Filled.Add, "Add repo")
}
},
appBarType = AppBarType.SMALL, appBarType = AppBarType.SMALL,
) { innerPadding -> ) { innerPadding ->
if (repoState.value is ExtensionRepoState.Loading) return@YokaiScaffold if (repoState.value is ExtensionRepoState.Loading) return@YokaiScaffold
@ -61,6 +50,7 @@ fun ExtensionRepoScreen(
item { item {
ExtensionRepoItem( ExtensionRepoItem(
inputText = inputText, inputText = inputText,
// TODO: i18n
inputHint = "Add new repo", inputHint = "Add new repo",
onInputChange = { inputText = it }, onInputChange = { inputText = it },
onAddClick = { viewModel.addRepo(it) }, onAddClick = { viewModel.addRepo(it) },
@ -72,6 +62,7 @@ fun ExtensionRepoScreen(
EmptyScreen( EmptyScreen(
modifier = Modifier.fillParentMaxSize(), modifier = Modifier.fillParentMaxSize(),
image = Icons.Filled.ExtensionOff, image = Icons.Filled.ExtensionOff,
// TODO: i18n
message = "No extension repo found", message = "No extension repo found",
) )
} }
@ -82,12 +73,17 @@ fun ExtensionRepoScreen(
item { item {
ExtensionRepoItem( ExtensionRepoItem(
repoUrl = repo, repoUrl = repo,
// TODO: Confirmation dialog
onDeleteClick = { viewModel.deleteRepo(it) }, onDeleteClick = { viewModel.deleteRepo(it) },
) )
} }
} }
} }
} }
LaunchedEffect(repoUrl) {
repoUrl?.let { viewModel.addRepo(repoUrl) }
}
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
viewModel.event.collectLatest { event -> viewModel.event.collectLatest { event ->

View file

@ -255,6 +255,11 @@ object Migrations {
} catch (_: Exception) { } catch (_: Exception) {
} }
} }
if (oldVersion < 112) {
prefs.edit {
remove("trusted_signatures")
}
}
return true return true
} }

View file

@ -343,9 +343,6 @@ class PreferencesHelper(val context: Context, val preferenceStore: PreferenceSto
fun migrateFlags() = preferenceStore.getInt("migrate_flags", Int.MAX_VALUE) fun migrateFlags() = preferenceStore.getInt("migrate_flags", Int.MAX_VALUE)
// TODO: SourcePref
fun trustedSignatures() = preferenceStore.getStringSet("trusted_signatures", emptySet())
// using string instead of set so it is ordered // using string instead of set so it is ordered
// TODO: SourcePref // TODO: SourcePref
fun migrationSources() = preferenceStore.getString("migrate_sources", "") fun migrationSources() = preferenceStore.getString("migrate_sources", "")

View file

@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.di
import android.app.Application import android.app.Application
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import dev.yokai.domain.extension.TrustExtension
import eu.kanade.tachiyomi.core.preference.AndroidPreferenceStore import eu.kanade.tachiyomi.core.preference.AndroidPreferenceStore
import eu.kanade.tachiyomi.core.preference.PreferenceStore import eu.kanade.tachiyomi.core.preference.PreferenceStore
import eu.kanade.tachiyomi.data.cache.ChapterCache import eu.kanade.tachiyomi.data.cache.ChapterCache
@ -61,6 +62,8 @@ class AppModule(val app: Application) : InjektModule {
addSingletonFactory { MangaShortcutManager() } addSingletonFactory { MangaShortcutManager() }
addSingletonFactory { TrustExtension() }
// Asynchronously init expensive components for a faster cold start // Asynchronously init expensive components for a faster cold start
ContextCompat.getMainExecutor(app).execute { ContextCompat.getMainExecutor(app).execute {

View file

@ -4,9 +4,8 @@ import android.content.Context
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.os.Build import android.os.Build
import android.os.Parcelable import android.os.Parcelable
import dev.yokai.domain.source.SourcePreferences import dev.yokai.domain.extension.TrustExtension
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.minusAssign
import eu.kanade.tachiyomi.data.preference.plusAssign import eu.kanade.tachiyomi.data.preference.plusAssign
import eu.kanade.tachiyomi.extension.api.ExtensionApi import eu.kanade.tachiyomi.extension.api.ExtensionApi
import eu.kanade.tachiyomi.extension.model.Extension import eu.kanade.tachiyomi.extension.model.Extension
@ -23,7 +22,6 @@ import kotlinx.coroutines.async
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.map
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import timber.log.Timber import timber.log.Timber
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
@ -43,6 +41,7 @@ import java.util.Locale
class ExtensionManager( class ExtensionManager(
private val context: Context, private val context: Context,
private val preferences: PreferencesHelper = Injekt.get(), private val preferences: PreferencesHelper = Injekt.get(),
private val trustExtension: TrustExtension = Injekt.get(),
) { ) {
/** /**
@ -307,34 +306,30 @@ class ExtensionManager(
} }
/** /**
* Adds the given signature to the list of trusted signatures. It also loads in background the * Adds the given extension to the list of trusted extensions. It also loads in background the
* extensions that match this signature. * now trusted extensions.
* *
* @param signature The signature to whitelist. * @param pkgName the package name of the extension
* @param versionCode the version code of the extension
* @param signatureHash the signature hash of the extension
*/ */
fun trustSignature(signature: String) { fun trust(pkgName: String, versionCode: Long, signatureHash: String) {
val untrustedSignatures = untrustedExtensionsFlow.value.map { it.signatureHash }.toSet() val untrustedPkgName = untrustedExtensionsFlow.value.map { it.pkgName }.toSet()
if (signature !in untrustedSignatures) return if (pkgName !in untrustedPkgName) return
ExtensionLoader.trustedSignatures += signature trustExtension.trust(pkgName, versionCode, signatureHash)
val preference = preferences.trustedSignatures()
preference.set(preference.get() + signature)
val nowTrustedExtensions = untrustedExtensionsFlow.value.filter { it.signatureHash == signature } val nowTrustedExtensions = untrustedExtensionsFlow.value
.filter { it.pkgName == pkgName && it.versionCode == versionCode }
_untrustedExtensionsFlow.value -= nowTrustedExtensions _untrustedExtensionsFlow.value -= nowTrustedExtensions
val ctx = context
launchNow { launchNow {
nowTrustedExtensions nowTrustedExtensions
.map { extension -> .map { extension ->
async { ExtensionLoader.loadExtensionFromPkgName(ctx, extension.pkgName) } async { ExtensionLoader.loadExtensionFromPkgName(context, extension.pkgName) }.await()
}
.map { it.await() }
.forEach { result ->
if (result is LoadResult.Success) {
registerNewExtension(result.extension)
}
} }
.filterIsInstance<LoadResult.Success>()
.forEach { registerNewExtension(it.extension) }
} }
} }

View file

@ -274,7 +274,7 @@ class ExtensionBottomPresenter : BaseMigrationPresenter<ExtensionBottomSheet>()
} }
} }
fun trustSignature(signatureHash: String) { fun trustExtension(pkgName: String, versionCode: Long, signatureHash: String) {
extensionManager.trustSignature(signatureHash) extensionManager.trust(pkgName, versionCode, signatureHash)
} }
} }

View file

@ -323,7 +323,7 @@ class ExtensionBottomSheet @JvmOverloads constructor(context: Context, attrs: At
} }
private fun openTrustDialog(extension: Extension.Untrusted) { private fun openTrustDialog(extension: Extension.Untrusted) {
ExtensionTrustDialog(this, extension.signatureHash, extension.pkgName) ExtensionTrustDialog(this, extension.signatureHash, extension.pkgName, extension.versionCode)
.showDialog(controller.router) .showDialog(controller.router)
} }
@ -407,9 +407,10 @@ class ExtensionBottomSheet @JvmOverloads constructor(context: Context, attrs: At
extAdapter?.updateItem(updateHeader) extAdapter?.updateItem(updateHeader)
} }
override fun trustSignature(signatureHash: String) { override fun trustExtension(pkgName: String, versionCode: Long, signatureHash: String) {
presenter.trustSignature(signatureHash) presenter.trustExtension(pkgName, versionCode, signatureHash)
} }
override fun uninstallExtension(pkgName: String) { override fun uninstallExtension(pkgName: String) {
presenter.uninstallExtension(pkgName) presenter.uninstallExtension(pkgName)
} }

View file

@ -10,10 +10,11 @@ class ExtensionTrustDialog<T>(bundle: Bundle? = null) : DialogController(bundle)
where T : ExtensionTrustDialog.Listener { where T : ExtensionTrustDialog.Listener {
lateinit var listener: Listener lateinit var listener: Listener
constructor(target: T, signatureHash: String, pkgName: String) : this( constructor(target: T, signatureHash: String, pkgName: String, versionCode: Long) : this(
Bundle().apply { Bundle().apply {
putString(SIGNATURE_KEY, signatureHash) putString(SIGNATURE_KEY, signatureHash)
putString(PKGNAME_KEY, pkgName) putString(PKGNAME_KEY, pkgName)
putLong(VERSION_CODE, versionCode)
}, },
) { ) {
listener = target listener = target
@ -24,7 +25,7 @@ class ExtensionTrustDialog<T>(bundle: Bundle? = null) : DialogController(bundle)
.setTitle(R.string.untrusted_extension) .setTitle(R.string.untrusted_extension)
.setMessage(R.string.untrusted_extension_message) .setMessage(R.string.untrusted_extension_message)
.setPositiveButton(R.string.trust) { _, _ -> .setPositiveButton(R.string.trust) { _, _ ->
listener.trustSignature(args.getString(SIGNATURE_KEY)!!) listener.trustExtension(args.getString(PKGNAME_KEY)!!, args.getLong(VERSION_CODE), args.getString(SIGNATURE_KEY)!!)
} }
.setNegativeButton(R.string.uninstall) { _, _ -> .setNegativeButton(R.string.uninstall) { _, _ ->
listener.uninstallExtension(args.getString(PKGNAME_KEY)!!) listener.uninstallExtension(args.getString(PKGNAME_KEY)!!)
@ -34,10 +35,11 @@ class ExtensionTrustDialog<T>(bundle: Bundle? = null) : DialogController(bundle)
private companion object { private companion object {
const val SIGNATURE_KEY = "signature_key" const val SIGNATURE_KEY = "signature_key"
const val PKGNAME_KEY = "pkgname_key" const val PKGNAME_KEY = "pkgname_key"
const val VERSION_CODE = "version_code"
} }
interface Listener { interface Listener {
fun trustSignature(signatureHash: String) fun trustExtension(pkgName: String, versionCode: Long, signatureHash: String)
fun uninstallExtension(pkgName: String) fun uninstallExtension(pkgName: String)
} }
} }

View file

@ -70,6 +70,7 @@ import com.google.android.material.snackbar.Snackbar
import com.google.android.material.transition.platform.MaterialContainerTransformSharedElementCallback import com.google.android.material.transition.platform.MaterialContainerTransformSharedElementCallback
import com.google.common.primitives.Floats.max import com.google.common.primitives.Floats.max
import com.google.common.primitives.Ints.max import com.google.common.primitives.Ints.max
import dev.yokai.presentation.extension.repo.ExtensionRepoController
import eu.kanade.tachiyomi.BuildConfig import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.Migrations import eu.kanade.tachiyomi.Migrations
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
@ -1053,6 +1054,14 @@ open class MainActivity : BaseActivity<MainActivityBinding>() {
controller?.showSheet() controller?.showSheet()
} }
} }
Intent.ACTION_VIEW -> {
if (intent.scheme == "tachiyomi" && intent.data?.host == "add-repo") {
intent.data?.getQueryParameter("url")?.let { repoUrl ->
router.popToRoot()
router.pushController(ExtensionRepoController(repoUrl).withFadeTransaction())
}
}
}
else -> return false else -> return false
} }
return true return true