Auto install app update (#561)

* migrating

* - Fix Broadcaster
- Include Updater for Debug
- json to emulate Github call

* - resetup AppUpdateJob when changing settings
- Fix ACTION_MY_PACKAGE_REPLACED

* improve notification

* update icon

* app update to high important

* - Run auto install on AppUpdateJob
- Add release link to job

* idle state & network aware

* fix duplicated job start

* don't show notification & start job for peek-into-preview

* allow cancel auto update job when set to NEVER

* always notify on install complete

* rearm AppUpdateJob each time MainActivity already checked for update

* MainActivity checks every 2 days, rearm AutoUpdateJob in 3 days

* - fix install error notification ID
- add timeout for manual installation if package manager crash

* settings & spotless

* restriction settings & auto run download-install if not in restrictions

* clean up
This commit is contained in:
Cuong-Tran 2024-12-01 00:46:58 +07:00
parent 5a893927a3
commit a8c587829c
No known key found for this signature in database
GPG key ID: 733AA7624B9315C2
24 changed files with 11120 additions and 32 deletions

View file

@ -71,6 +71,7 @@ android {
versionNameSuffix = "-${getCommitCount()}"
applicationIdSuffix = ".debug"
isPseudoLocalesEnabled = true
buildConfigField("boolean", "INCLUDE_UPDATER", "true")
}
create("releaseTest") {
applicationIdSuffix = ".rt"

View file

@ -0,0 +1,229 @@
{
"url": "https://api.github.com/repos/komikku-app/komikku-preview/releases/184548882",
"assets_url": "https://api.github.com/repos/komikku-app/komikku-preview/releases/184548882/assets",
"upload_url": "https://uploads.github.com/repos/komikku-app/komikku-preview/releases/184548882/assets{?name,label}",
"html_url": "https://github.com/komikku-app/komikku-preview/releases/tag/r9576",
"id": 184548882,
"author": {
"login": "github-actions[bot]",
"id": 41898282,
"node_id": "MDM6Qm90NDE4OTgyODI=",
"avatar_url": "https://avatars.githubusercontent.com/in/15368?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/github-actions%5Bbot%5D",
"html_url": "https://github.com/apps/github-actions",
"followers_url": "https://api.github.com/users/github-actions%5Bbot%5D/followers",
"following_url": "https://api.github.com/users/github-actions%5Bbot%5D/following{/other_user}",
"gists_url": "https://api.github.com/users/github-actions%5Bbot%5D/gists{/gist_id}",
"starred_url": "https://api.github.com/users/github-actions%5Bbot%5D/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/github-actions%5Bbot%5D/subscriptions",
"organizations_url": "https://api.github.com/users/github-actions%5Bbot%5D/orgs",
"repos_url": "https://api.github.com/users/github-actions%5Bbot%5D/repos",
"events_url": "https://api.github.com/users/github-actions%5Bbot%5D/events{/privacy}",
"received_events_url": "https://api.github.com/users/github-actions%5Bbot%5D/received_events",
"type": "Bot",
"user_view_type": "public",
"site_admin": false
},
"node_id": "RE_kwDOL4q0GM4K__4S",
"tag_name": "r9576",
"target_commitish": "main",
"name": "Komikku Preview r9576",
"draft": false,
"prerelease": false,
"created_at": "2024-10-31T21:22:20Z",
"published_at": "2024-11-10T07:50:48Z",
"assets": [
{
"url": "https://api.github.com/repos/komikku-app/komikku-preview/releases/assets/205437757",
"id": 205437757,
"node_id": "RA_kwDOL4q0GM4MPrs9",
"name": "Komikku-arm64-v8a-r9576.apk",
"label": "",
"uploader": {
"login": "github-actions[bot]",
"id": 41898282,
"node_id": "MDM6Qm90NDE4OTgyODI=",
"avatar_url": "https://avatars.githubusercontent.com/in/15368?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/github-actions%5Bbot%5D",
"html_url": "https://github.com/apps/github-actions",
"followers_url": "https://api.github.com/users/github-actions%5Bbot%5D/followers",
"following_url": "https://api.github.com/users/github-actions%5Bbot%5D/following{/other_user}",
"gists_url": "https://api.github.com/users/github-actions%5Bbot%5D/gists{/gist_id}",
"starred_url": "https://api.github.com/users/github-actions%5Bbot%5D/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/github-actions%5Bbot%5D/subscriptions",
"organizations_url": "https://api.github.com/users/github-actions%5Bbot%5D/orgs",
"repos_url": "https://api.github.com/users/github-actions%5Bbot%5D/repos",
"events_url": "https://api.github.com/users/github-actions%5Bbot%5D/events{/privacy}",
"received_events_url": "https://api.github.com/users/github-actions%5Bbot%5D/received_events",
"type": "Bot",
"user_view_type": "public",
"site_admin": false
},
"content_type": "application/vnd.android.package-archive",
"state": "uploaded",
"size": 36655297,
"download_count": 731,
"created_at": "2024-11-10T07:42:42Z",
"updated_at": "2024-11-10T07:42:43Z",
"browser_download_url": "https://github.com/komikku-app/komikku-preview/releases/download/r9576/Komikku-arm64-v8a-r9576.apk"
},
{
"url": "https://api.github.com/repos/komikku-app/komikku-preview/releases/assets/205437760",
"id": 205437760,
"node_id": "RA_kwDOL4q0GM4MPrtA",
"name": "Komikku-armeabi-v7a-r9576.apk",
"label": "",
"uploader": {
"login": "github-actions[bot]",
"id": 41898282,
"node_id": "MDM6Qm90NDE4OTgyODI=",
"avatar_url": "https://avatars.githubusercontent.com/in/15368?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/github-actions%5Bbot%5D",
"html_url": "https://github.com/apps/github-actions",
"followers_url": "https://api.github.com/users/github-actions%5Bbot%5D/followers",
"following_url": "https://api.github.com/users/github-actions%5Bbot%5D/following{/other_user}",
"gists_url": "https://api.github.com/users/github-actions%5Bbot%5D/gists{/gist_id}",
"starred_url": "https://api.github.com/users/github-actions%5Bbot%5D/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/github-actions%5Bbot%5D/subscriptions",
"organizations_url": "https://api.github.com/users/github-actions%5Bbot%5D/orgs",
"repos_url": "https://api.github.com/users/github-actions%5Bbot%5D/repos",
"events_url": "https://api.github.com/users/github-actions%5Bbot%5D/events{/privacy}",
"received_events_url": "https://api.github.com/users/github-actions%5Bbot%5D/received_events",
"type": "Bot",
"user_view_type": "public",
"site_admin": false
},
"content_type": "application/vnd.android.package-archive",
"state": "uploaded",
"size": 31707343,
"download_count": 72,
"created_at": "2024-11-10T07:42:42Z",
"updated_at": "2024-11-10T07:42:43Z",
"browser_download_url": "https://github.com/komikku-app/komikku-preview/releases/download/r9576/Komikku-armeabi-v7a-r9576.apk"
},
{
"url": "https://api.github.com/repos/komikku-app/komikku-preview/releases/assets/205437758",
"id": 205437758,
"node_id": "RA_kwDOL4q0GM4MPrs-",
"name": "Komikku-r9576.apk",
"label": "",
"uploader": {
"login": "github-actions[bot]",
"id": 41898282,
"node_id": "MDM6Qm90NDE4OTgyODI=",
"avatar_url": "https://avatars.githubusercontent.com/in/15368?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/github-actions%5Bbot%5D",
"html_url": "https://github.com/apps/github-actions",
"followers_url": "https://api.github.com/users/github-actions%5Bbot%5D/followers",
"following_url": "https://api.github.com/users/github-actions%5Bbot%5D/following{/other_user}",
"gists_url": "https://api.github.com/users/github-actions%5Bbot%5D/gists{/gist_id}",
"starred_url": "https://api.github.com/users/github-actions%5Bbot%5D/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/github-actions%5Bbot%5D/subscriptions",
"organizations_url": "https://api.github.com/users/github-actions%5Bbot%5D/orgs",
"repos_url": "https://api.github.com/users/github-actions%5Bbot%5D/repos",
"events_url": "https://api.github.com/users/github-actions%5Bbot%5D/events{/privacy}",
"received_events_url": "https://api.github.com/users/github-actions%5Bbot%5D/received_events",
"type": "Bot",
"user_view_type": "public",
"site_admin": false
},
"content_type": "application/vnd.android.package-archive",
"state": "uploaded",
"size": 88282864,
"download_count": 67,
"created_at": "2024-11-10T07:42:42Z",
"updated_at": "2024-11-10T07:42:44Z",
"browser_download_url": "https://github.com/komikku-app/komikku-preview/releases/download/r9576/Komikku-r9576.apk"
},
{
"url": "https://api.github.com/repos/komikku-app/komikku-preview/releases/assets/205437759",
"id": 205437759,
"node_id": "RA_kwDOL4q0GM4MPrs_",
"name": "Komikku-x86-r9576.apk",
"label": "",
"uploader": {
"login": "github-actions[bot]",
"id": 41898282,
"node_id": "MDM6Qm90NDE4OTgyODI=",
"avatar_url": "https://avatars.githubusercontent.com/in/15368?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/github-actions%5Bbot%5D",
"html_url": "https://github.com/apps/github-actions",
"followers_url": "https://api.github.com/users/github-actions%5Bbot%5D/followers",
"following_url": "https://api.github.com/users/github-actions%5Bbot%5D/following{/other_user}",
"gists_url": "https://api.github.com/users/github-actions%5Bbot%5D/gists{/gist_id}",
"starred_url": "https://api.github.com/users/github-actions%5Bbot%5D/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/github-actions%5Bbot%5D/subscriptions",
"organizations_url": "https://api.github.com/users/github-actions%5Bbot%5D/orgs",
"repos_url": "https://api.github.com/users/github-actions%5Bbot%5D/repos",
"events_url": "https://api.github.com/users/github-actions%5Bbot%5D/events{/privacy}",
"received_events_url": "https://api.github.com/users/github-actions%5Bbot%5D/received_events",
"type": "Bot",
"user_view_type": "public",
"site_admin": false
},
"content_type": "application/vnd.android.package-archive",
"state": "uploaded",
"size": 37822615,
"download_count": 3,
"created_at": "2024-11-10T07:42:42Z",
"updated_at": "2024-11-10T07:42:43Z",
"browser_download_url": "https://github.com/komikku-app/komikku-preview/releases/download/r9576/Komikku-x86-r9576.apk"
},
{
"url": "https://api.github.com/repos/komikku-app/komikku-preview/releases/assets/205437761",
"id": 205437761,
"node_id": "RA_kwDOL4q0GM4MPrtB",
"name": "Komikku-x86_64-r9576.apk",
"label": "",
"uploader": {
"login": "github-actions[bot]",
"id": 41898282,
"node_id": "MDM6Qm90NDE4OTgyODI=",
"avatar_url": "https://avatars.githubusercontent.com/in/15368?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/github-actions%5Bbot%5D",
"html_url": "https://github.com/apps/github-actions",
"followers_url": "https://api.github.com/users/github-actions%5Bbot%5D/followers",
"following_url": "https://api.github.com/users/github-actions%5Bbot%5D/following{/other_user}",
"gists_url": "https://api.github.com/users/github-actions%5Bbot%5D/gists{/gist_id}",
"starred_url": "https://api.github.com/users/github-actions%5Bbot%5D/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/github-actions%5Bbot%5D/subscriptions",
"organizations_url": "https://api.github.com/users/github-actions%5Bbot%5D/orgs",
"repos_url": "https://api.github.com/users/github-actions%5Bbot%5D/repos",
"events_url": "https://api.github.com/users/github-actions%5Bbot%5D/events{/privacy}",
"received_events_url": "https://api.github.com/users/github-actions%5Bbot%5D/received_events",
"type": "Bot",
"user_view_type": "public",
"site_admin": false
},
"content_type": "application/vnd.android.package-archive",
"state": "uploaded",
"size": 39817388,
"download_count": 14,
"created_at": "2024-11-10T07:42:42Z",
"updated_at": "2024-11-10T07:42:43Z",
"browser_download_url": "https://github.com/komikku-app/komikku-preview/releases/download/r9576/Komikku-x86_64-r9576.apk"
}
],
"tarball_url": "https://api.github.com/repos/komikku-app/komikku-preview/tarball/r9576",
"zipball_url": "https://api.github.com/repos/komikku-app/komikku-preview/zipball/r9576",
"body": "#### What's Changed\r\n##### New\r\n- Filter only Obsolete sources in Migration screen (@cuong-tran)\r\n- Add option to always use SSIV for image decoding (@AntsyLich)\r\n\r\n##### Improve\r\n- Trying to load saved-search as much as possible even though filterList might changed (@cuong-tran)\r\n- Improve saved-search usage (@cuong-tran)\r\n- Allow reset filters on SourceFeedScreen (@cuong-tran)\r\n- Allow using saved-search while migration source search (@cuong-tran)\r\n- Change bulk-favorite icon to Heart (@cuong-tran)\r\n- Bump default user agent (@AntsyLich)\r\n- Support comma (,) delimiter when searching library (@cuong-tran)\r\n- Auto format extension repo URLs (@AntsyLich)\r\n- Some improvements to Bangumi tracker search (@MajorTanya)\r\n- Do not sync automatically when not connected to a network. (@NGB-Was-Taken)\r\n\r\n##### Fix\r\n- Fix a rare crash when invoking \"Mark previous as read\" action (@AntsyLich)\r\n- Fix long strip images not loading in some old devices (@AntsyLich)\r\n- Fix InterceptActivity crash (@jobobby04)\r\n- Fix crashes from Exh Updater (@jobobby04)\r\n- Fix app onStart sync (@jobobby04)\r\n- Fix crash on MigrationListScreen with null accessing a mutableState when it's being put in the background (@cuong-tran)\r\n- Fix Reader's dialog font size (@cuong-tran)\r\n- Fix multiple issues with the E-Hentai updater (@jobobby04)\r\n- Fix a possible crash with auto-zoom (@jobobby04)\r\n- Fix source-search clear causing disappearance of search box (@cuong-tran)\r\n\r\n**Full Changelog**: https://github.com/komikku-app/komikku/compare/r9523...r9576\r\n\r\n---\r\n\r\n### Checksums\r\n\r\n| Variant | SHA-256 |\r\n| ------- | ------- |\r\n| Universal | d4810c40e54cfb0834dc1253b83c502ba0307a89114b9842f1a01f270d854979 |\r\n| arm64-v8a | 329720d8616359ee311c8bb4f1b151d8318e4afbf996c7d9202c01778ae99128 |\r\n| armeabi-v7a | cd500e73fd8397b07994a3d4de2028af35d2cc91c1c054ce55ad0321347498ca |\r\n| x86 | cc45fb1109cc08127d084fd780cba15d1d011aae6fae31a05690bc0a3ae027e4 |\r\n| x86_64 | a2da81a678d626e0a80b2fe393f987f8059e3924d67f6e1a6e145972e7373e04 |\r\n\r\n### If you are unsure which apk to download then go with `Komikku-r9576.apk`\r\n",
"reactions": {
"url": "https://api.github.com/repos/komikku-app/komikku-preview/releases/184548882/reactions",
"total_count": 6,
"+1": 0,
"-1": 0,
"laugh": 0,
"hooray": 0,
"confused": 0,
"heart": 6,
"rocket": 0,
"eyes": 0
},
"mentions_count": 5
}

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -217,6 +217,17 @@
android:name=".data.notification.NotificationReceiver"
android:exported="false" />
<receiver
android:name=".data.updater.AppUpdateBroadcast"
android:exported="false">
<intent-filter>
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.ACTION_MY_PACKAGE_REPLACED" />
</intent-filter>
</receiver>
<service
android:name=".extension.util.ExtensionInstallService"
android:exported="false"

View file

@ -25,6 +25,7 @@ import androidx.compose.runtime.toMutableStateList
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.window.DialogProperties
import androidx.core.content.ContextCompat
import androidx.core.net.toUri
import androidx.core.text.HtmlCompat
import cafe.adriel.voyager.navigator.LocalNavigator
@ -42,6 +43,7 @@ import eu.kanade.tachiyomi.core.security.SecurityPreferences
import eu.kanade.tachiyomi.data.download.DownloadCache
import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
import eu.kanade.tachiyomi.data.updater.AppUpdateJob
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.NetworkPreferences
import eu.kanade.tachiyomi.network.PREF_DOH_360
@ -90,6 +92,7 @@ import tachiyomi.domain.UnsortedPreferences
import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId
import tachiyomi.domain.manga.interactor.GetAllManga
import tachiyomi.domain.manga.interactor.ResetViewerFlags
import tachiyomi.domain.release.service.AppUpdatePolicy
import tachiyomi.domain.source.service.SourceManager
import tachiyomi.i18n.MR
import tachiyomi.i18n.kmk.KMR
@ -116,6 +119,7 @@ object SettingsAdvancedScreen : SearchableSettings {
val basePreferences = remember { Injekt.get<BasePreferences>() }
val networkPreferences = remember { Injekt.get<NetworkPreferences>() }
val unsortedPreferences = remember { Injekt.get<UnsortedPreferences>() }
return listOf(
Preference.PreferenceItem.TextPreference(
@ -153,6 +157,23 @@ object SettingsAdvancedScreen : SearchableSettings {
context.startActivity(intent)
},
),
// KMK -->
Preference.PreferenceItem.MultiSelectListPreference(
pref = unsortedPreferences.appShouldAutoUpdate(),
title = stringResource(KMR.strings.auto_update_app),
subtitle = stringResource(MR.strings.restrictions),
entries = persistentMapOf(
AppUpdatePolicy.DEVICE_ONLY_ON_WIFI to stringResource(MR.strings.connected_to_wifi),
AppUpdatePolicy.DEVICE_NETWORK_NOT_METERED to stringResource(MR.strings.network_not_metered),
AppUpdatePolicy.DEVICE_CHARGING to stringResource(MR.strings.charging),
),
onValueChanged = {
// Post to event looper to allow the preference to be updated.
ContextCompat.getMainExecutor(context).execute { AppUpdateJob.setupTask(context) }
true
},
),
// KMK <--
getBackgroundActivityGroup(),
getDataGroup(),
getNetworkGroup(networkPreferences = networkPreferences),

View file

@ -311,7 +311,11 @@ object AboutScreen : Screen() {
peekIntoPreview: Boolean = false,
// KMK <--
) {
val updateChecker = AppUpdateChecker(peekIntoPreview)
val updateChecker = AppUpdateChecker(
// KMK -->
peekIntoPreview = peekIntoPreview,
// KMK <--
)
withUIContext {
try {
when (val result = withIOContext { updateChecker.checkForUpdate(context, forceCheck = true) }) {

View file

@ -75,6 +75,12 @@ object Notifications {
const val ID_APP_UPDATER = 1
const val ID_APP_UPDATE_PROMPT = 2
const val ID_APP_UPDATE_ERROR = 3
// KMK -->
const val ID_APP_INSTALL = 4
const val ID_APP_INSTALLED = 5
// KMK <--
const val CHANNEL_EXTENSIONS_UPDATE = "ext_apk_update_channel"
const val ID_UPDATES_TO_EXTS = -401
const val ID_EXTENSION_INSTALLER = -402
@ -162,7 +168,7 @@ object Notifications {
buildNotificationChannel(CHANNEL_INCOGNITO_MODE, IMPORTANCE_LOW) {
setName(context.stringResource(MR.strings.pref_incognito_mode))
},
buildNotificationChannel(CHANNEL_APP_UPDATE, IMPORTANCE_DEFAULT) {
buildNotificationChannel(CHANNEL_APP_UPDATE, IMPORTANCE_HIGH) {
setGroup(GROUP_APK_UPDATES)
setName(context.stringResource(MR.strings.channel_app_updates))
},

View file

@ -0,0 +1,63 @@
package eu.kanade.tachiyomi.data.updater
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.pm.PackageInstaller
import androidx.core.net.toUri
import eu.kanade.tachiyomi.util.system.getParcelableExtraCompat
import eu.kanade.tachiyomi.util.system.toast
import tachiyomi.i18n.kmk.KMR
class AppUpdateBroadcast : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val appUpdateNotifier = AppUpdateNotifier(context)
if (intent.action == AppUpdateDownloadJob.PACKAGE_INSTALLED_ACTION) {
/*
* Callback on PackageInstaller status
*/
when (val status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE)) {
PackageInstaller.STATUS_PENDING_USER_ACTION -> {
val confirmIntent = intent.getParcelableExtraCompat<Intent>(Intent.EXTRA_INTENT)
context.startActivity(confirmIntent?.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
appUpdateNotifier.cancelInstallNotification()
val uri = intent.getStringExtra(AppUpdateDownloadJob.EXTRA_FILE_URI) ?: return
val title = intent.getStringExtra(AppUpdateDownloadJob.EXTRA_DOWNLOAD_TITLE)
appUpdateNotifier.promptInstall(uri.toUri(), title)
}
PackageInstaller.STATUS_SUCCESS -> {
try {
appUpdateNotifier.onInstallFinished()
} finally {
AppUpdateDownloadJob.stop(context)
appUpdateNotifier.cancelInstallNotification()
}
}
PackageInstaller.STATUS_FAILURE,
PackageInstaller.STATUS_FAILURE_ABORTED,
PackageInstaller.STATUS_FAILURE_BLOCKED,
PackageInstaller.STATUS_FAILURE_CONFLICT,
PackageInstaller.STATUS_FAILURE_INCOMPATIBLE,
PackageInstaller.STATUS_FAILURE_INVALID,
PackageInstaller.STATUS_FAILURE_STORAGE,
-> {
if (status != PackageInstaller.STATUS_FAILURE_ABORTED) {
context.toast(KMR.strings.could_not_install_update)
val uri = intent.getStringExtra(AppUpdateDownloadJob.EXTRA_FILE_URI) ?: return
val title = intent.getStringExtra(AppUpdateDownloadJob.EXTRA_DOWNLOAD_TITLE)
appUpdateNotifier.cancelInstallNotification()
appUpdateNotifier.onInstallError(uri.toUri(), title)
}
}
}
} else if (intent.action == Intent.ACTION_MY_PACKAGE_REPLACED) {
/*
* System broadcast that is sent when the current application package has been replaced with a new version,
* to perform actions when app is updated or reinstalled.
*/
appUpdateNotifier.onInstallFinished()
}
}
}

View file

@ -1,6 +1,7 @@
package eu.kanade.tachiyomi.data.updater
import android.content.Context
import android.os.Build
import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.util.system.isInstalledFromFDroid
import tachiyomi.core.common.util.lang.withIOContext
@ -15,7 +16,13 @@ class AppUpdateChecker(
private val getApplicationRelease: GetApplicationRelease by injectLazy()
suspend fun checkForUpdate(context: Context, forceCheck: Boolean = false): GetApplicationRelease.Result {
suspend fun checkForUpdate(
context: Context,
forceCheck: Boolean = false,
// KMK -->
pendingAutoUpdate: Boolean = true,
// KMK <--
): GetApplicationRelease.Result {
return withIOContext {
val result = getApplicationRelease.await(
GetApplicationRelease.Arguments(
@ -28,12 +35,36 @@ class AppUpdateChecker(
),
)
when (result) {
is GetApplicationRelease.Result.NewUpdate -> AppUpdateNotifier(context).promptUpdate(result.release)
is GetApplicationRelease.Result.ThirdPartyInstallation -> AppUpdateNotifier(
context,
).promptFdroidUpdate()
else -> {}
// KMK -->
if (!peekIntoPreview) {
// KMK <--
when (result) {
is GetApplicationRelease.Result.NewUpdate -> {
// KMK -->
AppUpdateNotifier.releasePageUrl = result.release.releaseLink
// KMK <--
AppUpdateNotifier(context).promptUpdate(result.release)
}
is GetApplicationRelease.Result.ThirdPartyInstallation -> AppUpdateNotifier(
context,
).promptFdroidUpdate()
else -> {}
}
// KMK -->
if (pendingAutoUpdate && result is GetApplicationRelease.Result.NewUpdate) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
AppUpdateDownloadJob.start(
context = context,
url = result.release.getDownloadLink(),
title = result.release.version,
scheduled = true,
)
}
}
// KMK <--
}
result

View file

@ -1,31 +1,53 @@
package eu.kanade.tachiyomi.data.updater
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.content.pm.PackageInstaller
import android.content.pm.ServiceInfo
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.work.BackoffPolicy
import androidx.work.Constraints
import androidx.work.CoroutineWorker
import androidx.work.Data
import androidx.work.ExistingWorkPolicy
import androidx.work.ForegroundInfo
import androidx.work.NetworkType
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.OutOfQuotaPolicy
import androidx.work.WorkerParameters
import androidx.work.workDataOf
import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.ProgressListener
import eu.kanade.tachiyomi.util.storage.getUriCompat
import eu.kanade.tachiyomi.util.system.connectivityManager
import eu.kanade.tachiyomi.util.system.isConnectedToWifi
import eu.kanade.tachiyomi.util.system.notificationManager
import eu.kanade.tachiyomi.util.system.setForegroundSafely
import eu.kanade.tachiyomi.util.system.toast
import eu.kanade.tachiyomi.util.system.workManager
import logcat.LogPriority
import exh.log.xLogE
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.withContext
import okhttp3.internal.http2.ErrorCode
import okhttp3.internal.http2.StreamResetException
import tachiyomi.core.common.i18n.stringResource
import tachiyomi.core.common.util.lang.launchUI
import tachiyomi.core.common.util.lang.withIOContext
import tachiyomi.core.common.util.system.logcat
import tachiyomi.domain.UnsortedPreferences
import tachiyomi.domain.release.service.AppUpdatePolicy
import tachiyomi.i18n.MR
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.io.File
import java.lang.ref.WeakReference
import java.util.concurrent.TimeUnit
import kotlin.coroutines.cancellation.CancellationException
class AppUpdateDownloadJob(private val context: Context, workerParams: WorkerParameters) :
@ -34,7 +56,28 @@ class AppUpdateDownloadJob(private val context: Context, workerParams: WorkerPar
private val notifier = AppUpdateNotifier(context)
private val network: NetworkHelper by injectLazy()
// KMK -->
private val preferences = Injekt.get<UnsortedPreferences>()
// KMK <--
override suspend fun doWork(): Result {
// KMK -->
val idleRun = inputData.getBoolean(SCHEDULED_RUN, false)
if (idleRun) {
if (!context.packageManager.canRequestPackageInstalls()) {
return Result.failure()
}
val restrictions = preferences.appShouldAutoUpdate().get()
if ((AppUpdatePolicy.DEVICE_ONLY_ON_WIFI in restrictions) &&
!context.isConnectedToWifi() ||
(AppUpdatePolicy.DEVICE_NETWORK_NOT_METERED in restrictions) &&
context.connectivityManager.isActiveNetworkMetered
) {
return Result.retry()
}
}
// KMK <--
val url = inputData.getString(EXTRA_DOWNLOAD_URL)
val title = inputData.getString(EXTRA_DOWNLOAD_TITLE) ?: context.stringResource(MR.strings.app_name)
@ -43,11 +86,18 @@ class AppUpdateDownloadJob(private val context: Context, workerParams: WorkerPar
}
setForegroundSafely()
// KMK -->
instance = WeakReference(this)
// KMK <--
withIOContext {
downloadApk(title, url)
}
// KMK -->
instance = null
// KMK <--
return Result.success()
}
@ -68,7 +118,7 @@ class AppUpdateDownloadJob(private val context: Context, workerParams: WorkerPar
*
* @param url url location of file
*/
private fun downloadApk(title: String, url: String) {
private suspend fun downloadApk(title: String, url: String) = coroutineScope {
// Show notification download starting.
notifier.onDownloadStarted(title)
@ -107,14 +157,27 @@ class AppUpdateDownloadJob(private val context: Context, workerParams: WorkerPar
try {
// File where the apk will be saved.
val apkFile = File(context.externalCacheDir, "update.apk")
// KMK -->
network.downloadFileWithResume(url, apkFile, progressListener)
if (isStopped) {
cancel()
return@coroutineScope
}
// KMK <--
notifier.cancel()
notifier.promptInstall(apkFile.getUriCompat(context))
// KMK -->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
startInstalling(apkFile, title)
} else {
// KMK <--
notifier.promptInstall(apkFile.getUriCompat(context))
}
} catch (e: Exception) {
logcat(LogPriority.ERROR) { e.toString() }
xLogE("App update stopped:", e)
val shouldCancel = e is CancellationException ||
isStopped ||
(e is StreamResetException && e.errorCode == ErrorCode.CANCEL)
if (shouldCancel) {
notifier.cancel()
@ -129,26 +192,121 @@ class AppUpdateDownloadJob(private val context: Context, workerParams: WorkerPar
}
}
// KMK -->
@RequiresApi(31)
private suspend fun startInstalling(file: File, title: String) {
try {
val packageInstaller = context.packageManager.packageInstaller
val data = file.inputStream()
val installParams = PackageInstaller.SessionParams(
PackageInstaller.SessionParams.MODE_FULL_INSTALL,
)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
installParams.setRequireUserAction(PackageInstaller.SessionParams.USER_ACTION_NOT_REQUIRED)
}
val sessionId = packageInstaller.createSession(installParams)
val session = packageInstaller.openSession(sessionId)
session.openWrite("package", 0, -1).use { packageInSession ->
data.copyTo(packageInSession)
}
val newIntent = Intent(context, AppUpdateBroadcast::class.java)
.setAction(PACKAGE_INSTALLED_ACTION)
.putExtra(EXTRA_FILE_URI, file.getUriCompat(context).toString())
.putExtra(EXTRA_DOWNLOAD_TITLE, title)
val pendingIntent = PendingIntent.getBroadcast(
context,
sessionId,
newIntent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE,
)
val statusReceiver = pendingIntent.intentSender
session.commit(statusReceiver)
notifier.onInstalling()
withContext(Dispatchers.IO) {
data.close()
}
launchUI {
delay(1000)
val hasNotification = context.notificationManager
.activeNotifications.any { it.id == Notifications.ID_APP_INSTALL }
// If the package manager crashes for whatever reason (china phone)
// set a timeout and let the user manually install
if (packageInstaller.getSessionInfo(sessionId) == null && !hasNotification) {
notifier.cancelInstallNotification()
notifier.promptInstall(file.getUriCompat(context))
}
}
} catch (error: Exception) {
// Either install package can't be found (probably bots) or there's a security exception
// with the download manager. Nothing we can workaround.
context.toast(error.message)
notifier.cancelInstallNotification()
notifier.promptInstall(file.getUriCompat(context))
}
}
// KMK <--
companion object {
private const val TAG = "AppUpdateDownload"
// KMK -->
const val PACKAGE_INSTALLED_ACTION =
"${BuildConfig.APPLICATION_ID}.SESSION_SELF_API_PACKAGE_INSTALLED"
internal const val EXTRA_FILE_URI = "${BuildConfig.APPLICATION_ID}.AppInstaller.FILE_URI"
private const val SCHEDULED_RUN = "scheduled_run"
// KMK <--
const val EXTRA_DOWNLOAD_URL = "DOWNLOAD_URL"
const val EXTRA_DOWNLOAD_TITLE = "DOWNLOAD_TITLE"
fun start(context: Context, url: String, title: String? = null) {
val constraints = Constraints(
requiredNetworkType = NetworkType.CONNECTED,
)
// KMK -->
private var instance: WeakReference<AppUpdateDownloadJob>? = null
// KMK <--
fun start(
context: Context,
url: String,
title: String? = null,
// KMK -->
scheduled: Boolean = false,
// KMK <--
) {
val data = Data.Builder()
data.putString(EXTRA_DOWNLOAD_URL, url)
data.putString(EXTRA_DOWNLOAD_TITLE, title)
val request = OneTimeWorkRequestBuilder<AppUpdateDownloadJob>()
.setConstraints(constraints)
.addTag(TAG)
.setInputData(
workDataOf(
EXTRA_DOWNLOAD_URL to url,
EXTRA_DOWNLOAD_TITLE to title,
),
)
.apply {
// KMK -->
if (scheduled) {
data.putBoolean(SCHEDULED_RUN, true)
val restrictions = Injekt.get<UnsortedPreferences>().appShouldAutoUpdate().get()
val constraints = Constraints(
requiredNetworkType = if (AppUpdatePolicy.DEVICE_NETWORK_NOT_METERED in restrictions) {
NetworkType.UNMETERED
} else {
NetworkType.CONNECTED
},
requiresCharging = AppUpdatePolicy.DEVICE_CHARGING in restrictions,
requiresBatteryNotLow = true,
)
setConstraints(constraints)
setInitialDelay(10, TimeUnit.MINUTES)
setBackoffCriteria(BackoffPolicy.LINEAR, 10, TimeUnit.MINUTES)
} else {
setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
// KMK <--
setConstraints(
Constraints(
requiredNetworkType = NetworkType.CONNECTED,
),
)
}
setInputData(data.build())
}
.build()
context.workManager.enqueueUniqueWork(TAG, ExistingWorkPolicy.REPLACE, request)

View file

@ -0,0 +1,61 @@
package eu.kanade.tachiyomi.data.updater
import android.content.Context
import androidx.core.app.NotificationCompat
import androidx.work.Constraints
import androidx.work.CoroutineWorker
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.NetworkType
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager
import androidx.work.WorkerParameters
import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.util.system.notificationManager
import exh.log.xLogE
import kotlinx.coroutines.coroutineScope
import java.util.concurrent.TimeUnit
class AppUpdateJob(private val context: Context, workerParams: WorkerParameters) :
CoroutineWorker(context, workerParams) {
override suspend fun doWork(): Result = coroutineScope {
try {
AppUpdateChecker().checkForUpdate(context)
Result.success()
} catch (e: Exception) {
xLogE("Unable to check for update", e)
Result.failure()
}
}
fun NotificationCompat.Builder.update(block: NotificationCompat.Builder.() -> Unit) {
block()
context.notificationManager.notify(Notifications.ID_APP_UPDATER, build())
}
companion object {
private const val TAG = "AppUpdateChecker"
fun setupTask(context: Context) {
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
val request = PeriodicWorkRequestBuilder<AppUpdateJob>(
3,
TimeUnit.DAYS,
3,
TimeUnit.HOURS,
)
.addTag(TAG)
.setConstraints(constraints)
.build()
WorkManager.getInstance(context).enqueueUniquePeriodicWork(TAG, ExistingPeriodicWorkPolicy.UPDATE, request)
}
fun cancelTask(context: Context) {
WorkManager.getInstance(context).cancelAllWorkByTag(TAG)
}
}
}

View file

@ -1,12 +1,12 @@
package eu.kanade.tachiyomi.data.updater
import android.annotation.SuppressLint
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.net.Uri
import androidx.core.app.NotificationCompat
import androidx.core.net.toUri
import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.notification.NotificationHandler
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
@ -35,7 +35,11 @@ internal class AppUpdateNotifier(private val context: Context) {
NotificationReceiver.dismissNotification(context, Notifications.ID_APP_UPDATER)
}
@SuppressLint("LaunchActivityFromNotification")
/**
* Create a notification to prompt user there is new update, with action to:
* - Download update [AppUpdateDownloadJob.start]
* - Show Github's release notes
*/
fun promptUpdate(release: Release) {
val updateIntent = NotificationReceiver.downloadAppUpdatePendingBroadcast(
context,
@ -115,9 +119,12 @@ internal class AppUpdateNotifier(private val context: Context) {
*
* @param uri path location of apk.
*/
fun promptInstall(uri: Uri) {
fun promptInstall(uri: Uri, title: String? = null) {
val installIntent = NotificationHandler.installApkPendingActivity(context, uri)
with(notificationBuilder) {
// KMK -->
title?.let { setContentTitle(title) }
// KMK <--
setContentText(context.stringResource(MR.strings.update_check_notification_download_complete))
setSmallIcon(android.R.drawable.stat_sys_download_done)
setOnlyAlertOnce(false)
@ -207,4 +214,97 @@ internal class AppUpdateNotifier(private val context: Context) {
}
notificationBuilder.show(Notifications.ID_APP_UPDATE_ERROR)
}
// KMK -->
fun onInstalling() {
with(notificationBuilder) {
setContentText(context.stringResource(MR.strings.ext_installing))
setSmallIcon(android.R.drawable.stat_sys_download)
setProgress(0, 0, true)
setOnlyAlertOnce(true)
clearActions()
show(Notifications.ID_APP_INSTALL)
}
}
/** Call when apk download is finished. */
fun onInstallFinished() {
with(notificationBuilder) {
setContentTitle(context.stringResource(KMR.strings.update_completed))
setContentText(context.stringResource(MR.strings.updated_version, BuildConfig.VERSION_NAME))
setSmallIcon(R.drawable.ic_komikku)
setAutoCancel(true)
setOngoing(false)
setProgress(0, 0, false)
val pendingIntent = PendingIntent.getActivity(
context,
0,
context.packageManager.getLaunchIntentForPackage(BuildConfig.APPLICATION_ID),
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
)
setContentIntent(pendingIntent)
clearActions()
addAction(
R.drawable.ic_launch,
context.stringResource(KMR.strings.open),
pendingIntent,
)
addReleasePageAction()
show(Notifications.ID_APP_INSTALLED)
}
}
private fun NotificationCompat.Builder.addReleasePageAction() {
releasePageUrl?.let { releaseUrl ->
val releaseIntent = Intent(Intent.ACTION_VIEW, releaseUrl.toUri()).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
}
addAction(
R.drawable.ic_info_24dp,
context.stringResource(KMR.strings.release_page),
PendingIntent.getActivity(
context,
releaseUrl.hashCode(),
releaseIntent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
),
)
}
}
fun onInstallError(uri: Uri, title: String?) {
with(notificationBuilder) {
title?.let { setContentTitle(title) }
setContentText(context.stringResource(KMR.strings.could_not_install_update))
setSmallIcon(R.drawable.ic_warning_white_24dp)
setOnlyAlertOnce(false)
setAutoCancel(false)
setProgress(0, 0, false)
clearActions()
// Retry action
addAction(
R.drawable.ic_refresh_24dp,
context.stringResource(MR.strings.action_retry),
NotificationHandler.installApkPendingActivity(context, uri),
)
// Cancel action
addAction(
R.drawable.ic_close_24dp,
context.stringResource(MR.strings.action_cancel),
NotificationReceiver.dismissNotificationPendingBroadcast(context, Notifications.ID_APP_UPDATE_ERROR),
)
addReleasePageAction()
}
notificationBuilder.show(Notifications.ID_APP_UPDATE_ERROR)
}
fun cancelInstallNotification() {
NotificationReceiver.dismissNotification(context, Notifications.ID_APP_INSTALL)
NotificationReceiver.dismissNotification(context, Notifications.ID_APP_UPDATE_PROMPT)
}
companion object {
var releasePageUrl: String? = null
}
// KMK <--
}

View file

@ -75,6 +75,7 @@ import eu.kanade.tachiyomi.data.coil.MangaCoverMetadata
import eu.kanade.tachiyomi.data.download.DownloadCache
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
import eu.kanade.tachiyomi.data.updater.AppUpdateChecker
import eu.kanade.tachiyomi.data.updater.AppUpdateJob
import eu.kanade.tachiyomi.extension.api.ExtensionApi
import eu.kanade.tachiyomi.ui.base.activity.BaseActivity
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceScreen
@ -438,6 +439,9 @@ class MainActivity : BaseActivity() {
LaunchedEffect(Unit) {
if (BuildConfig.INCLUDE_UPDATER) {
try {
// KMK -->
AppUpdateJob.setupTask(context)
// KMK <--
val result = AppUpdateChecker().checkForUpdate(context)
if (result is GetApplicationRelease.Result.NewUpdate) {
val updateScreen = NewUpdateScreen(

View file

@ -48,5 +48,6 @@ val migrations: List<Migration>
// KMK -->
OfficialExtensionRepositoryMigration(),
IntegratedHentaiMigration(),
SetupAppUpdateMigration(),
// KMK <--
)

View file

@ -0,0 +1,19 @@
package mihon.core.migration.migrations
import android.app.Application
import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.data.updater.AppUpdateJob
import mihon.core.migration.Migration
import mihon.core.migration.MigrationContext
class SetupAppUpdateMigration : Migration {
override val version: Float = Migration.ALWAYS
override suspend fun invoke(migrationContext: MigrationContext): Boolean {
if (!BuildConfig.INCLUDE_UPDATER) return false
val context = migrationContext.get<Application>() ?: return false
AppUpdateJob.setupTask(context)
return true
}
}

View file

@ -0,0 +1,10 @@
<!-- drawable/launch.xml -->
<!-- https://pictogrammers.com/library/mdi/ -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:width="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path android:fillColor="#000000"
android:pathData="M14,3V5H17.59L7.76,14.83L9.17,16.24L19,6.41V10H21V3M19,19H5V5H12V3H5C3.89,3 3,3.89 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V12H19V19Z" />
</vector>

View file

@ -1,5 +1,6 @@
package tachiyomi.data.release
import dev.icerock.moko.graphics.BuildConfig
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.awaitSuccess
@ -25,9 +26,14 @@ class ReleaseServiceImpl(
// KMK -->
override suspend fun releaseNotes(repository: String): List<Release> {
val releases = if (BuildConfig.DEBUG) {
"https://raw.githubusercontent.com/$repository/refs/heads/master/app/src/debug/res/raw/releases.json"
} else {
"https://api.github.com/repos/$repository/releases"
}
return with(json) {
networkService.client
.newCall(GET("https://api.github.com/repos/$repository/releases"))
.newCall(GET(releases))
.awaitSuccess()
.parseAs<List<GithubRelease>>()
.map(releaseMapper)

View file

@ -2,10 +2,19 @@ package tachiyomi.domain
import tachiyomi.core.common.preference.Preference
import tachiyomi.core.common.preference.PreferenceStore
import tachiyomi.domain.release.service.AppUpdatePolicy
class UnsortedPreferences(
private val preferenceStore: PreferenceStore,
) {
// KMK -->
fun appShouldAutoUpdate() = preferenceStore.getStringSet(
"should_auto_update",
setOf(
AppUpdatePolicy.DEVICE_ONLY_ON_WIFI,
),
)
// KMK <--
// SY -->

View file

@ -20,7 +20,7 @@ class GetApplicationRelease(
val now = Instant.now()
// Limit checks to once every 3 days at most
val nextCheckTime = Instant.ofEpochMilli(lastChecked.get()).plus(3, ChronoUnit.DAYS)
val nextCheckTime = Instant.ofEpochMilli(lastChecked.get()).plus(2, ChronoUnit.DAYS)
if (!arguments.forceCheck && now.isBefore(nextCheckTime)) {
return Result.NoNewUpdate
}

View file

@ -0,0 +1,9 @@
package tachiyomi.domain.release.service
class AppUpdatePolicy {
companion object {
const val DEVICE_ONLY_ON_WIFI = "wifi"
const val DEVICE_NETWORK_NOT_METERED = "network_not_metered"
const val DEVICE_CHARGING = "ac"
}
}

View file

@ -150,7 +150,7 @@ class GetApplicationReleaseTest {
}
@Test
fun `When now is before three days expect no new update`() = runTest {
fun `When now is before two days expect no new update`() = runTest {
every { preference.get() } returns Instant.now().toEpochMilli()
every { preference.set(any()) }.answers { }

View file

@ -102,6 +102,7 @@
<string name="pref_private_installer_warning">Avoid using Private Installer because it\'s not stable yet, will lead to really slow start/resume App.</string>
<string name="saved_searches_feeds">Saved Searches &amp; Feeds</string>
<string name="preload_library_cover_color">Preload library's cover color</string>
<string name="auto_update_app">Auto-update app</string>
<!-- Onboarding section -->
<string name="onboarding_permission_external_storage">Access media files permission</string>
@ -120,6 +121,10 @@
<!--UpdateCheck-->
<string name="update_check_try_now">Try now</string>
<string name="update_completed">Update completed</string>
<string name="open">Open</string>
<string name="release_page">Release page</string>
<string name="could_not_install_update">Could not install update</string>
<!--UpdateCheck Notifications-->
<string name="update_check_notification_preview_available">Coming (soon) updates!</string>