From 7f9cfdbf7afa0e45dc31ef6be8f7640cd8fd2b53 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Sat, 20 Jun 2020 12:10:42 +0300 Subject: [PATCH] Show app update in dialog --- .../kotatsu/core/github/AppVersion.kt | 3 +- .../kotatsu/core/github/GithubRepository.kt | 3 +- .../koitharu/kotatsu/ui/list/MainActivity.kt | 9 +- .../kotatsu/ui/settings/AppUpdateChecker.kt | 128 +++++++++++++ .../kotatsu/ui/settings/AppUpdateService.kt | 178 ------------------ .../ui/settings/MainSettingsFragment.kt | 2 +- app/src/main/res/values-ru/strings.xml | 2 + app/src/main/res/values/strings.xml | 2 + 8 files changed, 140 insertions(+), 187 deletions(-) create mode 100644 app/src/main/java/org/koitharu/kotatsu/ui/settings/AppUpdateChecker.kt delete mode 100644 app/src/main/java/org/koitharu/kotatsu/ui/settings/AppUpdateService.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/core/github/AppVersion.kt b/app/src/main/java/org/koitharu/kotatsu/core/github/AppVersion.kt index 52c61c7c1..c5961a543 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/github/AppVersion.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/github/AppVersion.kt @@ -9,5 +9,6 @@ data class AppVersion( val name: String, val url: String, val apkSize: Long, - val apkUrl: String + val apkUrl: String, + val description: String ) : Parcelable \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/core/github/GithubRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/github/GithubRepository.kt index 4cd5c445b..b6a6d000c 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/github/GithubRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/github/GithubRepository.kt @@ -22,7 +22,8 @@ class GithubRepository : KoinComponent { url = json.getString("html_url"), name = json.getString("name").removePrefix("v"), apkSize = asset.getLong("size"), - apkUrl = asset.getString("browser_download_url") + apkUrl = asset.getString("browser_download_url"), + description = json.getString("body") ) } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/ui/list/MainActivity.kt b/app/src/main/java/org/koitharu/kotatsu/ui/list/MainActivity.kt index efed16cc6..399ef4142 100644 --- a/app/src/main/java/org/koitharu/kotatsu/ui/list/MainActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/ui/list/MainActivity.kt @@ -11,7 +11,6 @@ import android.view.Menu import android.view.MenuItem import androidx.appcompat.app.ActionBarDrawerToggle import androidx.core.view.isVisible -import androidx.core.view.postDelayed import androidx.fragment.app.Fragment import androidx.swiperefreshlayout.widget.CircularProgressDrawable import com.google.android.material.navigation.NavigationView @@ -26,14 +25,14 @@ import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.domain.MangaProviderFactory import org.koitharu.kotatsu.ui.common.BaseActivity import org.koitharu.kotatsu.ui.list.favourites.FavouritesContainerFragment +import org.koitharu.kotatsu.ui.list.feed.FeedFragment import org.koitharu.kotatsu.ui.list.history.HistoryListFragment import org.koitharu.kotatsu.ui.list.local.LocalListFragment import org.koitharu.kotatsu.ui.list.remote.RemoteListFragment -import org.koitharu.kotatsu.ui.list.feed.FeedFragment import org.koitharu.kotatsu.ui.reader.ReaderActivity import org.koitharu.kotatsu.ui.reader.ReaderState import org.koitharu.kotatsu.ui.search.SearchHelper -import org.koitharu.kotatsu.ui.settings.AppUpdateService +import org.koitharu.kotatsu.ui.settings.AppUpdateChecker import org.koitharu.kotatsu.ui.settings.SettingsActivity import org.koitharu.kotatsu.ui.tracker.TrackWorker import org.koitharu.kotatsu.utils.ext.getDisplayMessage @@ -70,10 +69,8 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList } ?: run { openDefaultSection() } - drawer.postDelayed(2000) { - AppUpdateService.startIfRequired(applicationContext) - } TrackWorker.setup(applicationContext) + AppUpdateChecker(this).invoke() } override fun onDestroy() { diff --git a/app/src/main/java/org/koitharu/kotatsu/ui/settings/AppUpdateChecker.kt b/app/src/main/java/org/koitharu/kotatsu/ui/settings/AppUpdateChecker.kt new file mode 100644 index 000000000..52fd992c2 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/ui/settings/AppUpdateChecker.kt @@ -0,0 +1,128 @@ +package org.koitharu.kotatsu.ui.settings + +import android.annotation.SuppressLint +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.net.Uri +import androidx.activity.ComponentActivity +import androidx.lifecycle.lifecycleScope +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.koin.core.KoinComponent +import org.koin.core.inject +import org.koitharu.kotatsu.BuildConfig +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.core.github.AppVersion +import org.koitharu.kotatsu.core.github.GithubRepository +import org.koitharu.kotatsu.core.github.VersionId +import org.koitharu.kotatsu.core.prefs.AppSettings +import org.koitharu.kotatsu.utils.FileSizeUtils +import org.koitharu.kotatsu.utils.ext.byte2HexFormatted +import java.io.ByteArrayInputStream +import java.io.InputStream +import java.security.MessageDigest +import java.security.NoSuchAlgorithmException +import java.security.cert.CertificateEncodingException +import java.security.cert.CertificateException +import java.security.cert.CertificateFactory +import java.security.cert.X509Certificate +import java.util.concurrent.TimeUnit + +class AppUpdateChecker(private val activity: ComponentActivity) : KoinComponent { + + private val settings by inject() + + operator fun invoke() { + if (isUpdateSupported(activity) && settings.appUpdateAuto && settings.appUpdate + PERIOD < System.currentTimeMillis()) { + launch() + } + } + + private fun launch() = activity.lifecycleScope.launch { + try { + val repo = GithubRepository() + val version = withContext(Dispatchers.IO) { + repo.getLatestVersion() + } + val newVersionId = VersionId.parse(version.name) + val currentVersionId = VersionId.parse(BuildConfig.VERSION_NAME) + if (newVersionId > currentVersionId) { + showUpdateDialog(version) + } + settings.appUpdate = System.currentTimeMillis() + } catch (_: CancellationException) { + } catch (e: Throwable) { + if (BuildConfig.DEBUG) { + e.printStackTrace() + } + } + } + + private fun showUpdateDialog(version: AppVersion) { + MaterialAlertDialogBuilder(activity) + .setTitle(R.string.app_update_available) + .setMessage(buildString { + append(activity.getString(R.string.new_version_s, version.name)) + appendln() + append(activity.getString(R.string.size_s, FileSizeUtils.formatBytes(activity, version.apkSize))) + appendln() + appendln() + append(version.description) + }) + .setPositiveButton(R.string.download) { _, _ -> + activity.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(version.apkUrl))) + } + .setNegativeButton(R.string.close, null) + .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 getCertificateSHA1Fingerprint(context) == CERT_SHA1 + } + + @Suppress("DEPRECATION") + @SuppressLint("PackageManagerGetSignatures") + private fun getCertificateSHA1Fingerprint(context: Context): String? { + val packageInfo = try { + context.packageManager.getPackageInfo( + context.packageName, + PackageManager.GET_SIGNATURES + ) + } catch (e: PackageManager.NameNotFoundException) { + e.printStackTrace() + return null + } + val signatures = packageInfo?.signatures + val cert: ByteArray = signatures?.firstOrNull()?.toByteArray() ?: return null + val input: InputStream = ByteArrayInputStream(cert) + val c = try { + val cf = CertificateFactory.getInstance("X509") + cf.generateCertificate(input) as X509Certificate + } catch (e: CertificateException) { + e.printStackTrace() + return null + } + return try { + val md: MessageDigest = MessageDigest.getInstance("SHA1") + val publicKey: ByteArray = md.digest(c.encoded) + publicKey.byte2HexFormatted() + } catch (e: NoSuchAlgorithmException) { + e.printStackTrace() + null + } catch (e: CertificateEncodingException) { + e.printStackTrace() + null + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/ui/settings/AppUpdateService.kt b/app/src/main/java/org/koitharu/kotatsu/ui/settings/AppUpdateService.kt deleted file mode 100644 index 94d0d97fb..000000000 --- a/app/src/main/java/org/koitharu/kotatsu/ui/settings/AppUpdateService.kt +++ /dev/null @@ -1,178 +0,0 @@ -package org.koitharu.kotatsu.ui.settings - -import android.annotation.SuppressLint -import android.app.NotificationChannel -import android.app.NotificationManager -import android.app.PendingIntent -import android.content.Context -import android.content.Intent -import android.content.pm.PackageManager -import android.graphics.BitmapFactory -import android.net.Uri -import android.os.Build -import androidx.core.app.NotificationCompat -import androidx.core.content.ContextCompat -import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import org.koitharu.kotatsu.BuildConfig -import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.core.github.AppVersion -import org.koitharu.kotatsu.core.github.GithubRepository -import org.koitharu.kotatsu.core.github.VersionId -import org.koitharu.kotatsu.core.prefs.AppSettings -import org.koitharu.kotatsu.ui.common.BaseService -import org.koitharu.kotatsu.utils.FileSizeUtils -import org.koitharu.kotatsu.utils.ext.byte2HexFormatted -import java.io.ByteArrayInputStream -import java.io.InputStream -import java.security.MessageDigest -import java.security.NoSuchAlgorithmException -import java.security.cert.CertificateEncodingException -import java.security.cert.CertificateException -import java.security.cert.CertificateFactory -import java.security.cert.X509Certificate -import java.util.concurrent.TimeUnit - - -class AppUpdateService : BaseService() { - - override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - launch(Dispatchers.IO) { - try { - val repo = GithubRepository() - val version = repo.getLatestVersion() - val newVersionId = VersionId.parse(version.name) - val currentVersionId = VersionId.parse(BuildConfig.VERSION_NAME) - if (newVersionId > currentVersionId) { - withContext(Dispatchers.Main) { - showUpdateNotification(version) - } - } - AppSettings(this@AppUpdateService).appUpdate = System.currentTimeMillis() - } catch (_: CancellationException) { - } catch (e: Throwable) { - if (BuildConfig.DEBUG) { - e.printStackTrace() - } - } finally { - withContext(Dispatchers.Main) { - stopSelf(startId) - } - } - } - return START_NOT_STICKY - } - - private fun showUpdateNotification(newVersion: AppVersion) { - val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O - && manager.getNotificationChannel(CHANNEL_ID) == null - ) { - val channel = NotificationChannel( - CHANNEL_ID, - getString(R.string.application_update), - NotificationManager.IMPORTANCE_DEFAULT - ) - manager.createNotificationChannel(channel) - } - val builder = NotificationCompat.Builder(this, CHANNEL_ID) - builder.setContentTitle(getString(R.string.app_update_available)) - builder.setContentText(buildString { - append(newVersion.name) - append(" (") - append(FileSizeUtils.formatBytes(this@AppUpdateService, newVersion.apkSize)) - append(')') - }) - builder.setContentIntent( - PendingIntent.getActivity( - this, - NOTIFICATION_ID, - Intent(Intent.ACTION_VIEW, Uri.parse(newVersion.url)), - PendingIntent.FLAG_CANCEL_CURRENT - ) - ) - builder.addAction( - R.drawable.ic_download, getString(R.string.download), - PendingIntent.getActivity( - this, - NOTIFICATION_ID + 1, - Intent(Intent.ACTION_VIEW, Uri.parse(newVersion.apkUrl)), - PendingIntent.FLAG_CANCEL_CURRENT - ) - ) - builder.setSmallIcon(R.drawable.ic_stat_update) - builder.setAutoCancel(true) - builder.color = ContextCompat.getColor(this, R.color.blue_primary_dark) - builder.setLargeIcon(BitmapFactory.decodeResource(resources, R.mipmap.ic_launcher)) - manager.notify(NOTIFICATION_ID, builder.build()) - } - - 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 const val NOTIFICATION_ID = 202 - private const val CHANNEL_ID = "update" - private val PERIOD = TimeUnit.HOURS.toMillis(6) - - fun isUpdateSupported(context: Context): Boolean { - return getCertificateSHA1Fingerprint(context) == CERT_SHA1 - } - - fun startIfRequired(context: Context) { - if (!isUpdateSupported(context)) { - return - } - val settings = AppSettings(context) - if (settings.appUpdateAuto) { - val lastUpdate = settings.appUpdate - if (lastUpdate + PERIOD < System.currentTimeMillis()) { - start(context) - } - } - } - - private fun start(context: Context) { - try { - context.startService(Intent(context, AppUpdateService::class.java)) - } catch (_: IllegalStateException) { - } - } - - @Suppress("DEPRECATION") - @SuppressLint("PackageManagerGetSignatures") - private fun getCertificateSHA1Fingerprint(context: Context): String? { - val packageInfo = try { - context.packageManager.getPackageInfo( - context.packageName, - PackageManager.GET_SIGNATURES - ) - } catch (e: PackageManager.NameNotFoundException) { - e.printStackTrace() - return null - } - val signatures = packageInfo?.signatures - val cert: ByteArray = signatures?.firstOrNull()?.toByteArray() ?: return null - val input: InputStream = ByteArrayInputStream(cert) - val c = try { - val cf = CertificateFactory.getInstance("X509") - cf.generateCertificate(input) as X509Certificate - } catch (e: CertificateException) { - e.printStackTrace() - return null - } - return try { - val md: MessageDigest = MessageDigest.getInstance("SHA1") - val publicKey: ByteArray = md.digest(c.encoded) - publicKey.byte2HexFormatted() - } catch (e: NoSuchAlgorithmException) { - e.printStackTrace() - null - } catch (e: CertificateEncodingException) { - e.printStackTrace() - null - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/ui/settings/MainSettingsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/ui/settings/MainSettingsFragment.kt index a7cd0b7b2..a22f15647 100644 --- a/app/src/main/java/org/koitharu/kotatsu/ui/settings/MainSettingsFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/ui/settings/MainSettingsFragment.kt @@ -42,7 +42,7 @@ class MainSettingsFragment : BasePreferenceFragment(R.string.settings), findPreference(R.string.key_reader_switchers)?.summaryProvider = MultiSummaryProvider(R.string.gestures_only) findPreference(R.string.key_app_update_auto)?.run { - isVisible = AppUpdateService.isUpdateSupported(context) + isVisible = AppUpdateChecker.isUpdateSupported(context) } findPreference(R.string.key_local_storage)?.run { summary = settings.getStorageDir(context)?.getStorageName(context) diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index c6a32d4a7..aa40ca4e2 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -137,4 +137,6 @@ Здесь будут отображаться обновления манги, которую Вы читаете Результаты поиска Похожие + Новая версия: %s + Размер: %s \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 83e6bd10c..dfb998cf5 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -138,4 +138,6 @@ Here you will see the new chapters of the manga you are reading Search results Related + New version: %s + Size: %s \ No newline at end of file