Application update checker
This commit is contained in:
@@ -47,6 +47,7 @@
|
||||
<service
|
||||
android:name=".ui.download.DownloadService"
|
||||
android:foregroundServiceType="dataSync" />
|
||||
<service android:name=".ui.settings.UpdateService" />
|
||||
|
||||
<provider
|
||||
android:name=".ui.search.MangaSuggestionsProvider"
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package org.koitharu.kotatsu.core.github
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class AppVersion(
|
||||
val id: Long,
|
||||
val name: String,
|
||||
val url: String,
|
||||
val apkSize: Long,
|
||||
val apkUrl: String
|
||||
) : Parcelable {
|
||||
|
||||
fun isGreaterThen(version: String) {
|
||||
val thisParts = name.substringBeforeLast('-').split('.')
|
||||
val parts = version.substringBeforeLast('-').split('.')
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package org.koitharu.kotatsu.core.github
|
||||
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import org.koin.core.KoinComponent
|
||||
import org.koin.core.inject
|
||||
import org.koitharu.kotatsu.utils.ext.await
|
||||
import org.koitharu.kotatsu.utils.ext.parseJson
|
||||
|
||||
class GithubRepository : KoinComponent {
|
||||
|
||||
private val okHttp by inject<OkHttpClient>()
|
||||
|
||||
suspend fun getLatestVersion(): AppVersion {
|
||||
val request = Request.Builder()
|
||||
.get()
|
||||
.url("https://api.github.com/repos/nv95/Kotatsu/releases/latest")
|
||||
val json = okHttp.newCall(request.build()).await().parseJson()
|
||||
val asset = json.getJSONArray("assets").getJSONObject(0)
|
||||
return AppVersion(
|
||||
id = json.getLong("id"),
|
||||
url = json.getString("html_url"),
|
||||
name = json.getString("name").removePrefix("v"),
|
||||
apkSize = asset.getLong("size"),
|
||||
apkUrl = asset.getString("browser_download_url")
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package org.koitharu.kotatsu.core.github
|
||||
|
||||
import java.util.*
|
||||
|
||||
data class VersionId(
|
||||
val major: Int,
|
||||
val minor: Int,
|
||||
val build: Int,
|
||||
val variantType: String,
|
||||
val variantNumber: Int
|
||||
) : Comparable<VersionId> {
|
||||
|
||||
override fun compareTo(other: VersionId): Int {
|
||||
var diff = major.compareTo(other.major)
|
||||
if (diff != 0) {
|
||||
return diff
|
||||
}
|
||||
diff = minor.compareTo(other.minor)
|
||||
if (diff != 0) {
|
||||
return diff
|
||||
}
|
||||
diff = build.compareTo(other.build)
|
||||
if (diff != 0) {
|
||||
return diff
|
||||
}
|
||||
diff = variantWeight(variantType).compareTo(variantWeight(other.variantType))
|
||||
if (diff != 0) {
|
||||
return diff
|
||||
}
|
||||
return variantNumber.compareTo(other.variantNumber)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@JvmStatic
|
||||
private fun variantWeight(variantType: String) =
|
||||
when (variantType.toLowerCase(Locale.ROOT)) {
|
||||
"a" -> 1
|
||||
"b" -> 2
|
||||
"rc" -> 4
|
||||
else -> 8
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun parse(versionName: String): VersionId {
|
||||
val parts = versionName.substringBeforeLast('-').split('.')
|
||||
val variant = versionName.substringAfterLast('-', "")
|
||||
return VersionId(
|
||||
major = parts.getOrNull(0)?.toIntOrNull() ?: 0,
|
||||
minor = parts.getOrNull(1)?.toIntOrNull() ?: 0,
|
||||
build = parts.getOrNull(2)?.toIntOrNull() ?: 0,
|
||||
variantType = variant.filter(Char::isLetter),
|
||||
variantNumber = variant.filter(Char::isDigit).toIntOrNull() ?: 0
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,8 @@ class DownloadNotification(private val context: Context) {
|
||||
context.applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
|
||||
init {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
|
||||
&& manager.getNotificationChannel(CHANNEL_ID) == null) {
|
||||
val channel = NotificationChannel(
|
||||
CHANNEL_ID,
|
||||
context.getString(R.string.downloads),
|
||||
@@ -67,6 +68,7 @@ class DownloadNotification(private val context: Context) {
|
||||
builder.setSmallIcon(android.R.drawable.stat_notify_error)
|
||||
builder.setSubText(context.getString(R.string.error))
|
||||
builder.setContentText(e.getDisplayMessage(context.resources))
|
||||
builder.setAutoCancel(true)
|
||||
builder.setContentIntent(null)
|
||||
}
|
||||
|
||||
@@ -92,6 +94,7 @@ class DownloadNotification(private val context: Context) {
|
||||
builder.setProgress(0, 0, false)
|
||||
builder.setContentText(context.getString(R.string.download_complete))
|
||||
builder.setContentIntent(createIntent(context, manga))
|
||||
builder.setAutoCancel(true)
|
||||
builder.setSmallIcon(android.R.drawable.stat_sys_download_done)
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ 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
|
||||
@@ -28,6 +29,7 @@ import org.koitharu.kotatsu.ui.main.list.remote.RemoteListFragment
|
||||
import org.koitharu.kotatsu.ui.reader.ReaderActivity
|
||||
import org.koitharu.kotatsu.ui.reader.ReaderState
|
||||
import org.koitharu.kotatsu.ui.settings.SettingsActivity
|
||||
import org.koitharu.kotatsu.ui.settings.UpdateService
|
||||
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
|
||||
import org.koitharu.kotatsu.utils.ext.resolveDp
|
||||
|
||||
@@ -64,6 +66,9 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
|
||||
navigationView.setCheckedItem(R.id.nav_history)
|
||||
setPrimaryFragment(HistoryListFragment.newInstance())
|
||||
}
|
||||
drawer.postDelayed(4000) {
|
||||
UpdateService.startIfRequired(applicationContext)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
package org.koitharu.kotatsu.ui.settings
|
||||
|
||||
import android.os.Bundle
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.ui.common.BasePreferenceFragment
|
||||
|
||||
class AboutSettingsFragment : BasePreferenceFragment(R.string.about_app) {
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
addPreferencesFromResource(R.xml.pref_about)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
package org.koitharu.kotatsu.ui.settings
|
||||
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.BitmapFactory
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.content.edit
|
||||
import androidx.preference.PreferenceManager
|
||||
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.ui.common.BaseService
|
||||
import org.koitharu.kotatsu.utils.FileSizeUtils
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class UpdateService : 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)
|
||||
}
|
||||
}
|
||||
PreferenceManager.getDefaultSharedPreferences(this@UpdateService).edit(true) {
|
||||
putLong(getString(R.string.key_app_update), 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@UpdateService, newVersion.apkSize))
|
||||
append(')')
|
||||
})
|
||||
builder.setContentIntent(
|
||||
PendingIntent.getActivity(
|
||||
this,
|
||||
NOTIFICATION_ID,
|
||||
Intent(Intent.ACTION_VIEW, Uri.parse(newVersion.url)),
|
||||
PendingIntent.FLAG_CANCEL_CURRENT
|
||||
)
|
||||
)
|
||||
builder.setSmallIcon(R.drawable.ic_stat_update)
|
||||
builder.setAutoCancel(true)
|
||||
builder.setLargeIcon(BitmapFactory.decodeResource(resources, R.mipmap.ic_launcher))
|
||||
manager.notify(NOTIFICATION_ID, builder.build())
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val NOTIFICATION_ID = 202
|
||||
private const val CHANNEL_ID = "update"
|
||||
private val PERIOD = TimeUnit.HOURS.toMillis(10)
|
||||
|
||||
fun start(context: Context) =
|
||||
context.startService(Intent(context, UpdateService::class.java))
|
||||
|
||||
fun startIfRequired(context: Context) {
|
||||
val lastUpdate = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
.getLong(context.getString(R.string.key_app_update), 0)
|
||||
if (lastUpdate + PERIOD < System.currentTimeMillis()) {
|
||||
start(context)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,20 +2,32 @@ package org.koitharu.kotatsu.utils.ext
|
||||
|
||||
import okhttp3.Response
|
||||
import okhttp3.internal.closeQuietly
|
||||
import org.json.JSONObject
|
||||
import org.jsoup.Jsoup
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
|
||||
fun Response.parseHtml(): Document {
|
||||
val stream = body?.byteStream() ?: throw NullPointerException("Response body is null")
|
||||
val charset = body!!.contentType()?.charset()?.name()
|
||||
val doc = Jsoup.parse(
|
||||
stream,
|
||||
charset,
|
||||
this.request.url.toString()
|
||||
)
|
||||
closeQuietly()
|
||||
return doc
|
||||
try {
|
||||
val stream = body?.byteStream() ?: throw NullPointerException("Response body is null")
|
||||
val charset = body!!.contentType()?.charset()?.name()
|
||||
return Jsoup.parse(
|
||||
stream,
|
||||
charset,
|
||||
request.url.toString()
|
||||
)
|
||||
} finally {
|
||||
closeQuietly()
|
||||
}
|
||||
}
|
||||
|
||||
fun Response.parseJson(): JSONObject {
|
||||
try {
|
||||
val string = body?.string() ?: throw NullPointerException("Response body is null")
|
||||
return JSONObject(string)
|
||||
} finally {
|
||||
closeQuietly()
|
||||
}
|
||||
}
|
||||
|
||||
fun Element.firstChild(): Element? = children().first()
|
||||
BIN
app/src/main/res/drawable-hdpi/ic_stat_update.png
Normal file
BIN
app/src/main/res/drawable-hdpi/ic_stat_update.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 403 B |
BIN
app/src/main/res/drawable-mdpi/ic_stat_update.png
Normal file
BIN
app/src/main/res/drawable-mdpi/ic_stat_update.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 358 B |
BIN
app/src/main/res/drawable-xhdpi/ic_stat_update.png
Normal file
BIN
app/src/main/res/drawable-xhdpi/ic_stat_update.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 591 B |
BIN
app/src/main/res/drawable-xxhdpi/ic_stat_update.png
Normal file
BIN
app/src/main/res/drawable-xxhdpi/ic_stat_update.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 824 B |
BIN
app/src/main/res/drawable-xxxhdpi/ic_stat_update.png
Normal file
BIN
app/src/main/res/drawable-xxxhdpi/ic_stat_update.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
@@ -1,8 +1,11 @@
|
||||
<!-- drawable/information_outline.xml -->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:height="24dp"
|
||||
android:width="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path android:fillColor="#000" android:pathData="M11,9H13V7H11M12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M11,17H13V11H11V17Z" />
|
||||
<vector
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#000"
|
||||
android:pathData="M11,9H13V7H11M12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M11,17H13V11H11V17Z" />
|
||||
</vector>
|
||||
@@ -104,4 +104,7 @@
|
||||
<string name="external_storage">Внешнее хранилище</string>
|
||||
<string name="domain">Домен</string>
|
||||
<string name="_default">По умолчанию</string>
|
||||
<string name="application_update">Обновление приложения</string>
|
||||
<string name="app_update_available">Доступно обновление приложения</string>
|
||||
<string name="about_app">О программе</string>
|
||||
</resources>
|
||||
@@ -10,6 +10,7 @@
|
||||
<string name="key_reading_history_clear">reading_history_clear</string>
|
||||
<string name="key_grid_size">grid_size</string>
|
||||
<string name="key_reader_switchers">reader_switchers</string>
|
||||
<string name="key_app_update">app_update</string>
|
||||
|
||||
<string name="key_parser_domain">domain</string>
|
||||
<string-array name="values_theme">
|
||||
|
||||
@@ -105,4 +105,7 @@
|
||||
<string name="external_storage">External storage</string>
|
||||
<string name="domain">Domain</string>
|
||||
<string name="_default">Default</string>
|
||||
<string name="application_update">Application update</string>
|
||||
<string name="app_update_available">Application update is available</string>
|
||||
<string name="about_app">About</string>
|
||||
</resources>
|
||||
6
app/src/main/res/xml/pref_about.xml
Normal file
6
app/src/main/res/xml/pref_about.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
|
||||
|
||||
</PreferenceScreen>
|
||||
@@ -22,4 +22,9 @@
|
||||
android:icon="@drawable/ic_history"
|
||||
android:title="@string/history_and_cache" />
|
||||
|
||||
<PreferenceScreen
|
||||
android:fragment="org.koitharu.kotatsu.ui.settings.AboutSettingsFragment"
|
||||
android:icon="@drawable/ic_information"
|
||||
android:title="@string/about_app" />
|
||||
|
||||
</PreferenceScreen>
|
||||
Reference in New Issue
Block a user