Show app update in dialog
This commit is contained in:
@@ -9,5 +9,6 @@ data class AppVersion(
|
|||||||
val name: String,
|
val name: String,
|
||||||
val url: String,
|
val url: String,
|
||||||
val apkSize: Long,
|
val apkSize: Long,
|
||||||
val apkUrl: String
|
val apkUrl: String,
|
||||||
|
val description: String
|
||||||
) : Parcelable
|
) : Parcelable
|
||||||
@@ -22,7 +22,8 @@ class GithubRepository : KoinComponent {
|
|||||||
url = json.getString("html_url"),
|
url = json.getString("html_url"),
|
||||||
name = json.getString("name").removePrefix("v"),
|
name = json.getString("name").removePrefix("v"),
|
||||||
apkSize = asset.getLong("size"),
|
apkSize = asset.getLong("size"),
|
||||||
apkUrl = asset.getString("browser_download_url")
|
apkUrl = asset.getString("browser_download_url"),
|
||||||
|
description = json.getString("body")
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -11,7 +11,6 @@ import android.view.Menu
|
|||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import androidx.appcompat.app.ActionBarDrawerToggle
|
import androidx.appcompat.app.ActionBarDrawerToggle
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.core.view.postDelayed
|
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.swiperefreshlayout.widget.CircularProgressDrawable
|
import androidx.swiperefreshlayout.widget.CircularProgressDrawable
|
||||||
import com.google.android.material.navigation.NavigationView
|
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.domain.MangaProviderFactory
|
||||||
import org.koitharu.kotatsu.ui.common.BaseActivity
|
import org.koitharu.kotatsu.ui.common.BaseActivity
|
||||||
import org.koitharu.kotatsu.ui.list.favourites.FavouritesContainerFragment
|
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.history.HistoryListFragment
|
||||||
import org.koitharu.kotatsu.ui.list.local.LocalListFragment
|
import org.koitharu.kotatsu.ui.list.local.LocalListFragment
|
||||||
import org.koitharu.kotatsu.ui.list.remote.RemoteListFragment
|
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.ReaderActivity
|
||||||
import org.koitharu.kotatsu.ui.reader.ReaderState
|
import org.koitharu.kotatsu.ui.reader.ReaderState
|
||||||
import org.koitharu.kotatsu.ui.search.SearchHelper
|
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.settings.SettingsActivity
|
||||||
import org.koitharu.kotatsu.ui.tracker.TrackWorker
|
import org.koitharu.kotatsu.ui.tracker.TrackWorker
|
||||||
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
|
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
|
||||||
@@ -70,10 +69,8 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
|||||||
} ?: run {
|
} ?: run {
|
||||||
openDefaultSection()
|
openDefaultSection()
|
||||||
}
|
}
|
||||||
drawer.postDelayed(2000) {
|
|
||||||
AppUpdateService.startIfRequired(applicationContext)
|
|
||||||
}
|
|
||||||
TrackWorker.setup(applicationContext)
|
TrackWorker.setup(applicationContext)
|
||||||
|
AppUpdateChecker(this).invoke()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
|
|||||||
@@ -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<AppSettings>()
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -42,7 +42,7 @@ class MainSettingsFragment : BasePreferenceFragment(R.string.settings),
|
|||||||
findPreference<MultiSelectListPreference>(R.string.key_reader_switchers)?.summaryProvider =
|
findPreference<MultiSelectListPreference>(R.string.key_reader_switchers)?.summaryProvider =
|
||||||
MultiSummaryProvider(R.string.gestures_only)
|
MultiSummaryProvider(R.string.gestures_only)
|
||||||
findPreference<Preference>(R.string.key_app_update_auto)?.run {
|
findPreference<Preference>(R.string.key_app_update_auto)?.run {
|
||||||
isVisible = AppUpdateService.isUpdateSupported(context)
|
isVisible = AppUpdateChecker.isUpdateSupported(context)
|
||||||
}
|
}
|
||||||
findPreference<Preference>(R.string.key_local_storage)?.run {
|
findPreference<Preference>(R.string.key_local_storage)?.run {
|
||||||
summary = settings.getStorageDir(context)?.getStorageName(context)
|
summary = settings.getStorageDir(context)?.getStorageName(context)
|
||||||
|
|||||||
@@ -137,4 +137,6 @@
|
|||||||
<string name="text_feed_holder">Здесь будут отображаться обновления манги, которую Вы читаете</string>
|
<string name="text_feed_holder">Здесь будут отображаться обновления манги, которую Вы читаете</string>
|
||||||
<string name="search_results">Результаты поиска</string>
|
<string name="search_results">Результаты поиска</string>
|
||||||
<string name="related">Похожие</string>
|
<string name="related">Похожие</string>
|
||||||
|
<string name="new_version_s">Новая версия: %s</string>
|
||||||
|
<string name="size_s">Размер: %s</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -138,4 +138,6 @@
|
|||||||
<string name="text_feed_holder">Here you will see the new chapters of the manga you are reading</string>
|
<string name="text_feed_holder">Here you will see the new chapters of the manga you are reading</string>
|
||||||
<string name="search_results">Search results</string>
|
<string name="search_results">Search results</string>
|
||||||
<string name="related">Related</string>
|
<string name="related">Related</string>
|
||||||
|
<string name="new_version_s">New version: %s</string>
|
||||||
|
<string name="size_s">Size: %s</string>
|
||||||
</resources>
|
</resources>
|
||||||
Reference in New Issue
Block a user