From 0248f84ca04f45f59598d72d12c7cfa3caa6e905 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Thu, 18 Aug 2022 16:13:24 +0300 Subject: [PATCH] Fix in-app update checking --- .../core/github/AppUpdateRepository.kt | 8 +- .../kotatsu/core/prefs/AppSettings.kt | 20 +--- .../koitharu/kotatsu/main/ui/MainViewModel.kt | 5 +- .../kotatsu/settings/AppUpdateChecker.kt | 110 ------------------ .../settings/about/AboutSettingsFragment.kt | 51 ++++---- .../settings/about/AboutSettingsViewModel.kt | 24 ++++ .../kotatsu/settings/about/AppUpdateDialog.kt | 39 +++++++ .../kotatsu/settings/tools/ToolsFragment.kt | 5 + app/src/main/res/layout/layout_app_update.xml | 5 +- app/src/main/res/xml/pref_about.xml | 6 - 10 files changed, 106 insertions(+), 167 deletions(-) delete mode 100644 app/src/main/java/org/koitharu/kotatsu/settings/AppUpdateChecker.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/settings/about/AboutSettingsViewModel.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/settings/about/AppUpdateDialog.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/core/github/AppUpdateRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/github/AppUpdateRepository.kt index 62ea43d64..84bc96d21 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/github/AppUpdateRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/github/AppUpdateRepository.kt @@ -11,8 +11,10 @@ import java.security.cert.CertificateFactory import java.security.cert.X509Certificate import javax.inject.Inject import javax.inject.Singleton +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.withContext import okhttp3.OkHttpClient import okhttp3.Request import org.koitharu.kotatsu.BuildConfig @@ -53,11 +55,11 @@ class AppUpdateRepository @Inject constructor( } } - suspend fun fetchUpdate(): AppVersion? { + suspend fun fetchUpdate(): AppVersion? = withContext(Dispatchers.Default) { if (!isUpdateSupported()) { - return null + return@withContext null } - return runCatching { + runCatching { val currentVersion = VersionId(BuildConfig.VERSION_NAME) val available = getAvailableVersions().asArrayList() available.sortBy { it.versionId } diff --git a/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt b/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt index 53360ca0a..17fbf1078 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt @@ -11,6 +11,12 @@ import androidx.core.content.edit import androidx.preference.PreferenceManager import com.google.android.material.color.DynamicColors import dagger.hilt.android.qualifiers.ApplicationContext +import java.io.File +import java.text.DateFormat +import java.text.SimpleDateFormat +import java.util.* +import javax.inject.Inject +import javax.inject.Singleton import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.core.model.ZoomMode import org.koitharu.kotatsu.core.network.DoHProvider @@ -19,12 +25,6 @@ import org.koitharu.kotatsu.utils.ext.getEnumValue import org.koitharu.kotatsu.utils.ext.observe import org.koitharu.kotatsu.utils.ext.putEnumValue import org.koitharu.kotatsu.utils.ext.toUriOrNull -import java.io.File -import java.text.DateFormat -import java.text.SimpleDateFormat -import java.util.* -import javax.inject.Inject -import javax.inject.Singleton @Singleton class AppSettings @Inject constructor(@ApplicationContext context: Context) { @@ -73,13 +73,6 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) { get() = prefs.getBoolean(KEY_ALL_FAVOURITES_VISIBLE, true) set(value) = prefs.edit { putBoolean(KEY_ALL_FAVOURITES_VISIBLE, value) } - val isUpdateCheckingEnabled: Boolean - get() = prefs.getBoolean(KEY_APP_UPDATE_AUTO, true) - - var lastUpdateCheckTimestamp: Long - get() = prefs.getLong(KEY_APP_UPDATE, 0L) - set(value) = prefs.edit { putLong(KEY_APP_UPDATE, value) } - val isTrackerEnabled: Boolean get() = prefs.getBoolean(KEY_TRACKER_ENABLED, true) @@ -327,7 +320,6 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) { // About const val KEY_APP_UPDATE = "app_update" - const val KEY_APP_UPDATE_AUTO = "app_update_auto" const val KEY_APP_TRANSLATION = "about_app_translation" private const val NETWORK_NEVER = 0 diff --git a/app/src/main/java/org/koitharu/kotatsu/main/ui/MainViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/main/ui/MainViewModel.kt index c60ae4aa8..1f117f03f 100644 --- a/app/src/main/java/org/koitharu/kotatsu/main/ui/MainViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/main/ui/MainViewModel.kt @@ -4,6 +4,7 @@ import android.util.SparseIntArray import androidx.core.util.set import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.combine import org.koitharu.kotatsu.R @@ -16,12 +17,10 @@ import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.tracker.domain.TrackingRepository import org.koitharu.kotatsu.utils.SingleLiveEvent import org.koitharu.kotatsu.utils.ext.asLiveDataDistinct -import javax.inject.Inject @HiltViewModel class MainViewModel @Inject constructor( private val historyRepository: HistoryRepository, - private val settings: AppSettings, private val appUpdateRepository: AppUpdateRepository, private val trackingRepository: TrackingRepository, ) : BaseViewModel() { @@ -43,7 +42,7 @@ class MainViewModel @Inject constructor( }.asLiveDataDistinct(viewModelScope.coroutineContext + Dispatchers.Default, SparseIntArray(0)) init { - launchJob(Dispatchers.Default) { + launchJob { appUpdateRepository.fetchUpdate() } } diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/AppUpdateChecker.kt b/app/src/main/java/org/koitharu/kotatsu/settings/AppUpdateChecker.kt deleted file mode 100644 index 7d8cccdd8..000000000 --- a/app/src/main/java/org/koitharu/kotatsu/settings/AppUpdateChecker.kt +++ /dev/null @@ -1,110 +0,0 @@ -package org.koitharu.kotatsu.settings - -import android.annotation.SuppressLint -import android.content.Context -import android.content.Intent -import android.content.pm.PackageManager -import androidx.activity.ComponentActivity -import androidx.annotation.MainThread -import androidx.core.net.toUri -import com.google.android.material.R as materialR -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import java.io.ByteArrayInputStream -import java.io.InputStream -import java.security.MessageDigest -import java.security.cert.CertificateFactory -import java.security.cert.X509Certificate -import java.util.concurrent.TimeUnit -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import org.koitharu.kotatsu.BuildConfig -import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.core.github.AppUpdateRepository -import org.koitharu.kotatsu.core.github.AppVersion -import org.koitharu.kotatsu.core.github.VersionId -import org.koitharu.kotatsu.core.prefs.AppSettings -import org.koitharu.kotatsu.parsers.util.byte2HexFormatted -import org.koitharu.kotatsu.utils.FileSize -import org.koitharu.kotatsu.utils.ext.printStackTraceDebug - -@Deprecated("") -class AppUpdateChecker(private val activity: ComponentActivity) { - - private val settings: AppSettings = TODO() - private val repo: AppUpdateRepository = TODO() - - suspend fun checkIfNeeded(): Boolean? = if ( - settings.isUpdateCheckingEnabled && - settings.lastUpdateCheckTimestamp + PERIOD < System.currentTimeMillis() - ) { - checkNow() - } else { - null - } - - suspend fun checkNow() = runCatching { - val version = repo.fetchUpdate() ?: return@runCatching false - val newVersionId = VersionId(version.name) - val currentVersionId = VersionId(BuildConfig.VERSION_NAME) - val result = newVersionId > currentVersionId - if (result) { - withContext(Dispatchers.Main) { - showUpdateDialog(version) - } - } - settings.lastUpdateCheckTimestamp = System.currentTimeMillis() - result - }.onFailure { - it.printStackTraceDebug() - }.getOrNull() - - @MainThread - private fun showUpdateDialog(version: AppVersion) { - val message = buildString { - append(activity.getString(R.string.new_version_s, version.name)) - appendLine() - append(activity.getString(R.string.size_s, FileSize.BYTES.format(activity, version.apkSize))) - appendLine() - appendLine() - append(version.description) - } - MaterialAlertDialogBuilder(activity, materialR.style.ThemeOverlay_Material3_MaterialAlertDialog_Centered) - .setTitle(R.string.app_update_available) - .setMessage(message) - .setIcon(R.drawable.ic_app_update) - .setPositiveButton(R.string.download) { _, _ -> - val intent = Intent(Intent.ACTION_VIEW, version.apkUrl.toUri()) - activity.startActivity(Intent.createChooser(intent, activity.getString(R.string.open_in_browser))) - } - .setNegativeButton(R.string.close, null) - .setCancelable(false) - .create() - .show() - } - - companion object { - - private const val CERT_SHA1 = "2C:19:C7:E8:07:61:2B:8E:94:51:1B:FD:72:67:07:64:5D:C2:58:AE" - private val PERIOD = TimeUnit.HOURS.toMillis(6) - - fun isUpdateSupported(context: Context): Boolean { - return BuildConfig.DEBUG || getCertificateSHA1Fingerprint(context) == CERT_SHA1 - } - - @Suppress("DEPRECATION") - @SuppressLint("PackageManagerGetSignatures") - private fun getCertificateSHA1Fingerprint(context: Context): String? = runCatching { - val packageInfo = context.packageManager.getPackageInfo(context.packageName, PackageManager.GET_SIGNATURES) - val signatures = requireNotNull(packageInfo?.signatures) - val cert: ByteArray = signatures.first().toByteArray() - val input: InputStream = ByteArrayInputStream(cert) - val cf = CertificateFactory.getInstance("X509") - val c = cf.generateCertificate(input) as X509Certificate - val md: MessageDigest = MessageDigest.getInstance("SHA1") - val publicKey: ByteArray = md.digest(c.encoded) - return publicKey.byte2HexFormatted() - }.onFailure { error -> - error.printStackTraceDebug() - }.getOrNull() - } -} diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/about/AboutSettingsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/about/AboutSettingsFragment.kt index 985ca0d1a..199cc513e 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/about/AboutSettingsFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/about/AboutSettingsFragment.kt @@ -2,34 +2,41 @@ package org.koitharu.kotatsu.settings.about import android.content.Intent import android.os.Bundle +import android.view.View import androidx.core.net.toUri +import androidx.fragment.app.viewModels import androidx.preference.Preference -import kotlinx.coroutines.launch +import com.google.android.material.snackbar.Snackbar import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.R import org.koitharu.kotatsu.base.ui.BasePreferenceFragment +import org.koitharu.kotatsu.core.github.AppVersion import org.koitharu.kotatsu.core.prefs.AppSettings -import org.koitharu.kotatsu.settings.AppUpdateChecker -import org.koitharu.kotatsu.utils.ext.viewLifecycleScope class AboutSettingsFragment : BasePreferenceFragment(R.string.about) { + private val viewModel by viewModels() + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { addPreferencesFromResource(R.xml.pref_about) - val isUpdateSupported = AppUpdateChecker.isUpdateSupported(requireContext()) - findPreference(AppSettings.KEY_APP_UPDATE_AUTO)?.run { - isVisible = isUpdateSupported - } findPreference(AppSettings.KEY_APP_VERSION)?.run { title = getString(R.string.app_version, BuildConfig.VERSION_NAME) - isEnabled = isUpdateSupported + isEnabled = viewModel.isUpdateSupported + } + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + viewModel.isLoading.observe(viewLifecycleOwner) { + findPreference(AppSettings.KEY_APP_UPDATE)?.isEnabled = !it } + viewModel.onUpdateAvailable.observe(viewLifecycleOwner, ::onUpdateAvailable) } override fun onPreferenceTreeClick(preference: Preference): Boolean { return when (preference.key) { AppSettings.KEY_APP_VERSION -> { - checkForUpdates() + viewModel.checkForUpdates() true } AppSettings.KEY_APP_TRANSLATION -> { @@ -40,24 +47,12 @@ class AboutSettingsFragment : BasePreferenceFragment(R.string.about) { } } - private fun checkForUpdates() { - viewLifecycleScope.launch { - findPreference(AppSettings.KEY_APP_VERSION)?.run { - setSummary(R.string.checking_for_updates) - isSelectable = false - } - val result = AppUpdateChecker(activity ?: return@launch).checkNow() - findPreference(AppSettings.KEY_APP_VERSION)?.run { - setSummary( - when (result) { - true -> R.string.check_for_updates - false -> R.string.no_update_available - null -> R.string.update_check_failed - } - ) - isSelectable = true - } + private fun onUpdateAvailable(version: AppVersion?) { + if (version == null) { + Snackbar.make(listView, R.string.no_update_available, Snackbar.LENGTH_SHORT).show() + return } + AppUpdateDialog(context ?: return).show(version) } private fun openLink(url: String, title: CharSequence?) { @@ -68,7 +63,7 @@ class AboutSettingsFragment : BasePreferenceFragment(R.string.about) { Intent.createChooser(intent, title) } else { intent - } + }, ) } -} \ No newline at end of file +} diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/about/AboutSettingsViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/settings/about/AboutSettingsViewModel.kt new file mode 100644 index 000000000..5289f751c --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/settings/about/AboutSettingsViewModel.kt @@ -0,0 +1,24 @@ +package org.koitharu.kotatsu.settings.about + +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject +import org.koitharu.kotatsu.base.ui.BaseViewModel +import org.koitharu.kotatsu.core.github.AppUpdateRepository +import org.koitharu.kotatsu.core.github.AppVersion +import org.koitharu.kotatsu.utils.SingleLiveEvent + +@HiltViewModel +class AboutSettingsViewModel @Inject constructor( + private val appUpdateRepository: AppUpdateRepository, +) : BaseViewModel() { + + val isUpdateSupported = appUpdateRepository.isUpdateSupported() + val onUpdateAvailable = SingleLiveEvent() + + fun checkForUpdates() { + launchLoadingJob { + val update = appUpdateRepository.fetchUpdate() + onUpdateAvailable.call(update) + } + } +} diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/about/AppUpdateDialog.kt b/app/src/main/java/org/koitharu/kotatsu/settings/about/AppUpdateDialog.kt new file mode 100644 index 000000000..4752a7461 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/settings/about/AppUpdateDialog.kt @@ -0,0 +1,39 @@ +package org.koitharu.kotatsu.settings.about + +import android.content.Context +import android.content.Intent +import androidx.core.net.toUri +import com.google.android.material.R as materialR +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.core.github.AppVersion +import org.koitharu.kotatsu.utils.FileSize + +class AppUpdateDialog(private val context: Context) { + + fun show(version: AppVersion) { + val message = buildString { + append(context.getString(R.string.new_version_s, version.name)) + appendLine() + append(context.getString(R.string.size_s, FileSize.BYTES.format(context, version.apkSize))) + appendLine() + appendLine() + append(version.description) + } + MaterialAlertDialogBuilder( + context, + materialR.style.ThemeOverlay_Material3_MaterialAlertDialog_Centered, + ) + .setTitle(R.string.app_update_available) + .setMessage(message) + .setIcon(R.drawable.ic_app_update) + .setPositiveButton(R.string.download) { _, _ -> + val intent = Intent(Intent.ACTION_VIEW, version.apkUrl.toUri()) + context.startActivity(Intent.createChooser(intent, context.getString(R.string.open_in_browser))) + } + .setNegativeButton(R.string.close, null) + .setCancelable(false) + .create() + .show() + } +} diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/tools/ToolsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/tools/ToolsFragment.kt index 58b9d6658..8cb75ead8 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/tools/ToolsFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/tools/ToolsFragment.kt @@ -26,6 +26,7 @@ import org.koitharu.kotatsu.core.github.AppVersion import org.koitharu.kotatsu.databinding.FragmentToolsBinding import org.koitharu.kotatsu.download.ui.DownloadsActivity import org.koitharu.kotatsu.settings.SettingsActivity +import org.koitharu.kotatsu.settings.about.AppUpdateDialog import org.koitharu.kotatsu.settings.tools.model.StorageUsage import org.koitharu.kotatsu.utils.FileSize import org.koitharu.kotatsu.utils.ext.getThemeColor @@ -68,6 +69,10 @@ class ToolsFragment : intent.data = url.toUri() startActivity(Intent.createChooser(intent, getString(R.string.open_in_browser))) } + R.id.card_update -> { + val version = viewModel.appUpdate.value ?: return + AppUpdateDialog(v.context).show(version) + } } } diff --git a/app/src/main/res/layout/layout_app_update.xml b/app/src/main/res/layout/layout_app_update.xml index 046ba497a..a11a63c20 100644 --- a/app/src/main/res/layout/layout_app_update.xml +++ b/app/src/main/res/layout/layout_app_update.xml @@ -2,7 +2,6 @@ @@ -61,4 +60,4 @@ - \ No newline at end of file + diff --git a/app/src/main/res/xml/pref_about.xml b/app/src/main/res/xml/pref_about.xml index ab078ac09..a4e72c089 100644 --- a/app/src/main/res/xml/pref_about.xml +++ b/app/src/main/res/xml/pref_about.xml @@ -9,12 +9,6 @@ android:persistent="false" android:summary="@string/check_for_updates" /> - -