Application update checker

This commit is contained in:
Koitharu
2020-03-14 11:39:33 +02:00
parent 3c4e29149f
commit 4e7034cd59
22 changed files with 289 additions and 22 deletions

View File

@@ -61,11 +61,11 @@ dependencies {
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.3'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.3'
implementation 'androidx.core:core-ktx:1.3.0-alpha01'
implementation 'androidx.core:core-ktx:1.3.0-alpha02'
implementation 'androidx.fragment:fragment-ktx:1.2.2'
implementation 'androidx.appcompat:appcompat:1.2.0-alpha02'
implementation 'androidx.appcompat:appcompat:1.2.0-alpha03'
implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta4'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0-alpha03'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0-beta01'
implementation 'androidx.recyclerview:recyclerview:1.2.0-alpha01'
implementation 'androidx.preference:preference:1.1.0'
implementation 'com.google.android.material:material:1.2.0-alpha05'

View File

@@ -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"

View File

@@ -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('.')
}
}

View File

@@ -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")
)
}
}

View File

@@ -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
)
}
}
}

View File

@@ -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)
}

View File

@@ -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() {

View File

@@ -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)
}
}

View File

@@ -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)
}
}
}
}

View File

@@ -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()

Binary file not shown.

After

Width:  |  Height:  |  Size: 403 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 358 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 591 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 824 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -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>

View File

@@ -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>

View File

@@ -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">

View File

@@ -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>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
</PreferenceScreen>

View File

@@ -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>