diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index d9e377c7f4..58fea94f0b 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -200,10 +200,22 @@
android:name=".data.notification.NotificationReceiver"
android:exported="false" />
+
+
+
+
+
+
+
+
diff --git a/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt b/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt
index ce0e136ac1..3bd969dae2 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt
@@ -10,6 +10,7 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.data.updater.UpdaterJob
+import eu.kanade.tachiyomi.data.updater.UpdaterService
import eu.kanade.tachiyomi.extension.ExtensionUpdateJob
import eu.kanade.tachiyomi.network.PREF_DOH_CLOUDFLARE
import eu.kanade.tachiyomi.ui.library.LibraryPresenter
@@ -29,6 +30,10 @@ object Migrations {
*/
fun upgrade(preferences: PreferencesHelper): Boolean {
val context = preferences.context
+ val prefs = PreferenceManager.getDefaultSharedPreferences(context)
+ prefs.edit {
+ remove(UpdaterService.NOTIFY_ON_INSTALL_KEY)
+ }
val oldVersion = preferences.lastVersionCode().getOrDefault()
if (oldVersion < BuildConfig.VERSION_CODE) {
preferences.lastVersionCode().set(BuildConfig.VERSION_CODE)
@@ -103,7 +108,6 @@ object Migrations {
}
if (oldVersion < 71) {
// Migrate DNS over HTTPS setting
- val prefs = PreferenceManager.getDefaultSharedPreferences(context)
val wasDohEnabled = prefs.getBoolean("enable_doh", false)
if (wasDohEnabled) {
prefs.edit {
@@ -114,7 +118,6 @@ object Migrations {
}
if (oldVersion < 73) {
// Reset rotation to Free after replacing Lock
- val prefs = PreferenceManager.getDefaultSharedPreferences(context)
if (prefs.contains("pref_rotation_type_key")) {
prefs.edit {
putInt("pref_rotation_type_key", 1)
@@ -128,7 +131,6 @@ object Migrations {
}
}
if (oldVersion < 75) {
- val prefs = PreferenceManager.getDefaultSharedPreferences(context)
val wasShortcutsDisabled = !prefs.getBoolean("show_manga_app_shortcuts", true)
if (wasShortcutsDisabled) {
prefs.edit {
@@ -149,7 +151,6 @@ object Migrations {
}
if (oldVersion < 77) {
// Migrate Rotation and Viewer values to default values for viewer_flags
- val prefs = PreferenceManager.getDefaultSharedPreferences(context)
val newOrientation = when (prefs.getInt("pref_rotation_type_key", 1)) {
1 -> OrientationType.FREE.flagValue
2 -> OrientationType.PORTRAIT.flagValue
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt b/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt
index a1cb5ff53c..c9ccbd1c30 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt
@@ -19,6 +19,7 @@ import eu.kanade.tachiyomi.data.download.DownloadService
import eu.kanade.tachiyomi.data.library.LibraryUpdateService
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.updater.UpdaterService
+import eu.kanade.tachiyomi.extension.ExtensionInstallService
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.ui.main.MainActivity
import eu.kanade.tachiyomi.ui.manga.MangaDetailsController
@@ -71,6 +72,7 @@ class NotificationReceiver : BroadcastReceiver() {
)
// Cancel library update and dismiss notification
ACTION_CANCEL_LIBRARY_UPDATE -> cancelLibraryUpdate(context)
+ ACTION_CANCEL_EXTENSION_UPDATE -> cancelExtensionUpdate(context)
ACTION_CANCEL_UPDATE_DOWNLOAD -> cancelDownloadUpdate(context)
ACTION_CANCEL_RESTORE -> cancelRestoreUpdate(context)
// Share backup file
@@ -199,6 +201,17 @@ class NotificationReceiver : BroadcastReceiver() {
Handler().post { dismissNotification(context, Notifications.ID_LIBRARY_PROGRESS) }
}
+ /**
+ * Method called when user wants to stop a library update
+ *
+ * @param context context of application
+ * @param notificationId id of notification
+ */
+ private fun cancelExtensionUpdate(context: Context) {
+ ExtensionInstallService.stop(context)
+ Handler().post { dismissNotification(context, Notifications.ID_EXTENSION_PROGRESS) }
+ }
+
/**
* Method called when user wants to mark as read
*
@@ -251,6 +264,9 @@ class NotificationReceiver : BroadcastReceiver() {
// Called to cancel library update.
private const val ACTION_CANCEL_LIBRARY_UPDATE = "$ID.$NAME.CANCEL_LIBRARY_UPDATE"
+ // Called to cancel extension update.
+ private const val ACTION_CANCEL_EXTENSION_UPDATE = "$ID.$NAME.CANCEL_EXTENSION_UPDATE"
+
private const val ACTION_CANCEL_UPDATE_DOWNLOAD = "$ID.$NAME.CANCEL_UPDATE_DOWNLOAD"
// Called to mark as read
@@ -568,6 +584,19 @@ class NotificationReceiver : BroadcastReceiver() {
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
}
+ /**
+ * Returns [PendingIntent] that starts a service which stops the library update
+ *
+ * @param context context of application
+ * @return [PendingIntent]
+ */
+ internal fun cancelExtensionUpdatePendingBroadcast(context: Context): PendingIntent {
+ val intent = Intent(context, NotificationReceiver::class.java).apply {
+ action = ACTION_CANCEL_EXTENSION_UPDATE
+ }
+ return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
+ }
+
/**
* Returns [PendingIntent] that cancels the download for a Tachiyomi update
*
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/notification/Notifications.kt b/app/src/main/java/eu/kanade/tachiyomi/data/notification/Notifications.kt
index 2b0fe72714..c04e3d47d6 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/data/notification/Notifications.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/notification/Notifications.kt
@@ -20,6 +20,8 @@ object Notifications {
const val ID_UPDATER = 1
const val ID_DOWNLOAD_IMAGE = 2
const val ID_INSTALL = 3
+ const val CHANNEL_UPDATED = "updated_channel"
+ const val ID_INSTALLED = -6
/**
* Notification channel and ids used by the library updater.
@@ -48,6 +50,9 @@ object Notifications {
const val CHANNEL_UPDATES_TO_EXTS = "updates_ext_channel"
const val ID_UPDATES_TO_EXTS = -401
+ const val CHANNEL_EXT_PROGRESS = "ext_update_progress_channel"
+ const val ID_EXTENSION_PROGRESS = -402
+
private const val GROUP_BACKUP_RESTORE = "group_backup_restore"
const val CHANNEL_BACKUP_RESTORE_PROGRESS = "backup_restore_progress_channel"
const val ID_RESTORE_PROGRESS = -501
@@ -135,5 +140,25 @@ object Notifications {
)
)
context.notificationManager.createNotificationChannels(channels)
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ val newChannels = listOf(
+ NotificationChannel(
+ CHANNEL_EXT_PROGRESS,
+ context.getString(R.string.updating_extensions),
+ NotificationManager.IMPORTANCE_LOW
+ ).apply {
+ setShowBadge(false)
+ setSound(null, null)
+ },
+ NotificationChannel(
+ CHANNEL_UPDATED,
+ context.getString(R.string.update_completed),
+ NotificationManager.IMPORTANCE_DEFAULT
+ ).apply {
+ setShowBadge(false)
+ }
+ )
+ context.notificationManager.createNotificationChannels(newChannels)
+ }
}
}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt
index 78dba72b07..a8320b8fd7 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt
@@ -229,6 +229,8 @@ object PreferenceKeys {
const val incognitoMode = "incognito_mode"
+ const val shouldAutoUpdate = "should_auto_update"
+
const val defaultChapterFilterByRead = "default_chapter_filter_by_read"
const val defaultChapterFilterByDownloaded = "default_chapter_filter_by_downloaded"
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt
index 41cf0a9e17..1824968f05 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt
@@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.data.preference
import android.content.Context
import android.content.SharedPreferences
import android.net.Uri
+import android.os.Build
import android.os.Environment
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.content.edit
@@ -13,6 +14,7 @@ import com.tfcporciuncula.flow.FlowSharedPreferences
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.track.TrackService
+import eu.kanade.tachiyomi.data.updater.AutoUpdaterJob
import eu.kanade.tachiyomi.ui.library.filter.FilterBottomSheet
import eu.kanade.tachiyomi.ui.reader.settings.OrientationType
import eu.kanade.tachiyomi.ui.reader.settings.PageLayout
@@ -120,8 +122,9 @@ class PreferencesHelper(val context: Context) {
fun themeDarkAmoled() = flowPrefs.getBoolean(Keys.themeDarkAmoled, false)
- fun lightTheme() = flowPrefs.getEnum(Keys.lightTheme, Themes.DEFAULT)
- fun darkTheme() = flowPrefs.getEnum(Keys.darkTheme, Themes.DEFAULT)
+ val isOnA12 = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
+ fun lightTheme() = flowPrefs.getEnum(Keys.lightTheme, if (isOnA12) Themes.MONET else Themes.DEFAULT)
+ fun darkTheme() = flowPrefs.getEnum(Keys.darkTheme, if (isOnA12) Themes.MONET else Themes.DEFAULT)
fun pageTransitions() = flowPrefs.getBoolean(Keys.enableTransitions, true)
@@ -424,6 +427,10 @@ class PreferencesHelper(val context: Context) {
fun incognitoMode() = flowPrefs.getBoolean(Keys.incognitoMode, false)
+ fun hasPromptedBeforeUpdateAll() = flowPrefs.getBoolean("has_prompted_update_all", false)
+
+ fun shouldAutoUpdate() = prefs.getInt(Keys.shouldAutoUpdate, AutoUpdaterJob.ONLY_ON_UNMETERED)
+
fun filterChapterByRead() = flowPrefs.getInt(Keys.defaultChapterFilterByRead, Manga.SHOW_ALL)
fun filterChapterByDownloaded() = flowPrefs.getInt(Keys.defaultChapterFilterByDownloaded, Manga.SHOW_ALL)
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/updater/AutoUpdaterJob.kt b/app/src/main/java/eu/kanade/tachiyomi/data/updater/AutoUpdaterJob.kt
new file mode 100644
index 0000000000..8accacc6bb
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/updater/AutoUpdaterJob.kt
@@ -0,0 +1,83 @@
+package eu.kanade.tachiyomi.data.updater
+
+import android.app.ActivityManager
+import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND
+import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_VISIBLE
+import android.content.Context
+import androidx.core.app.NotificationCompat
+import androidx.work.Constraints
+import androidx.work.CoroutineWorker
+import androidx.work.ExistingWorkPolicy
+import androidx.work.NetworkType
+import androidx.work.OneTimeWorkRequestBuilder
+import androidx.work.WorkManager
+import androidx.work.WorkerParameters
+import eu.kanade.tachiyomi.data.notification.Notifications
+import eu.kanade.tachiyomi.data.preference.PreferencesHelper
+import eu.kanade.tachiyomi.util.system.notificationManager
+import kotlinx.coroutines.coroutineScope
+import uy.kohesive.injekt.Injekt
+import uy.kohesive.injekt.api.get
+
+class AutoUpdaterJob(private val context: Context, workerParams: WorkerParameters) :
+ CoroutineWorker(context, workerParams) {
+
+ override suspend fun doWork(): Result = coroutineScope {
+ try {
+ val result = UpdateChecker.getUpdateChecker().checkForUpdate()
+ if (result is UpdateResult.NewUpdate<*> && !UpdaterService.isRunning()) {
+ UpdaterNotifier(context).cancel()
+ UpdaterNotifier.releasePageUrl = result.release.releaseLink
+ UpdaterService.start(context, result.release.downloadLink, false)
+ }
+ Result.success()
+ } catch (e: Exception) {
+ Result.failure()
+ }
+ }
+
+ fun foregrounded(): Boolean {
+ val appProcessInfo = ActivityManager.RunningAppProcessInfo()
+ ActivityManager.getMyMemoryState(appProcessInfo)
+ return appProcessInfo.importance == IMPORTANCE_FOREGROUND || appProcessInfo.importance == IMPORTANCE_VISIBLE
+ }
+
+ fun NotificationCompat.Builder.update(block: NotificationCompat.Builder.() -> Unit) {
+ block()
+ context.notificationManager.notify(Notifications.ID_UPDATER, build())
+ }
+
+ companion object {
+ private const val TAG = "AutoUpdateRunner"
+ const val ALWAYS = 0
+ const val ONLY_ON_UNMETERED = 1
+ const val NEVER = 2
+
+ fun setupTask(context: Context) {
+ val preferences = Injekt.get()
+ val restrictions = preferences.shouldAutoUpdate()
+ val wifiRestriction = if (restrictions == ONLY_ON_UNMETERED) {
+ NetworkType.UNMETERED
+ } else {
+ NetworkType.CONNECTED
+ }
+
+ val constraints = Constraints.Builder()
+ .setRequiredNetworkType(wifiRestriction)
+ .setRequiresDeviceIdle(true)
+ .build()
+
+ val request = OneTimeWorkRequestBuilder()
+ .addTag(TAG)
+ .setConstraints(constraints)
+ .build()
+
+ WorkManager.getInstance(context)
+ .enqueueUniqueWork(TAG, ExistingWorkPolicy.REPLACE, request)
+ }
+
+ fun cancelTask(context: Context) {
+ WorkManager.getInstance(context).cancelAllWorkByTag(TAG)
+ }
+ }
+}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdaterBroadcast.kt b/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdaterBroadcast.kt
new file mode 100644
index 0000000000..59644902d8
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdaterBroadcast.kt
@@ -0,0 +1,55 @@
+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.content.edit
+import androidx.core.net.toUri
+import androidx.preference.PreferenceManager
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.util.system.toast
+
+class UpdaterBroadcast : BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent) {
+ if (UpdaterService.PACKAGE_INSTALLED_ACTION == intent.action) {
+ val extras = intent.extras ?: return
+ when (val status = extras.getInt(PackageInstaller.EXTRA_STATUS)) {
+ PackageInstaller.STATUS_PENDING_USER_ACTION -> {
+ val confirmIntent = extras[Intent.EXTRA_INTENT] as? Intent
+ context.startActivity(confirmIntent)
+ }
+ PackageInstaller.STATUS_SUCCESS -> {
+ val prefs = PreferenceManager.getDefaultSharedPreferences(context)
+ prefs.edit {
+ remove(UpdaterService.NOTIFY_ON_INSTALL_KEY)
+ }
+ val notifyOnInstall = extras.getBoolean(UpdaterService.EXTRA_NOTIFY_ON_INSTALL, false)
+ try {
+ if (notifyOnInstall) {
+ UpdaterNotifier(context).onInstallFinished()
+ }
+ } finally {
+ UpdaterService.stop(context)
+ }
+ }
+ 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(R.string.could_not_install_update)
+ val uri = intent.getStringExtra(UpdaterService.EXTRA_FILE_URI) ?: return
+ UpdaterNotifier(context).onInstallError(uri.toUri())
+ }
+ }
+ }
+ } else if (intent.action == Intent.ACTION_MY_PACKAGE_REPLACED) {
+ val prefs = PreferenceManager.getDefaultSharedPreferences(context)
+ val notifyOnInstall = prefs.getBoolean(UpdaterService.NOTIFY_ON_INSTALL_KEY, false)
+ prefs.edit {
+ remove(UpdaterService.NOTIFY_ON_INSTALL_KEY)
+ }
+ if (notifyOnInstall) {
+ UpdaterNotifier(context).onInstallFinished()
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdaterJob.kt b/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdaterJob.kt
index 41f9129081..c23f7bd419 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdaterJob.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdaterJob.kt
@@ -1,6 +1,7 @@
package eu.kanade.tachiyomi.data.updater
import android.content.Context
+import android.os.Build
import androidx.core.app.NotificationCompat
import androidx.work.Constraints
import androidx.work.CoroutineWorker
@@ -10,8 +11,10 @@ import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager
import androidx.work.WorkerParameters
import eu.kanade.tachiyomi.data.notification.Notifications
+import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.util.system.notificationManager
import kotlinx.coroutines.coroutineScope
+import uy.kohesive.injekt.injectLazy
import java.util.concurrent.TimeUnit
class UpdaterJob(private val context: Context, workerParams: WorkerParameters) :
@@ -19,8 +22,14 @@ class UpdaterJob(private val context: Context, workerParams: WorkerParameters) :
override suspend fun doWork(): Result = coroutineScope {
try {
+ val preferences: PreferencesHelper by injectLazy()
val result = UpdateChecker.getUpdateChecker().checkForUpdate()
if (result is UpdateResult.NewUpdate<*>) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S &&
+ preferences.shouldAutoUpdate() != AutoUpdaterJob.NEVER
+ ) {
+ AutoUpdaterJob.setupTask(context)
+ }
UpdaterNotifier(context).promptUpdate(
result.release.info,
result.release.downloadLink,
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdaterNotifier.kt b/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdaterNotifier.kt
index f72237eb0d..aa62a1a3bc 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdaterNotifier.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdaterNotifier.kt
@@ -4,9 +4,11 @@ import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.net.Uri
+import android.os.Build
import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
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
@@ -44,6 +46,7 @@ internal class UpdaterNotifier(private val context: Context) {
fun promptUpdate(body: String, url: String, releaseUrl: String) {
val intent = Intent(context, UpdaterService::class.java).apply {
putExtra(UpdaterService.EXTRA_DOWNLOAD_URL, url)
+ putExtra(UpdaterService.EXTRA_NOTIFY_ON_INSTALL, true)
}
val pendingIntent = NotificationReceiver.openUpdatePendingActivity(context, body, url)
@@ -56,10 +59,11 @@ internal class UpdaterNotifier(private val context: Context) {
setSmallIcon(android.R.drawable.stat_sys_download_done)
color = context.getResourceColor(R.attr.colorAccent)
clearActions()
+ val isOnA12 = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
// Download action
addAction(
android.R.drawable.stat_sys_download_done,
- context.getString(R.string.download),
+ context.getString(if (isOnA12) R.string.update else R.string.download),
PendingIntent.getService(
context,
0,
@@ -155,6 +159,31 @@ internal class UpdaterNotifier(private val context: Context) {
notificationBuilder.show(Notifications.ID_INSTALL)
}
+ /**
+ * Call when apk download is finished.
+ *
+ * @param uri path location of apk.
+ */
+ fun onInstallFinished() {
+ with(NotificationCompat.Builder(context, Notifications.CHANNEL_UPDATED)) {
+ setContentTitle(context.getString(R.string.updated_to_, BuildConfig.VERSION_NAME))
+ setSmallIcon(R.drawable.ic_tachij2k_notification)
+ setAutoCancel(true)
+ setOngoing(false)
+ setProgress(0, 0, false)
+ val pendingIntent = PendingIntent.getActivity(
+ context,
+ 0,
+ context.packageManager.getLaunchIntentForPackage(BuildConfig.APPLICATION_ID),
+ PendingIntent.FLAG_UPDATE_CURRENT
+ )
+ setContentIntent(pendingIntent)
+ clearActions()
+ addReleasePageAction()
+ show(Notifications.ID_INSTALLED)
+ }
+ }
+
/**
* Call when apk download throws a error
*
@@ -186,6 +215,32 @@ internal class UpdaterNotifier(private val context: Context) {
notificationBuilder.show(Notifications.ID_UPDATER)
}
+ fun onInstallError(uri: Uri) {
+ with(notificationBuilder) {
+ setContentText(context.getString(R.string.could_not_install_update))
+ setSmallIcon(android.R.drawable.stat_sys_warning)
+ setOnlyAlertOnce(false)
+ setAutoCancel(false)
+ setProgress(0, 0, false)
+ color = ContextCompat.getColor(context, R.color.colorAccent)
+ clearActions()
+ // Retry action
+ addAction(
+ R.drawable.ic_refresh_24dp,
+ context.getString(R.string.retry),
+ NotificationHandler.installApkPendingActivity(context, uri)
+ )
+ // Cancel action
+ addAction(
+ R.drawable.ic_close_24dp,
+ context.getString(R.string.cancel),
+ NotificationReceiver.dismissNotificationPendingBroadcast(context, Notifications.ID_UPDATER)
+ )
+ addReleasePageAction()
+ }
+ notificationBuilder.show(Notifications.ID_UPDATER)
+ }
+
fun cancel() {
NotificationReceiver.dismissNotification(context, Notifications.ID_UPDATER)
}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdaterService.kt b/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdaterService.kt
index ef574a782e..81e9189aa1 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdaterService.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/data/updater/UpdaterService.kt
@@ -4,9 +4,12 @@ import android.app.PendingIntent
import android.app.Service
import android.content.Context
import android.content.Intent
+import android.content.pm.PackageInstaller
import android.os.Build
import android.os.IBinder
import android.os.PowerManager
+import androidx.core.content.edit
+import androidx.preference.PreferenceManager
import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.notification.Notifications
@@ -18,12 +21,11 @@ import eu.kanade.tachiyomi.network.newCallWithProgress
import eu.kanade.tachiyomi.util.storage.getUriCompat
import eu.kanade.tachiyomi.util.storage.saveTo
import eu.kanade.tachiyomi.util.system.acquireWakeLock
-import eu.kanade.tachiyomi.util.system.isServiceRunning
+import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job
-import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import okhttp3.Call
import okhttp3.internal.http2.ErrorCode
@@ -64,6 +66,8 @@ class UpdaterService : Service() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
if (intent == null) return START_NOT_STICKY
+ instance = this
+
val handler = CoroutineExceptionHandler { _, exception ->
Timber.e(exception)
stopSelf(startId)
@@ -71,9 +75,10 @@ class UpdaterService : Service() {
val url = intent.getStringExtra(EXTRA_DOWNLOAD_URL) ?: return START_NOT_STICKY
val title = intent.getStringExtra(EXTRA_DOWNLOAD_TITLE) ?: getString(R.string.app_name)
+ val notifyOnInstall = intent.getBooleanExtra(EXTRA_NOTIFY_ON_INSTALL, false)
runningJob = GlobalScope.launch(handler) {
- downloadApk(title, url)
+ downloadApk(title, url, notifyOnInstall)
}
runningJob?.invokeOnCompletion { stopSelf(startId) }
@@ -88,6 +93,9 @@ class UpdaterService : Service() {
override fun onDestroy() {
destroyJob()
+ if (instance == this) {
+ instance = null
+ }
super.onDestroy()
}
@@ -104,7 +112,7 @@ class UpdaterService : Service() {
*
* @param url url location of file
*/
- private suspend fun downloadApk(title: String, url: String) {
+ private suspend fun downloadApk(title: String, url: String, notifyOnInstall: Boolean) {
// Show notification download starting.
notifier.onDownloadStarted(title)
@@ -141,7 +149,11 @@ class UpdaterService : Service() {
response.close()
throw Exception("Unsuccessful response")
}
- notifier.onDownloadFinished(apkFile.getUriCompat(this))
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ startInstalling(apkFile, notifyOnInstall)
+ } else {
+ notifier.onDownloadFinished(apkFile.getUriCompat(this))
+ }
} catch (error: Exception) {
Timber.e(error)
if (error is CancellationException ||
@@ -154,30 +166,77 @@ class UpdaterService : Service() {
}
}
+ private fun startInstalling(file: File, notifyOnInstall: Boolean) {
+ try {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) return
+ val packageInstaller = packageManager.packageInstaller
+ val data = file.inputStream()
+
+ val params = PackageInstaller.SessionParams(
+ PackageInstaller.SessionParams.MODE_FULL_INSTALL
+ )
+ params.setRequireUserAction(PackageInstaller.SessionParams.USER_ACTION_NOT_REQUIRED)
+ val sessionId = packageInstaller.createSession(params)
+ val session = packageInstaller.openSession(sessionId)
+ session.openWrite("package", 0, -1).use { packageInSession ->
+ data.copyTo(packageInSession)
+ }
+ if (notifyOnInstall) {
+ val prefs = PreferenceManager.getDefaultSharedPreferences(this)
+ prefs.edit {
+ putBoolean(NOTIFY_ON_INSTALL_KEY, true)
+ }
+ }
+
+ val newIntent = Intent(this, UpdaterBroadcast::class.java)
+ .setAction(PACKAGE_INSTALLED_ACTION)
+ .putExtra(EXTRA_NOTIFY_ON_INSTALL, notifyOnInstall)
+ .putExtra(EXTRA_FILE_URI, file.getUriCompat(this).toString())
+
+ val pendingIntent = PendingIntent.getBroadcast(this, -10053, newIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE)
+ val statusReceiver = pendingIntent.intentSender
+ session.commit(statusReceiver)
+ data.close()
+ } 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.
+ toast(error.message)
+ }
+ }
+
companion object {
+ const val PACKAGE_INSTALLED_ACTION =
+ "${BuildConfig.APPLICATION_ID}.SESSION_SELF_API_PACKAGE_INSTALLED"
+ internal const val EXTRA_NOTIFY_ON_INSTALL = "${BuildConfig.APPLICATION_ID}.UpdaterService.ACTION_ON_INSTALL"
internal const val EXTRA_DOWNLOAD_URL = "${BuildConfig.APPLICATION_ID}.UpdaterService.DOWNLOAD_URL"
+ internal const val EXTRA_FILE_URI = "${BuildConfig.APPLICATION_ID}.UpdaterService.FILE_URI"
internal const val EXTRA_DOWNLOAD_TITLE = "${BuildConfig.APPLICATION_ID}.UpdaterService.DOWNLOAD_TITLE"
+ internal const val NOTIFY_ON_INSTALL_KEY = "notify_on_install_complete"
+
+ private var instance: UpdaterService? = null
+
/**
* Returns the status of the service.
*
* @param context the application context.
* @return true if the service is running, false otherwise.
*/
- private fun isRunning(context: Context): Boolean =
- context.isServiceRunning(UpdaterService::class.java)
+ fun isRunning(): Boolean = instance != null
/**
* Downloads a new update and let the user install the new version from a notification.
* @param context the application context.
* @param url the url to the new update.
*/
- fun start(context: Context, url: String, title: String = context.getString(R.string.app_name)) {
- if (!isRunning(context)) {
+ fun start(context: Context, url: String, notifyOnInstall: Boolean) {
+ if (!isRunning()) {
+ val title = context.getString(R.string.app_name)
val intent = Intent(context, UpdaterService::class.java).apply {
putExtra(EXTRA_DOWNLOAD_TITLE, title)
putExtra(EXTRA_DOWNLOAD_URL, url)
+ putExtra(EXTRA_NOTIFY_ON_INSTALL, notifyOnInstall)
}
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
context.startService(intent)
@@ -202,9 +261,10 @@ class UpdaterService : Service() {
* @param url the url to the new update.
* @return [PendingIntent]
*/
- internal fun downloadApkPendingService(context: Context, url: String): PendingIntent {
+ internal fun downloadApkPendingService(context: Context, url: String, notifyOnInstall: Boolean = false): PendingIntent {
val intent = Intent(context, UpdaterService::class.java).apply {
putExtra(EXTRA_DOWNLOAD_URL, url)
+ putExtra(EXTRA_NOTIFY_ON_INSTALL, notifyOnInstall)
}
return PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionInstallNotifier.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionInstallNotifier.kt
new file mode 100644
index 0000000000..f2109ff42e
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionInstallNotifier.kt
@@ -0,0 +1,62 @@
+package eu.kanade.tachiyomi.extension
+
+import android.content.Context
+import android.graphics.BitmapFactory
+import androidx.core.content.ContextCompat
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.notification.NotificationReceiver
+import eu.kanade.tachiyomi.data.notification.Notifications
+import eu.kanade.tachiyomi.util.system.notificationBuilder
+import eu.kanade.tachiyomi.util.system.notificationManager
+
+class ExtensionInstallNotifier(private val context: Context) {
+
+ /**
+ * Bitmap of the app for notifications.
+ */
+ private val notificationBitmap by lazy {
+ BitmapFactory.decodeResource(context.resources, R.mipmap.ic_launcher)
+ }
+
+ /**
+ * Pending intent of action that cancels the library update
+ */
+ private val cancelIntent by lazy {
+ NotificationReceiver.cancelExtensionUpdatePendingBroadcast(context)
+ }
+
+ /**
+ * Cached progress notification to avoid creating a lot.
+ */
+ val progressNotificationBuilder by lazy {
+ context.notificationBuilder(Notifications.CHANNEL_UPDATES_TO_EXTS) {
+ setContentTitle(context.getString(R.string.app_name))
+ setSmallIcon(android.R.drawable.stat_sys_download)
+ setLargeIcon(notificationBitmap)
+ setContentTitle(context.getString(R.string.updating_extensions))
+ setProgress(0, 0, true)
+ setOngoing(true)
+ setSilent(true)
+ setOnlyAlertOnce(true)
+ color = ContextCompat.getColor(context, R.color.colorAccent)
+ addAction(R.drawable.ic_close_24dp, context.getString(android.R.string.cancel), cancelIntent)
+ }
+ }
+
+ /**
+ * Shows the notification containing the currently updating manga and the progress.
+ *
+ * @param manga the manga that's being updated.
+ * @param current the current progress.
+ * @param total the total progress.
+ */
+ fun showProgressNotification(progress: Int, max: Int) {
+ context.notificationManager.notify(
+ Notifications.ID_EXTENSION_PROGRESS,
+ progressNotificationBuilder
+ .setContentTitle(context.getString(R.string.updating_extensions))
+ .setProgress(max, progress, progress == 0)
+ .build()
+ )
+ }
+}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionInstallService.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionInstallService.kt
new file mode 100644
index 0000000000..26f7ccd80d
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionInstallService.kt
@@ -0,0 +1,160 @@
+package eu.kanade.tachiyomi.extension
+
+import android.app.Service
+import android.content.Context
+import android.content.Intent
+import android.os.IBinder
+import android.os.PowerManager
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.notification.Notifications
+import eu.kanade.tachiyomi.data.preference.PreferencesHelper
+import eu.kanade.tachiyomi.extension.ExtensionManager.ExtensionInfo
+import eu.kanade.tachiyomi.extension.model.Extension
+import eu.kanade.tachiyomi.util.system.notificationManager
+import eu.kanade.tachiyomi.util.system.toast
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.async
+import kotlinx.coroutines.awaitAll
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.sync.Semaphore
+import kotlinx.coroutines.sync.withPermit
+import uy.kohesive.injekt.Injekt
+import uy.kohesive.injekt.api.get
+import java.util.ArrayList
+import java.util.concurrent.TimeUnit
+
+class ExtensionInstallService(
+ val extensionManager: ExtensionManager = Injekt.get(),
+) : Service() {
+
+ /**
+ * Wake lock that will be held until the service is destroyed.
+ */
+ private lateinit var wakeLock: PowerManager.WakeLock
+
+ private lateinit var notifier: ExtensionInstallNotifier
+
+ private var job: Job? = null
+
+ private var serviceScope = CoroutineScope(Job() + Dispatchers.Default)
+
+ private val requestSemaphore = Semaphore(3)
+
+ private val preferences: PreferencesHelper = Injekt.get()
+
+ /**
+ * This method needs to be implemented, but it's not used/needed.
+ */
+ override fun onBind(intent: Intent): IBinder? {
+ return null
+ }
+
+ /**
+ * Method called when the service receives an intent.
+ *
+ * @param intent the start intent from.
+ * @param flags the flags of the command.
+ * @param startId the start id of this command.
+ * @return the start value of the command.
+ */
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+ if (intent == null) return START_NOT_STICKY
+ if (!preferences.hasPromptedBeforeUpdateAll().get()) {
+ toast(R.string.some_extensions_may_prompt)
+ preferences.hasPromptedBeforeUpdateAll().set(true)
+ }
+
+ instance = this
+
+ val list = intent.getParcelableArrayListExtra(KEY_EXTENSION)?.filter {
+ (
+ extensionManager.installedExtensions.find { installed ->
+ installed.pkgName == it.pkgName
+ }?.versionCode ?: 0
+ ) < it.versionCode
+ }
+ ?: return START_NOT_STICKY
+ var installed = 0
+ job = serviceScope.launch {
+ val results = list.map {
+ async {
+ requestSemaphore.withPermit {
+ extensionManager.installExtension(it, serviceScope)
+ .collect {
+ if (it.first.isCompleted()) {
+ installed++
+ }
+ notifier.showProgressNotification(installed, list.size)
+ }
+ }
+ }
+ }
+ results.awaitAll()
+ }
+ job?.invokeOnCompletion { stopSelf(startId) }
+
+ return START_REDELIVER_INTENT
+ }
+
+ /**
+ * Method called when the service is created. It injects dagger dependencies and acquire
+ * the wake lock.
+ */
+ override fun onCreate() {
+ super.onCreate()
+ notificationManager.cancel(Notifications.ID_UPDATES_TO_EXTS)
+ notifier = ExtensionInstallNotifier(this)
+ wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager).newWakeLock(
+ PowerManager.PARTIAL_WAKE_LOCK,
+ "ExtensionInstallService:WakeLock"
+ )
+ wakeLock.acquire(TimeUnit.MINUTES.toMillis(30))
+ startForeground(Notifications.ID_EXTENSION_PROGRESS, notifier.progressNotificationBuilder.build())
+ }
+
+ /**
+ * Method called when the service is destroyed. It cancels jobs and releases the wake lock.
+ */
+ override fun onDestroy() {
+ job?.cancel()
+ serviceScope.cancel()
+ if (instance == this) {
+ instance = null
+ }
+ if (wakeLock.isHeld) {
+ wakeLock.release()
+ }
+ super.onDestroy()
+ }
+
+ companion object {
+
+ private var instance: ExtensionInstallService? = null
+
+ /**
+ * Stops the service.
+ *
+ * @param context the application context.
+ */
+ fun stop(context: Context) {
+ instance?.serviceScope?.cancel()
+ context.stopService(Intent(context, ExtensionUpdateJob::class.java))
+ }
+
+ /**
+ * Key that defines what should be updated.
+ */
+ private const val KEY_EXTENSION = "extension"
+
+ fun jobIntent(context: Context, extensions: List): Intent {
+ return Intent(context, ExtensionInstallService::class.java).apply {
+ val info = extensions.map(::ExtensionInfo)
+ putParcelableArrayListExtra(KEY_EXTENSION, ArrayList(info))
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt
index df0fc1f320..828e40c949 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt
@@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.extension
import android.content.Context
import android.graphics.drawable.Drawable
+import android.os.Parcelable
import com.jakewharton.rxrelay.BehaviorRelay
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault
@@ -15,9 +16,12 @@ import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.ui.extension.ExtensionIntallInfo
import eu.kanade.tachiyomi.util.system.launchNow
+import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
+import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.withContext
+import kotlinx.parcelize.Parcelize
import rx.Observable
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@@ -47,6 +51,15 @@ class ExtensionManager(
*/
private val installer by lazy { ExtensionInstaller(context) }
+ val downloadRelay
+ get() = installer.downloadsStateFlow
+
+ fun getExtension(downloadId: Long): String? {
+ return installer.activeDownloads.entries.find { downloadId == it.value }?.key
+ }
+
+ fun getActiveInstalls(): Int = installer.activeDownloads.size
+
/**
* Relay used to notify the installed extensions.
*/
@@ -232,27 +245,14 @@ class ExtensionManager(
}
/**
- * Returns an observable of the installation process for the given extension. It will complete
- * once the extension is installed or throws an error. The process will be canceled if
- * unsubscribed before its completion.
+ * Returns a flow of the installation process for the given extension. It will complete
+ * once the extension is installed or throws an error. The process will be canceled the scope
+ * is canceled before its completion.
*
* @param extension The extension to be installed.
*/
- fun installExtension(extension: Extension.Available): Observable {
- return installer.downloadAndInstall(api.getApkUrl(extension), extension)
- }
-
- /**
- * Returns an observable of the installation process for the given extension. It will complete
- * once the extension is updated or throws an error. The process will be canceled if
- * unsubscribed before its completion.
- *
- * @param extension The extension to be updated.
- */
- fun updateExtension(extension: Extension.Installed): Observable {
- val availableExt = availableExtensions.find { it.pkgName == extension.pkgName }
- ?: return Observable.empty()
- return installExtension(availableExt)
+ suspend fun installExtension(extension: ExtensionInfo, scope: CoroutineScope): Flow {
+ return installer.downloadAndInstall(api.getApkUrl(extension), extension, scope)
}
/**
@@ -405,6 +405,21 @@ class ExtensionManager(
}
return this
}
+
+ @Parcelize
+ data class ExtensionInfo(
+ val apkName: String,
+ val pkgName: String,
+ val name: String,
+ val versionCode: Int,
+ ) : Parcelable {
+ constructor(extension: Extension.Available) : this(
+ apkName = extension.apkName,
+ pkgName = extension.pkgName,
+ name = extension.name,
+ versionCode = extension.versionCode
+ )
+ }
}
interface ExtensionsChangedListener {
diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionUpdateJob.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionUpdateJob.kt
index ce9dbe322a..cf7284d066 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionUpdateJob.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionUpdateJob.kt
@@ -1,6 +1,8 @@
package eu.kanade.tachiyomi.extension
+import android.app.PendingIntent
import android.content.Context
+import android.os.Build
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat
@@ -17,6 +19,7 @@ import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi
+import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.util.system.notification
import kotlinx.coroutines.coroutineScope
import uy.kohesive.injekt.Injekt
@@ -35,15 +38,21 @@ class ExtensionUpdateJob(private val context: Context, workerParams: WorkerParam
}
if (pendingUpdates.isNotEmpty()) {
- createUpdateNotification(pendingUpdates.map { it.name })
+ createUpdateNotification(pendingUpdates)
}
Result.success()
}
- private fun createUpdateNotification(names: List) {
+ private fun createUpdateNotification(extensions: List) {
val preferences: PreferencesHelper by injectLazy()
- preferences.extensionUpdatesCount().set(names.size)
+ preferences.extensionUpdatesCount().set(extensions.size)
+ // Not doing this yet since users will get prompted while device is idle
+// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && preferences.autoUpdateExtensions()) {
+// val intent = ExtensionInstallService.jobIntent(context, extensions)
+// context.startForegroundService(intent)
+// return
+// }
NotificationManagerCompat.from(context).apply {
notify(
Notifications.ID_UPDATES_TO_EXTS,
@@ -51,11 +60,11 @@ class ExtensionUpdateJob(private val context: Context, workerParams: WorkerParam
setContentTitle(
context.resources.getQuantityString(
R.plurals.extension_updates_available,
- names.size,
- names.size
+ extensions.size,
+ extensions.size
)
)
- val extNames = names.joinToString(", ")
+ val extNames = extensions.joinToString(", ") { it.name }
setContentText(extNames)
setStyle(NotificationCompat.BigTextStyle().bigText(extNames))
setSmallIcon(R.drawable.ic_extension_update_24dp)
@@ -65,6 +74,16 @@ class ExtensionUpdateJob(private val context: Context, workerParams: WorkerParam
context
)
)
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ val intent = ExtensionInstallService.jobIntent(context, extensions)
+ val pendingIntent =
+ PendingIntent.getForegroundService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
+ addAction(
+ R.drawable.ic_file_download_24dp,
+ context.getString(R.string.update_all),
+ pendingIntent
+ )
+ }
setAutoCancel(true)
}
)
diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/api/ExtensionGithubApi.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/api/ExtensionGithubApi.kt
index a2ff814ce2..c5caeac509 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/extension/api/ExtensionGithubApi.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/extension/api/ExtensionGithubApi.kt
@@ -1,6 +1,7 @@
package eu.kanade.tachiyomi.extension.api
import android.content.Context
+import eu.kanade.tachiyomi.extension.ExtensionManager
import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.extension.model.LoadResult
import eu.kanade.tachiyomi.extension.util.ExtensionLoader
@@ -30,7 +31,7 @@ internal class ExtensionGithubApi {
}
}
- suspend fun checkForUpdates(context: Context): List {
+ suspend fun checkForUpdates(context: Context): List {
return withContext(Dispatchers.IO) {
val extensions = findExtensions()
@@ -38,7 +39,7 @@ internal class ExtensionGithubApi {
.filterIsInstance()
.map { it.extension }
- val extensionsWithUpdate = mutableListOf()
+ val extensionsWithUpdate = mutableListOf()
val mutInstalledExtensions = installedExtensions.toMutableList()
for (installedExt in mutInstalledExtensions) {
val pkgName = installedExt.pkgName
@@ -46,7 +47,7 @@ internal class ExtensionGithubApi {
val hasUpdate = availableExt.versionCode > installedExt.versionCode
if (hasUpdate) {
- extensionsWithUpdate.add(installedExt)
+ extensionsWithUpdate.add(availableExt)
}
}
@@ -75,7 +76,7 @@ internal class ExtensionGithubApi {
}
}
- fun getApkUrl(extension: Extension.Available): String {
+ fun getApkUrl(extension: ExtensionManager.ExtensionInfo): String {
return "${REPO_URL_PREFIX}apk/${extension.apkName}"
}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/model/InstallStep.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/model/InstallStep.kt
index c50188bb62..299555b6e8 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/extension/model/InstallStep.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/extension/model/InstallStep.kt
@@ -1,9 +1,9 @@
package eu.kanade.tachiyomi.extension.model
enum class InstallStep {
- Pending, Downloading, Loading, Installing, Installed, Error;
+ Pending, Downloading, Loading, Installing, Installed, Error, Done;
fun isCompleted(): Boolean {
- return this == Installed || this == Error
+ return this == Installed || this == Error || this == Done
}
}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstallActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstallActivity.kt
index e77f2d4092..8d081dd37c 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstallActivity.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstallActivity.kt
@@ -1,10 +1,14 @@
package eu.kanade.tachiyomi.extension.util
import android.app.Activity
+import android.app.DownloadManager
import android.app.PendingIntent
+import android.content.Context
import android.content.Intent
import android.content.pm.PackageInstaller
import android.content.pm.PackageInstaller.SessionParams
+import android.content.pm.PackageInstaller.SessionParams.USER_ACTION_NOT_REQUIRED
+import android.os.Build
import android.os.Bundle
import android.widget.Toast
import com.hippo.unifile.UniFile
@@ -35,10 +39,9 @@ class ExtensionInstallActivity : Activity() {
val params = SessionParams(
SessionParams.MODE_FULL_INSTALL
)
- // TODO: Add once compiling via SDK 31
-// if (Build.VERSION.SDK_INT >= 31) {
-// params.setRequireUserAction(USER_ACTION_NOT_REQUIRED)
-// }
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ params.setRequireUserAction(USER_ACTION_NOT_REQUIRED)
+ }
val sessionId = packageInstaller.createSession(params)
val session = packageInstaller.openSession(sessionId)
session.openWrite("package", 0, -1).use { packageInSession ->
@@ -55,6 +58,9 @@ class ExtensionInstallActivity : Activity() {
session.commit(statusReceiver)
val extensionManager: ExtensionManager by injectLazy()
extensionManager.setInstalling(downloadId, sessionId)
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ (getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager).remove(downloadId)
+ }
data.close()
} catch (error: Exception) {
// Either install package can't be found (probably bots) or there's a security exception
diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstaller.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstaller.kt
index 9b2ceda0d7..937c5df728 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstaller.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstaller.kt
@@ -5,20 +5,35 @@ import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
-import android.content.pm.PackageInstaller
import android.net.Uri
import android.os.Environment
import androidx.core.net.toUri
-import com.jakewharton.rxrelay.PublishRelay
-import eu.kanade.tachiyomi.extension.model.Extension
+import eu.kanade.tachiyomi.extension.ExtensionManager
import eu.kanade.tachiyomi.extension.model.InstallStep
import eu.kanade.tachiyomi.ui.extension.ExtensionIntallInfo
import eu.kanade.tachiyomi.util.storage.getUriCompat
-import rx.Observable
-import rx.android.schedulers.AndroidSchedulers
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.catch
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.emptyFlow
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.flatMapConcat
+import kotlinx.coroutines.flow.flattenMerge
+import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onCompletion
+import kotlinx.coroutines.flow.transformWhile
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.takeWhile
+import kotlinx.coroutines.launch
import timber.log.Timber
import java.io.File
-import java.util.concurrent.TimeUnit
/**
* The installer which installs, updates and uninstalls the extensions.
@@ -30,7 +45,8 @@ internal class ExtensionInstaller(private val context: Context) {
/**
* The system's download manager
*/
- private val downloadManager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
+ private val downloadManager =
+ context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
/**
* The broadcast receiver which listens to download completion events.
@@ -41,30 +57,24 @@ internal class ExtensionInstaller(private val context: Context) {
* The currently requested downloads, with the package name (unique id) as key, and the id
* returned by the download manager.
*/
- private val activeDownloads = hashMapOf()
+ val activeDownloads = hashMapOf()
/**
- * Relay used to notify the installation step of every download.
+ * StateFlow used to notify the installation step of every download.
*/
- private val downloadsRelay = PublishRelay.create>()
+ val downloadsStateFlow = MutableStateFlow(0L to ExtensionIntallInfo(InstallStep.Pending, null))
/** Map of download id to installer session id */
val downloadInstallerMap = hashMapOf()
- data class DownloadSessionInfo(
- val downloadId: Long,
- val session: PackageInstaller.Session,
- val sessionId: Int
- )
-
/**
- * Adds the given extension to the downloads queue and returns an observable containing its
+ * Adds the given extension to the downloads queue and returns a flow containing its
* step in the installation process.
*
* @param url The url of the apk.
* @param extension The extension to install.
*/
- fun downloadAndInstall(url: String, extension: Extension) = Observable.defer {
+ suspend fun downloadAndInstall(url: String, extension: ExtensionManager.ExtensionInfo, scope: CoroutineScope): Flow {
val pkgName = extension.pkgName
val oldDownload = activeDownloads[pkgName]
@@ -79,77 +89,120 @@ internal class ExtensionInstaller(private val context: Context) {
val request = DownloadManager.Request(downloadUri)
.setTitle(extension.name)
.setMimeType(APK_MIME)
- .setDestinationInExternalFilesDir(context, Environment.DIRECTORY_DOWNLOADS, downloadUri.lastPathSegment)
+ .setDestinationInExternalFilesDir(
+ context,
+ Environment.DIRECTORY_DOWNLOADS,
+ downloadUri.lastPathSegment
+ )
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
val id = downloadManager.enqueue(request)
activeDownloads[pkgName] = id
- downloadsRelay.filter { it.first == id }
- .map {
- val sessionId = downloadInstallerMap[it.first] ?: return@map it.second to null
- val session = context.packageManager.packageInstaller.getSessionInfo(sessionId)
- it.second to session
+ scope.launch {
+ flowOf(
+ pollStatus(id),
+ pollInstallStatus(id)
+ ).flattenMerge()
+ .transformWhile {
+ emit(it)
+ !it.first.isCompleted()
+ }
+ .flowOn(Dispatchers.IO)
+ .catch { e ->
+ Timber.e(e)
+ emit(InstallStep.Error to null)
+ }
+ .onCompletion {
+ deleteDownload(pkgName)
+ }
+ .collect {
+ downloadsStateFlow.emit(id to it)
+ }
+ }
+
+ return downloadsStateFlow.filter { it.first == id }.map { it.second }
+ .flowOn(Dispatchers.IO)
+ .transformWhile {
+ emit(it)
+ !it.first.isCompleted()
+ }
+ .onCompletion {
+ deleteDownload(pkgName)
}
- // Poll download status
- .mergeWith(pollStatus(id))
- // Poll installation status
- .mergeWith(pollInstallStatus(id))
- // Force an error if the download takes more than 3 minutes
- .mergeWith(Observable.timer(3, TimeUnit.MINUTES).map { InstallStep.Error to null })
- // Stop when the application is installed or errors
- .takeUntil { it.first.isCompleted() }
- // Always notify on main thread
- .observeOn(AndroidSchedulers.mainThread())
- // Always remove the download when unsubscribed
- .doOnUnsubscribe { deleteDownload(pkgName) }
}
/**
- * Returns an observable that polls the given download id for its status every second, as the
+ * Returns a flow that polls the given download id for its status every second, as the
* manager doesn't have any notification system. It'll stop once the download finishes.
*
* @param id The id of the download to poll.
*/
- private fun pollStatus(id: Long): Observable {
+ private fun pollStatus(id: Long): Flow {
val query = DownloadManager.Query().setFilterById(id)
- return Observable.interval(0, 1, TimeUnit.SECONDS)
- // Get the current download status
- .map {
- downloadManager.query(query).use { cursor ->
- cursor.moveToFirst()
- cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_STATUS))
+ return flow {
+ while (true) {
+ val newDownloadState = try {
+ downloadManager.query(query)?.use { cursor ->
+ cursor.moveToFirst()
+ cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_STATUS))
+ }
+ } catch (_: Exception) {
}
+ if (newDownloadState != null) {
+ emit(newDownloadState)
+ }
+ delay(1000)
}
- // Ignore duplicate results
+ }
.distinctUntilChanged()
- // Stop polling when the download fails or finishes
- .takeUntil { it == DownloadManager.STATUS_SUCCESSFUL || it == DownloadManager.STATUS_FAILED }
- // Map to our model
- .flatMap { status ->
- val step = when (status) {
+ .transformWhile {
+ emit(it)
+ !(it == DownloadManager.STATUS_SUCCESSFUL || it == DownloadManager.STATUS_FAILED)
+ }
+ .flatMapConcat { downloadState ->
+ val step = when (downloadState) {
DownloadManager.STATUS_PENDING -> InstallStep.Pending
DownloadManager.STATUS_RUNNING -> InstallStep.Downloading
- else -> return@flatMap Observable.empty()
+ else -> return@flatMapConcat emptyFlow()
}
- Observable.just(ExtensionIntallInfo(step, null))
- }
- .doOnError {
- Timber.e(it)
+ flowOf(ExtensionIntallInfo(step, null))
}
}
- private fun pollInstallStatus(id: Long): Observable {
- return Observable.interval(0, 500, TimeUnit.MILLISECONDS)
- .flatMap {
- val sessionId = downloadInstallerMap[id] ?: return@flatMap Observable.empty()
- val session = context.packageManager.packageInstaller.getSessionInfo(sessionId)
- Observable.just(InstallStep.Installing to session)
+ /**
+ * Returns a flow that polls the given installer session for its status every half second, as the
+ * manager doesn't have any notification system. This will only stop once
+ *
+ * @param id The id of the download mapped to the session to poll.
+ */
+ private fun pollInstallStatus(id: Long): Flow {
+ return flow {
+ while (true) {
+ val sessionId = downloadInstallerMap[id]
+ if (sessionId != null) {
+ val session =
+ context.packageManager.packageInstaller.getSessionInfo(sessionId)
+ emit(InstallStep.Installing to session)
+ }
+ delay(500)
}
- .doOnError {
+ }
+ .takeWhile { info ->
+ val sessionId = downloadInstallerMap[id]
+ if (sessionId != null) {
+ info.second != null
+ } else {
+ true
+ }
+ }
+ .catch {
Timber.e(it)
}
+ .onCompletion {
+ emit(InstallStep.Done to null)
+ }
}
/**
@@ -185,7 +238,7 @@ internal class ExtensionInstaller(private val context: Context) {
* @param downloadId The id of the download.
*/
fun setInstalling(downloadId: Long, sessionId: Int) {
- downloadsRelay.call(downloadId to InstallStep.Installing)
+ downloadsStateFlow.tryEmit(downloadId to ExtensionIntallInfo(InstallStep.Installing, null))
downloadInstallerMap[downloadId] = sessionId
}
@@ -204,7 +257,11 @@ internal class ExtensionInstaller(private val context: Context) {
fun setInstallationResult(downloadId: Long, result: Boolean) {
val step = if (result) InstallStep.Installed else InstallStep.Error
downloadInstallerMap.remove(downloadId)
- downloadsRelay.call(downloadId to step)
+ downloadsStateFlow.tryEmit(downloadId to ExtensionIntallInfo(step, null))
+ }
+
+ fun softDeleteDownload(downloadId: Long) {
+ downloadManager.remove(downloadId)
}
/**
@@ -267,10 +324,10 @@ internal class ExtensionInstaller(private val context: Context) {
// Set next installation step
if (uri != null) {
- downloadsRelay.call(id to InstallStep.Loading)
+ downloadsStateFlow.tryEmit(id to ExtensionIntallInfo(InstallStep.Loading, null))
} else {
Timber.e("Couldn't locate downloaded APK")
- downloadsRelay.call(id to InstallStep.Error)
+ downloadsStateFlow.tryEmit(id to ExtensionIntallInfo(InstallStep.Error, null))
return
}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionAdapter.kt
index a0d4dc24b8..2f2c77108e 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionAdapter.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionAdapter.kt
@@ -24,5 +24,6 @@ class ExtensionAdapter(val listener: OnButtonClickListener) :
interface OnButtonClickListener {
fun onButtonClick(position: Int)
fun onCancelClick(position: Int)
+ fun onUpdateAllClicked(position: Int)
}
}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionBottomPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionBottomPresenter.kt
index 59ff68e33d..5a26ba60d9 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionBottomPresenter.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionBottomPresenter.kt
@@ -19,12 +19,15 @@ import eu.kanade.tachiyomi.ui.migration.SelectionHeader
import eu.kanade.tachiyomi.ui.migration.SourceItem
import eu.kanade.tachiyomi.util.system.LocaleHelper
import eu.kanade.tachiyomi.util.system.executeOnIO
+import eu.kanade.tachiyomi.util.system.withUIContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
-import rx.Observable
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@@ -38,7 +41,7 @@ typealias ExtensionIntallInfo = Pair
class ExtensionBottomPresenter(
private val bottomSheet: ExtensionBottomSheet,
private val extensionManager: ExtensionManager = Injekt.get(),
- private val preferences: PreferencesHelper = Injekt.get()
+ val preferences: PreferencesHelper = Injekt.get()
) : BaseCoroutinePresenter(), ExtensionsChangedListener {
private var extensions = emptyList()
@@ -76,7 +79,10 @@ class ExtensionBottomPresenter(
sourceItems = findSourcesWithManga(favs)
mangaItems = HashMap(
sourceItems.associate {
- it.source.id to this@ExtensionBottomPresenter.libraryToMigrationItem(favs, it.source.id)
+ it.source.id to this@ExtensionBottomPresenter.libraryToMigrationItem(
+ favs,
+ it.source.id
+ )
}
)
withContext(Dispatchers.Main) {
@@ -89,6 +95,27 @@ class ExtensionBottomPresenter(
}
listOf(migrationJob, extensionJob).awaitAll()
}
+ presenterScope.launch {
+ extensionManager.downloadRelay
+ .collect {
+ val extPageName = extensionManager.getExtension(it.first)
+ val extension = extensions.find { item ->
+ extPageName == item.extension.pkgName
+ } ?: return@collect
+ when (it.second.first) {
+ InstallStep.Installed, InstallStep.Error -> {
+ currentDownloads.remove(extension.extension.pkgName)
+ }
+ else -> {
+ currentDownloads[extension.extension.pkgName] = it.second
+ }
+ }
+ val item = updateInstallStep(extension.extension, it.second.first, it.second.second)
+ if (item != null) {
+ withUIContext { bottomSheet.downloadUpdate(item) }
+ }
+ }
+ }
}
private fun findSourcesWithManga(library: List): List {
@@ -176,7 +203,8 @@ class ExtensionBottomPresenter(
updatesSorted.size,
updatesSorted.size
),
- updatesSorted.size
+ updatesSorted.size,
+ items.count { it.extension.pkgName in currentDownloads.keys } != updatesSorted.size
)
items += updatesSorted.map { extension ->
ExtensionItem(extension, header, currentDownloads[extension.pkgName])
@@ -215,7 +243,7 @@ class ExtensionBottomPresenter(
@Synchronized
private fun updateInstallStep(
extension: Extension,
- state: InstallStep,
+ state: InstallStep?,
session: PackageInstaller.SessionInfo?
): ExtensionItem? {
val extensions = extensions.toMutableList()
@@ -242,13 +270,21 @@ class ExtensionBottomPresenter(
fun installExtension(extension: Extension.Available) {
if (isNotMIUIOptimized()) {
- extensionManager.installExtension(extension).subscribeToInstallUpdate(extension)
+ presenterScope.launch {
+ extensionManager.installExtension(ExtensionManager.ExtensionInfo(extension), presenterScope)
+ .launchIn(this)
+ }
}
}
fun updateExtension(extension: Extension.Installed) {
if (isNotMIUIOptimized()) {
- extensionManager.updateExtension(extension).subscribeToInstallUpdate(extension)
+ val availableExt =
+ extensionManager.availableExtensions.find { it.pkgName == extension.pkgName } ?: return
+ presenterScope.launch {
+ extensionManager.installExtension(ExtensionManager.ExtensionInfo(availableExt), presenterScope)
+ .launchIn(this)
+ }
}
}
@@ -260,17 +296,6 @@ class ExtensionBottomPresenter(
return true
}
- private fun Observable.subscribeToInstallUpdate(extension: Extension) {
- this.doOnNext { currentDownloads[extension.pkgName] = it }
- .doOnUnsubscribe { currentDownloads.remove(extension.pkgName) }
- .map { state -> updateInstallStep(extension, state.first, state.second) }
- .subscribe { item ->
- if (item != null) {
- bottomSheet.downloadUpdate(item)
- }
- }
- }
-
fun uninstallExtension(pkgName: String) {
extensionManager.uninstallExtension(pkgName)
}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionBottomSheet.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionBottomSheet.kt
index efd1312963..687362ff82 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionBottomSheet.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionBottomSheet.kt
@@ -7,6 +7,7 @@ import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import androidx.recyclerview.widget.RecyclerView
+import com.afollestad.materialdialogs.MaterialDialog
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.tabs.TabLayout
import eu.davidea.flexibleadapter.FlexibleAdapter
@@ -201,6 +202,35 @@ class ExtensionBottomSheet @JvmOverloads constructor(context: Context, attrs: At
presenter.cancelExtensionInstall(extension)
}
+ override fun onUpdateAllClicked(position: Int) {
+ if (!presenter.preferences.hasPromptedBeforeUpdateAll().get()) {
+ MaterialDialog(controller.activity!!)
+ .title(R.string.update_all)
+ .message(R.string.some_extensions_may_prompt)
+ .positiveButton(android.R.string.ok) {
+ presenter.preferences.hasPromptedBeforeUpdateAll().set(true)
+ updateAllExtensions(position)
+ }
+ .show()
+ } else {
+ updateAllExtensions(position)
+ }
+ }
+
+ fun updateAllExtensions(position: Int) {
+ val header = (extAdapter?.getSectionHeader(position)) as? ExtensionGroupItem ?: return
+ val items = extAdapter?.getSectionItemPositions(header)
+ items?.forEach {
+ val extItem = (extAdapter?.getItem(it) as? ExtensionItem) ?: return
+ val extension = (extAdapter?.getItem(it) as? ExtensionItem)?.extension ?: return
+ if (extItem.installStep == null &&
+ extension is Extension.Installed && extension.hasUpdate
+ ) {
+ presenter.updateExtension(extension)
+ }
+ }
+ }
+
override fun onItemClick(view: View?, position: Int): Boolean {
when (binding.tabs.selectedTabPosition) {
0 -> {
@@ -298,6 +328,7 @@ class ExtensionBottomSheet @JvmOverloads constructor(context: Context, attrs: At
extAdapter?.updateDataSet(extensions)
}
updateExtTitle()
+ updateExtUpdateAllButton()
}
fun canGoBack(): Boolean {
@@ -310,6 +341,20 @@ class ExtensionBottomSheet @JvmOverloads constructor(context: Context, attrs: At
fun downloadUpdate(item: ExtensionItem) {
extAdapter?.updateItem(item, item.installStep)
+ updateExtUpdateAllButton()
+ }
+
+ fun updateExtUpdateAllButton() {
+ val updateHeader =
+ extAdapter?.headerItems?.find { it is ExtensionGroupItem && it.canUpdate != null } as? ExtensionGroupItem
+ ?: return
+ val items = extAdapter?.getSectionItemPositions(updateHeader) ?: return
+ updateHeader.canUpdate = items.any {
+ val extItem = (extAdapter?.getItem(it) as? ExtensionItem) ?: return
+ val extension = (extAdapter?.getItem(it) as? ExtensionItem)?.extension ?: return
+ extItem.installStep == null
+ }
+ extAdapter?.updateItem(updateHeader)
}
override fun trustSignature(signatureHash: String) {
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionGroupHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionGroupHolder.kt
index f6f00096f9..4c16428d90 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionGroupHolder.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionGroupHolder.kt
@@ -1,7 +1,9 @@
package eu.kanade.tachiyomi.ui.extension
import android.annotation.SuppressLint
+import android.os.Build
import android.view.View
+import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.IFlexible
@@ -13,8 +15,16 @@ class ExtensionGroupHolder(view: View, adapter: FlexibleAdapter= Build.VERSION_CODES.S
+ binding.extButton.isEnabled = item.canUpdate == true
}
}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionGroupItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionGroupItem.kt
index 8bd8fc78f4..10cefb51ba 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionGroupItem.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionGroupItem.kt
@@ -13,7 +13,7 @@ import eu.kanade.tachiyomi.R
* @param name The header name.
* @param size The number of items in the group.
*/
-data class ExtensionGroupItem(val name: String, val size: Int) : AbstractHeaderItem() {
+data class ExtensionGroupItem(val name: String, val size: Int, var canUpdate: Boolean? = null) : AbstractHeaderItem() {
/**
* Returns the layout resource of this item.
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionHolder.kt
index da24759def..3706c5f04f 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionHolder.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/extension/ExtensionHolder.kt
@@ -68,6 +68,7 @@ class ExtensionHolder(view: View, val adapter: ExtensionAdapter) :
@Suppress("ResourceType")
fun bindButton(item: ExtensionItem) = with(binding.extButton) {
+ if (item.installStep == InstallStep.Done) return@with
isEnabled = true
isClickable = true
isActivated = false
@@ -87,6 +88,7 @@ class ExtensionHolder(view: View, val adapter: ExtensionAdapter) :
InstallStep.Installing -> R.string.installing
InstallStep.Installed -> R.string.installed
InstallStep.Error -> R.string.retry
+ else -> return@with
}
)
if (installStep != InstallStep.Error) {
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt
index 55e124b954..1c385eabd5 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt
@@ -3,10 +3,12 @@ package eu.kanade.tachiyomi.ui.main
import android.animation.AnimatorSet
import android.animation.ValueAnimator
import android.app.Dialog
+import android.app.assist.AssistContent
import android.content.Intent
import android.graphics.Color
import android.graphics.Rect
import android.graphics.drawable.Drawable
+import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Handler
@@ -55,6 +57,7 @@ import eu.kanade.tachiyomi.data.updater.UpdateResult
import eu.kanade.tachiyomi.data.updater.UpdaterNotifier
import eu.kanade.tachiyomi.databinding.MainActivityBinding
import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi
+import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.ui.base.MaterialMenuSheet
import eu.kanade.tachiyomi.ui.base.activity.BaseActivity
import eu.kanade.tachiyomi.ui.base.controller.BaseController
@@ -647,6 +650,25 @@ open class MainActivity : BaseActivity(), DownloadServiceLi
return true
}
+ override fun onProvideAssistContent(outContent: AssistContent) {
+ super.onProvideAssistContent(outContent)
+ when (val controller = router.backstack.lastOrNull()?.controller) {
+ is MangaDetailsController -> {
+ val source = controller.presenter.source as? HttpSource ?: return
+ val url = try {
+ source.mangaDetailsRequest(controller.presenter.manga).url.toString()
+ } catch (e: Exception) {
+ return
+ }
+ outContent.webUri = Uri.parse(url)
+ }
+ is BrowseSourceController -> {
+ val source = controller.presenter.source as? HttpSource ?: return
+ outContent.webUri = Uri.parse(source.baseUrl)
+ }
+ }
+ }
+
override fun onDestroy() {
super.onDestroy()
overflowDialog?.dismiss()
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaHeaderHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaHeaderHolder.kt
index 10fc11fadf..f92505024b 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaHeaderHolder.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaHeaderHolder.kt
@@ -4,6 +4,9 @@ import android.annotation.SuppressLint
import android.content.res.ColorStateList
import android.graphics.Color
import android.view.LayoutInflater
+import android.graphics.RenderEffect
+import android.graphics.Shader
+import android.os.Build
import android.view.MotionEvent
import android.view.View
import androidx.constraintlayout.widget.ConstraintLayout
@@ -124,6 +127,15 @@ class MangaHeaderHolder(
)
true
}
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ backdrop.setRenderEffect(
+ RenderEffect.createBlurEffect(
+ 10f,
+ 10f,
+ Shader.TileMode.MIRROR
+ )
+ )
+ }
mangaCover.setOnClickListener { adapter.delegate.zoomImageFromThumb(coverCard) }
trackButton.setOnClickListener { adapter.delegate.showTrackingSheet() }
if (startExpanded) expandDesc()
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt
index c137ac181a..62b408a7d7 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt
@@ -1,6 +1,7 @@
package eu.kanade.tachiyomi.ui.reader
import android.annotation.SuppressLint
+import android.app.assist.AssistContent
import android.content.ClipData
import android.content.Context
import android.content.Intent
@@ -9,6 +10,7 @@ import android.content.res.Configuration
import android.graphics.Bitmap
import android.graphics.Color
import android.graphics.drawable.LayerDrawable
+import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.view.KeyEvent
@@ -41,6 +43,7 @@ import eu.kanade.tachiyomi.data.preference.asImmediateFlowIn
import eu.kanade.tachiyomi.data.preference.toggle
import eu.kanade.tachiyomi.databinding.ReaderActivityBinding
import eu.kanade.tachiyomi.source.model.Page
+import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.ui.base.MaterialMenuSheet
import eu.kanade.tachiyomi.ui.base.activity.BaseRxActivity
import eu.kanade.tachiyomi.ui.main.MainActivity
@@ -1225,6 +1228,18 @@ class ReaderActivity :
startActivity(Intent.createChooser(intent, getString(R.string.share)))
}
+ override fun onProvideAssistContent(outContent: AssistContent) {
+ super.onProvideAssistContent(outContent)
+ val manga = presenter.manga ?: return
+ val source = presenter.source as? HttpSource ?: return
+ val url = try {
+ source.mangaDetailsRequest(manga).url.toString()
+ } catch (e: Exception) {
+ return
+ }
+ outContent.webUri = Uri.parse(url)
+ }
+
/**
* Called from the page sheet. It delegates saving the image of the given [page] on external
* storage to the presenter.
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt
index da4db21345..e50384e9f8 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt
@@ -20,6 +20,7 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.track.DelayedTrackingUpdateJob
import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.source.LocalSource
+import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.online.HttpSource
@@ -76,6 +77,9 @@ class ReaderPresenter(
var manga: Manga? = null
private set
+ val source: Source?
+ get() = manga?.source?.let { sourceManager.getOrStub(it) }
+
/**
* The chapter id of the currently loaded chapter. Used to restore from process kill.
*/
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/AboutController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/AboutController.kt
index 1938da45fe..e0df1f654c 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/AboutController.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/AboutController.kt
@@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.ui.setting
import android.app.Dialog
import android.content.Intent
+import android.os.Build
import android.os.Bundle
import androidx.core.net.toUri
import androidx.preference.PreferenceScreen
@@ -190,15 +191,16 @@ class AboutController : SettingsController() {
)
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
+ val isOnA12 = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
return MaterialDialog(activity!!)
.title(R.string.new_version_available)
.message(text = args.getString(BODY_KEY) ?: "")
- .positiveButton(R.string.download) {
+ .positiveButton(if (isOnA12) R.string.update else R.string.download) {
val appContext = applicationContext
if (appContext != null) {
// Start download
val url = args.getString(URL_KEY) ?: ""
- UpdaterService.start(appContext, url)
+ UpdaterService.start(appContext, url, true)
}
}
.negativeButton(R.string.ignore)
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsGeneralController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsGeneralController.kt
index 678fd27699..a9f1a0830a 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsGeneralController.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsGeneralController.kt
@@ -1,5 +1,6 @@
package eu.kanade.tachiyomi.ui.setting
+import android.os.Build
import android.os.Bundle
import android.view.View
import androidx.appcompat.app.AppCompatDelegate
@@ -8,6 +9,7 @@ import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.asImmediateFlow
import eu.kanade.tachiyomi.data.preference.asImmediateFlowIn
+import eu.kanade.tachiyomi.data.updater.AutoUpdaterJob
import eu.kanade.tachiyomi.ui.main.MainActivity
import eu.kanade.tachiyomi.util.system.LocaleHelper
import eu.kanade.tachiyomi.util.system.appDelegateNightMode
@@ -270,6 +272,20 @@ class SettingsGeneralController : SettingsController() {
defaultValue = ""
}
}
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && isUpdaterEnabled) {
+ preferenceCategory {
+ titleRes = R.string.auto_updates
+
+ intListPreference(activity) {
+ key = Keys.shouldAutoUpdate
+ titleRes = R.string.auto_update_app
+ entryRange = 0..2
+ entriesRes = arrayOf(R.string.over_any_network, R.string.over_wifi_only, R.string.dont_auto_update)
+ defaultValue = AutoUpdaterJob.ONLY_ON_UNMETERED
+ }
+ }
+ }
}
override fun onDestroyView(view: View) {
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/ThemePreference.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/ThemePreference.kt
index 43c9f10972..be21d53f76 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/ThemePreference.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/ThemePreference.kt
@@ -4,6 +4,7 @@ import android.app.Activity
import android.content.Context
import android.content.res.ColorStateList
import android.graphics.Color
+import android.os.Build
import android.util.AttributeSet
import android.view.View
import androidx.appcompat.app.AppCompatDelegate
@@ -26,6 +27,7 @@ import eu.kanade.tachiyomi.databinding.ThemesPreferenceBinding
import eu.kanade.tachiyomi.util.system.ThemeUtil
import eu.kanade.tachiyomi.util.system.Themes
import eu.kanade.tachiyomi.util.system.appDelegateNightMode
+import eu.kanade.tachiyomi.util.system.contextCompatColor
import eu.kanade.tachiyomi.util.system.dpToPx
import eu.kanade.tachiyomi.util.system.isInNightMode
import uy.kohesive.injekt.injectLazy
@@ -56,14 +58,15 @@ class ThemePreference @JvmOverloads constructor(context: Context, attrs: Attribu
selectExtensionLight = fastAdapterLight.getSelectExtension().setThemeListener(false)
selectExtensionDark = fastAdapterDark.getSelectExtension().setThemeListener(true)
val enumConstants = Themes.values()
+ val isOnA12 = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
itemAdapterLight.set(
enumConstants
- .filter { !it.isDarkTheme || it.followsSystem }
+ .filter { (!it.isDarkTheme || it.followsSystem) && (it.styleRes != R.style.Theme_Tachiyomi_Monet || isOnA12) }
.map { ThemeItem(it, false) }
)
itemAdapterDark.set(
enumConstants
- .filter { it.isDarkTheme || it.followsSystem }
+ .filter { (it.isDarkTheme || it.followsSystem) && (it.styleRes != R.style.Theme_Tachiyomi_Monet || isOnA12) }
.map { ThemeItem(it, true) }
)
isSelectable = false
@@ -207,6 +210,7 @@ class ThemePreference @JvmOverloads constructor(context: Context, attrs: Attribu
inner class ViewHolder(view: View) : FastAdapter.ViewHolder(view) {
val binding = ThemeItemBinding.bind(view)
+
override fun bindView(item: ThemeItem, payloads: List) {
binding.themeNameText.setText(
if (item.isDarkTheme) {
@@ -228,27 +232,75 @@ class ThemePreference @JvmOverloads constructor(context: Context, attrs: Attribu
binding.themeSelected.alpha = if (themeMatchesApp) 1f else 0.5f
binding.checkbox.alpha = if (themeMatchesApp) 1f else 0.5f
}
- binding.themeToolbar.setBackgroundColor(item.colors.appBar)
- binding.themeAppBarText.imageTintList =
- ColorStateList.valueOf(item.colors.appBarText)
- binding.themeHeroImage.imageTintList =
- ColorStateList.valueOf(item.colors.primaryText)
- binding.themePrimaryText.imageTintList =
- ColorStateList.valueOf(item.colors.primaryText)
- binding.themeAccentedButton.imageTintList =
- ColorStateList.valueOf(item.colors.colorAccent)
- binding.themeSecondaryText.imageTintList =
- ColorStateList.valueOf(item.colors.secondaryText)
- binding.themeSecondaryText2.imageTintList =
- ColorStateList.valueOf(item.colors.secondaryText)
+ if (item.theme.styleRes == R.style.Theme_Tachiyomi_Monet &&
+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
+ ) {
+ val nightMode = item.isDarkTheme
+ val appBar = context.contextCompatColor(
+ if (nightMode) android.R.color.system_neutral1_900
+ else android.R.color.system_neutral1_50
+ )
+ val appBarText = context.contextCompatColor(
+ if (nightMode) android.R.color.system_accent2_10
+ else android.R.color.system_accent2_800
+ )
+ val colorAccent = context.contextCompatColor(
+ if (nightMode) android.R.color.system_accent1_300
+ else android.R.color.system_accent1_500
+ )
+ val bottomBar = context.contextCompatColor(
+ if (nightMode) android.R.color.system_neutral1_800
+ else android.R.color.system_accent2_100
+ )
+ val colorBackground = context.contextCompatColor(
+ if (nightMode) android.R.color.system_neutral1_900
+ else android.R.color.system_neutral1_50
+ )
- binding.themeBottomBar.setBackgroundColor(item.colors.bottomBar)
- binding.themeItem1.imageTintList =
- ColorStateList.valueOf(item.colors.inactiveTab)
- binding.themeItem2.imageTintList = ColorStateList.valueOf(item.colors.activeTab)
- binding.themeItem3.imageTintList =
- ColorStateList.valueOf(item.colors.inactiveTab)
- binding.themeLayout.setBackgroundColor(item.colors.colorBackground)
+ binding.themeToolbar.setBackgroundColor(appBar)
+ binding.themeAppBarText.imageTintList =
+ ColorStateList.valueOf(appBarText)
+ binding.themeHeroImage.imageTintList =
+ ColorStateList.valueOf(item.colors.primaryText)
+ binding.themePrimaryText.imageTintList =
+ ColorStateList.valueOf(item.colors.primaryText)
+ binding.themeAccentedButton.imageTintList =
+ ColorStateList.valueOf(colorAccent)
+ binding.themeSecondaryText.imageTintList =
+ ColorStateList.valueOf(item.colors.secondaryText)
+ binding.themeSecondaryText2.imageTintList =
+ ColorStateList.valueOf(item.colors.secondaryText)
+
+ binding.themeBottomBar.setBackgroundColor(bottomBar)
+ binding.themeItem1.imageTintList =
+ ColorStateList.valueOf(item.colors.inactiveTab)
+ binding.themeItem2.imageTintList = ColorStateList.valueOf(colorAccent)
+ binding.themeItem3.imageTintList =
+ ColorStateList.valueOf(item.colors.inactiveTab)
+ binding.themeLayout.setBackgroundColor(colorBackground)
+ } else {
+ binding.themeToolbar.setBackgroundColor(item.colors.appBar)
+ binding.themeAppBarText.imageTintList =
+ ColorStateList.valueOf(item.colors.appBarText)
+ binding.themeHeroImage.imageTintList =
+ ColorStateList.valueOf(item.colors.primaryText)
+ binding.themePrimaryText.imageTintList =
+ ColorStateList.valueOf(item.colors.primaryText)
+ binding.themeAccentedButton.imageTintList =
+ ColorStateList.valueOf(item.colors.colorAccent)
+ binding.themeSecondaryText.imageTintList =
+ ColorStateList.valueOf(item.colors.secondaryText)
+ binding.themeSecondaryText2.imageTintList =
+ ColorStateList.valueOf(item.colors.secondaryText)
+
+ binding.themeBottomBar.setBackgroundColor(item.colors.bottomBar)
+ binding.themeItem1.imageTintList =
+ ColorStateList.valueOf(item.colors.inactiveTab)
+ binding.themeItem2.imageTintList = ColorStateList.valueOf(item.colors.activeTab)
+ binding.themeItem3.imageTintList =
+ ColorStateList.valueOf(item.colors.inactiveTab)
+ binding.themeLayout.setBackgroundColor(item.colors.colorBackground)
+ }
if (item.isDarkTheme && preferences.themeDarkAmoled().get()) {
binding.themeLayout.setBackgroundColor(Color.BLACK)
if (!ThemeUtil.isColoredTheme(item.theme)) {
diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/system/Themes.kt b/app/src/main/java/eu/kanade/tachiyomi/util/system/Themes.kt
index cecf938f2e..2b4bb83de0 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/util/system/Themes.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/util/system/Themes.kt
@@ -11,6 +11,12 @@ import kotlin.math.roundToInt
@Suppress("unused")
enum class Themes(@StyleRes val styleRes: Int, val nightMode: Int, @StringRes val nameRes: Int, @StringRes altNameRes: Int? = null) {
+ MONET(
+ R.style.Theme_Tachiyomi_Monet,
+ AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM,
+ R.string.a_brighter_you,
+ R.string.a_calmer_you
+ ),
DEFAULT(
R.style.Theme_Tachiyomi,
AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM,
diff --git a/app/src/main/res/color/accent_text_btn_color_selector.xml b/app/src/main/res/color/accent_text_btn_color_selector.xml
new file mode 100644
index 0000000000..3c44c75b60
--- /dev/null
+++ b/app/src/main/res/color/accent_text_btn_color_selector.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/anim_tachij2k_splash.xml b/app/src/main/res/drawable/anim_tachij2k_splash.xml
new file mode 100644
index 0000000000..5543b6ce0a
--- /dev/null
+++ b/app/src/main/res/drawable/anim_tachij2k_splash.xml
@@ -0,0 +1,171 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/extension_card_header.xml b/app/src/main/res/layout/extension_card_header.xml
index 3b5d2c1763..416b0be1ac 100644
--- a/app/src/main/res/layout/extension_card_header.xml
+++ b/app/src/main/res/layout/extension_card_header.xml
@@ -24,6 +24,25 @@
android:layout_marginTop="20dp"
tools:text="Title"/>
+
+
diff --git a/app/src/main/res/values-night-v31/themes.xml b/app/src/main/res/values-night-v31/themes.xml
new file mode 100644
index 0000000000..eed409b7ec
--- /dev/null
+++ b/app/src/main/res/values-night-v31/themes.xml
@@ -0,0 +1,15 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml
index 16dabf1ce0..3921fd9b2f 100644
--- a/app/src/main/res/values-night/colors.xml
+++ b/app/src/main/res/values-night/colors.xml
@@ -18,7 +18,7 @@
@color/md_white_1000_12
@color/material_green_700
#cc4444
-
+ @color/md_white_1000
#1C1C1D
#212121
diff --git a/app/src/main/res/values-v31/themes.xml b/app/src/main/res/values-v31/themes.xml
new file mode 100644
index 0000000000..882a83cb4f
--- /dev/null
+++ b/app/src/main/res/values-v31/themes.xml
@@ -0,0 +1,14 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
index 7d1720c093..741b0db70e 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -34,6 +34,7 @@
@color/md_grey_50
@color/md_white_1000
+ @color/md_black_1000
#DE000000
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 9467ff6ff4..c18f34433b 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -117,6 +117,8 @@
No new updates available
Searching for updates…
Release page
+ Could not install update
+ Update completed
@@ -312,6 +314,9 @@
App info
%1$s must be enabled first
Could not install extension
+ Update all
+ Some extensions may still prompt to be installed first.
+ Updating extensions
- %d update pending
- %d updates pending
@@ -625,6 +630,8 @@
Midnight Dusk
Spring Blossom
Strawberry Daiquiri
+ A Brighter You
+ A Calmer You
Yotsuba
Yin
Yang
@@ -651,6 +658,11 @@
Starting screen
Back to start
Pressing back to starting screen
+ Auto-updates
+ Auto-update app
+ Over Wi-Fi only
+ Over any network
+ Don\'t auto-update
App shortcuts
Show recently used sources
@@ -980,5 +992,6 @@
Use default
View all errors
View chapters
+ Warning
Wi-Fi
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
index 6ae4156e8d..623c32113b 100644
--- a/app/src/main/res/values/themes.xml
+++ b/app/src/main/res/values/themes.xml
@@ -10,6 +10,9 @@
- false
- false
- true
+ - @color/colorPrimary
+ - @drawable/anim_tachij2k_splash
+ - 775
- @color/app_color_primary
- @color/colorPrimary
- @color/background
@@ -105,6 +108,7 @@
- @color/colorAccentYinyangText
- @color/colorOnAccentYinyang
+
diff --git a/buildSrc/src/main/kotlin/Dependencies.kt b/buildSrc/src/main/kotlin/Dependencies.kt
index c6bb5b7277..323b22bf11 100644
--- a/buildSrc/src/main/kotlin/Dependencies.kt
+++ b/buildSrc/src/main/kotlin/Dependencies.kt
@@ -93,7 +93,7 @@ object LegacyPluginClassPath {
}
object AndroidVersions {
- const val compileSdk = 30
+ const val compileSdk = 31
const val minSdk = 23
const val targetSdk = 30
const val versionCode = 77