ExtensionManager.kt 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368
  1. package eu.kanade.tachiyomi.extension
  2. import android.content.Context
  3. import android.graphics.drawable.Drawable
  4. import com.jakewharton.rxrelay.BehaviorRelay
  5. import eu.kanade.domain.source.model.SourceData
  6. import eu.kanade.tachiyomi.R
  7. import eu.kanade.tachiyomi.data.preference.PreferencesHelper
  8. import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi
  9. import eu.kanade.tachiyomi.extension.model.Extension
  10. import eu.kanade.tachiyomi.extension.model.InstallStep
  11. import eu.kanade.tachiyomi.extension.model.LoadResult
  12. import eu.kanade.tachiyomi.extension.util.ExtensionInstallReceiver
  13. import eu.kanade.tachiyomi.extension.util.ExtensionInstaller
  14. import eu.kanade.tachiyomi.extension.util.ExtensionLoader
  15. import eu.kanade.tachiyomi.source.Source
  16. import eu.kanade.tachiyomi.util.lang.launchNow
  17. import eu.kanade.tachiyomi.util.lang.withUIContext
  18. import eu.kanade.tachiyomi.util.preference.plusAssign
  19. import eu.kanade.tachiyomi.util.system.logcat
  20. import eu.kanade.tachiyomi.util.system.toast
  21. import kotlinx.coroutines.async
  22. import kotlinx.coroutines.flow.MutableStateFlow
  23. import kotlinx.coroutines.flow.StateFlow
  24. import kotlinx.coroutines.flow.asStateFlow
  25. import logcat.LogPriority
  26. import rx.Observable
  27. import uy.kohesive.injekt.Injekt
  28. import uy.kohesive.injekt.api.get
  29. /**
  30. * The manager of extensions installed as another apk which extend the available sources. It handles
  31. * the retrieval of remotely available extensions as well as installing, updating and removing them.
  32. * To avoid malicious distribution, every extension must be signed and it will only be loaded if its
  33. * signature is trusted, otherwise the user will be prompted with a warning to trust it before being
  34. * loaded.
  35. *
  36. * @param context The application context.
  37. * @param preferences The application preferences.
  38. */
  39. class ExtensionManager(
  40. private val context: Context,
  41. private val preferences: PreferencesHelper = Injekt.get(),
  42. ) {
  43. /**
  44. * API where all the available extensions can be found.
  45. */
  46. private val api = ExtensionGithubApi()
  47. /**
  48. * The installer which installs, updates and uninstalls the extensions.
  49. */
  50. private val installer by lazy { ExtensionInstaller(context) }
  51. /**
  52. * Relay used to notify the installed extensions.
  53. */
  54. private val installedExtensionsRelay = BehaviorRelay.create<List<Extension.Installed>>()
  55. private val iconMap = mutableMapOf<String, Drawable>()
  56. /**
  57. * List of the currently installed extensions.
  58. */
  59. var installedExtensions = emptyList<Extension.Installed>()
  60. private set(value) {
  61. field = value
  62. installedExtensionsFlow.value = field
  63. installedExtensionsRelay.call(value)
  64. }
  65. private val installedExtensionsFlow = MutableStateFlow(installedExtensions)
  66. fun getInstalledExtensionsFlow(): StateFlow<List<Extension.Installed>> {
  67. return installedExtensionsFlow.asStateFlow()
  68. }
  69. fun getAppIconForSource(source: Source): Drawable? {
  70. return getAppIconForSource(source.id)
  71. }
  72. fun getAppIconForSource(sourceId: Long): Drawable? {
  73. val pkgName = installedExtensions.find { ext -> ext.sources.any { it.id == sourceId } }?.pkgName
  74. if (pkgName != null) {
  75. return iconMap[pkgName] ?: iconMap.getOrPut(pkgName) { context.packageManager.getApplicationIcon(pkgName) }
  76. }
  77. return null
  78. }
  79. /**
  80. * List of the currently available extensions.
  81. */
  82. var availableExtensions = emptyList<Extension.Available>()
  83. private set(value) {
  84. field = value
  85. availableExtensionsFlow.value = field
  86. updatedInstalledExtensionsStatuses(value)
  87. setupAvailableExtensionsSourcesDataMap(value)
  88. }
  89. private val availableExtensionsFlow = MutableStateFlow(availableExtensions)
  90. fun getAvailableExtensionsFlow(): StateFlow<List<Extension.Available>> {
  91. return availableExtensionsFlow.asStateFlow()
  92. }
  93. private var availableExtensionsSourcesData: Map<Long, SourceData> = mapOf()
  94. private fun setupAvailableExtensionsSourcesDataMap(extensions: List<Extension.Available>) {
  95. if (extensions.isEmpty()) return
  96. availableExtensionsSourcesData = extensions
  97. .flatMap { ext -> ext.sources.map { it.toSourceData() } }
  98. .associateBy { it.id }
  99. }
  100. fun getSourceData(id: Long) = availableExtensionsSourcesData[id]
  101. /**
  102. * List of the currently untrusted extensions.
  103. */
  104. var untrustedExtensions = emptyList<Extension.Untrusted>()
  105. private set(value) {
  106. field = value
  107. untrustedExtensionsFlow.value = field
  108. }
  109. private val untrustedExtensionsFlow = MutableStateFlow(untrustedExtensions)
  110. fun getUntrustedExtensionsFlow(): StateFlow<List<Extension.Untrusted>> {
  111. return untrustedExtensionsFlow.asStateFlow()
  112. }
  113. init {
  114. initExtensions()
  115. ExtensionInstallReceiver(InstallationListener()).register(context)
  116. }
  117. /**
  118. * Loads and registers the installed extensions.
  119. */
  120. private fun initExtensions() {
  121. val extensions = ExtensionLoader.loadExtensions(context)
  122. installedExtensions = extensions
  123. .filterIsInstance<LoadResult.Success>()
  124. .map { it.extension }
  125. untrustedExtensions = extensions
  126. .filterIsInstance<LoadResult.Untrusted>()
  127. .map { it.extension }
  128. }
  129. /**
  130. * Finds the available extensions in the [api] and updates [availableExtensions].
  131. */
  132. suspend fun findAvailableExtensions() {
  133. val extensions: List<Extension.Available> = try {
  134. api.findExtensions()
  135. } catch (e: Exception) {
  136. logcat(LogPriority.ERROR, e)
  137. withUIContext { context.toast(R.string.extension_api_error) }
  138. emptyList()
  139. }
  140. availableExtensions = extensions
  141. }
  142. /**
  143. * Sets the update field of the installed extensions with the given [availableExtensions].
  144. *
  145. * @param availableExtensions The list of extensions given by the [api].
  146. */
  147. private fun updatedInstalledExtensionsStatuses(availableExtensions: List<Extension.Available>) {
  148. if (availableExtensions.isEmpty()) {
  149. preferences.extensionUpdatesCount().set(0)
  150. return
  151. }
  152. val mutInstalledExtensions = installedExtensions.toMutableList()
  153. var changed = false
  154. for ((index, installedExt) in mutInstalledExtensions.withIndex()) {
  155. val pkgName = installedExt.pkgName
  156. val availableExt = availableExtensions.find { it.pkgName == pkgName }
  157. if (availableExt == null && !installedExt.isObsolete) {
  158. mutInstalledExtensions[index] = installedExt.copy(isObsolete = true)
  159. changed = true
  160. } else if (availableExt != null) {
  161. val hasUpdate = !installedExt.isUnofficial &&
  162. availableExt.versionCode > installedExt.versionCode
  163. if (installedExt.hasUpdate != hasUpdate) {
  164. mutInstalledExtensions[index] = installedExt.copy(hasUpdate = hasUpdate)
  165. changed = true
  166. }
  167. }
  168. }
  169. if (changed) {
  170. installedExtensions = mutInstalledExtensions
  171. }
  172. updatePendingUpdatesCount()
  173. }
  174. /**
  175. * Returns an observable of the installation process for the given extension. It will complete
  176. * once the extension is installed or throws an error. The process will be canceled if
  177. * unsubscribed before its completion.
  178. *
  179. * @param extension The extension to be installed.
  180. */
  181. fun installExtension(extension: Extension.Available): Observable<InstallStep> {
  182. return installer.downloadAndInstall(api.getApkUrl(extension), extension)
  183. }
  184. /**
  185. * Returns an observable of the installation process for the given extension. It will complete
  186. * once the extension is updated or throws an error. The process will be canceled if
  187. * unsubscribed before its completion.
  188. *
  189. * @param extension The extension to be updated.
  190. */
  191. fun updateExtension(extension: Extension.Installed): Observable<InstallStep> {
  192. val availableExt = availableExtensions.find { it.pkgName == extension.pkgName }
  193. ?: return Observable.empty()
  194. return installExtension(availableExt)
  195. }
  196. fun cancelInstallUpdateExtension(extension: Extension) {
  197. installer.cancelInstall(extension.pkgName)
  198. }
  199. /**
  200. * Sets to "installing" status of an extension installation.
  201. *
  202. * @param downloadId The id of the download.
  203. */
  204. fun setInstalling(downloadId: Long) {
  205. installer.updateInstallStep(downloadId, InstallStep.Installing)
  206. }
  207. fun updateInstallStep(downloadId: Long, step: InstallStep) {
  208. installer.updateInstallStep(downloadId, step)
  209. }
  210. /**
  211. * Uninstalls the extension that matches the given package name.
  212. *
  213. * @param pkgName The package name of the application to uninstall.
  214. */
  215. fun uninstallExtension(pkgName: String) {
  216. installer.uninstallApk(pkgName)
  217. }
  218. /**
  219. * Adds the given signature to the list of trusted signatures. It also loads in background the
  220. * extensions that match this signature.
  221. *
  222. * @param signature The signature to whitelist.
  223. */
  224. fun trustSignature(signature: String) {
  225. val untrustedSignatures = untrustedExtensions.map { it.signatureHash }.toSet()
  226. if (signature !in untrustedSignatures) return
  227. ExtensionLoader.trustedSignatures += signature
  228. preferences.trustedSignatures() += signature
  229. val nowTrustedExtensions = untrustedExtensions.filter { it.signatureHash == signature }
  230. untrustedExtensions -= nowTrustedExtensions
  231. val ctx = context
  232. launchNow {
  233. nowTrustedExtensions
  234. .map { extension ->
  235. async { ExtensionLoader.loadExtensionFromPkgName(ctx, extension.pkgName) }
  236. }
  237. .map { it.await() }
  238. .forEach { result ->
  239. if (result is LoadResult.Success) {
  240. registerNewExtension(result.extension)
  241. }
  242. }
  243. }
  244. }
  245. /**
  246. * Registers the given extension in this and the source managers.
  247. *
  248. * @param extension The extension to be registered.
  249. */
  250. private fun registerNewExtension(extension: Extension.Installed) {
  251. installedExtensions += extension
  252. }
  253. /**
  254. * Registers the given updated extension in this and the source managers previously removing
  255. * the outdated ones.
  256. *
  257. * @param extension The extension to be registered.
  258. */
  259. private fun registerUpdatedExtension(extension: Extension.Installed) {
  260. val mutInstalledExtensions = installedExtensions.toMutableList()
  261. val oldExtension = mutInstalledExtensions.find { it.pkgName == extension.pkgName }
  262. if (oldExtension != null) {
  263. mutInstalledExtensions -= oldExtension
  264. }
  265. mutInstalledExtensions += extension
  266. installedExtensions = mutInstalledExtensions
  267. }
  268. /**
  269. * Unregisters the extension in this and the source managers given its package name. Note this
  270. * method is called for every uninstalled application in the system.
  271. *
  272. * @param pkgName The package name of the uninstalled application.
  273. */
  274. private fun unregisterExtension(pkgName: String) {
  275. val installedExtension = installedExtensions.find { it.pkgName == pkgName }
  276. if (installedExtension != null) {
  277. installedExtensions -= installedExtension
  278. }
  279. val untrustedExtension = untrustedExtensions.find { it.pkgName == pkgName }
  280. if (untrustedExtension != null) {
  281. untrustedExtensions -= untrustedExtension
  282. }
  283. }
  284. /**
  285. * Listener which receives events of the extensions being installed, updated or removed.
  286. */
  287. private inner class InstallationListener : ExtensionInstallReceiver.Listener {
  288. override fun onExtensionInstalled(extension: Extension.Installed) {
  289. registerNewExtension(extension.withUpdateCheck())
  290. updatePendingUpdatesCount()
  291. }
  292. override fun onExtensionUpdated(extension: Extension.Installed) {
  293. registerUpdatedExtension(extension.withUpdateCheck())
  294. updatePendingUpdatesCount()
  295. }
  296. override fun onExtensionUntrusted(extension: Extension.Untrusted) {
  297. untrustedExtensions += extension
  298. }
  299. override fun onPackageUninstalled(pkgName: String) {
  300. unregisterExtension(pkgName)
  301. updatePendingUpdatesCount()
  302. }
  303. }
  304. /**
  305. * Extension method to set the update field of an installed extension.
  306. */
  307. private fun Extension.Installed.withUpdateCheck(): Extension.Installed {
  308. val availableExt = availableExtensions.find { it.pkgName == pkgName }
  309. if (isUnofficial.not() && availableExt != null && availableExt.versionCode > versionCode) {
  310. return copy(hasUpdate = true)
  311. }
  312. return this
  313. }
  314. private fun updatePendingUpdatesCount() {
  315. preferences.extensionUpdatesCount().set(installedExtensions.count { it.hasUpdate })
  316. }
  317. }