diff --git a/app/build.gradle b/app/build.gradle
index 0181471fd..71d2696b3 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -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'
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index fbb64b0c9..3cbcdd36c 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -47,6 +47,7 @@
+
()
+
+ 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")
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/core/github/VersionId.kt b/app/src/main/java/org/koitharu/kotatsu/core/github/VersionId.kt
new file mode 100644
index 000000000..0ee6affdf
--- /dev/null
+++ b/app/src/main/java/org/koitharu/kotatsu/core/github/VersionId.kt
@@ -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 {
+
+ 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
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/ui/download/DownloadNotification.kt b/app/src/main/java/org/koitharu/kotatsu/ui/download/DownloadNotification.kt
index 398478a4b..c75b7674c 100644
--- a/app/src/main/java/org/koitharu/kotatsu/ui/download/DownloadNotification.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/ui/download/DownloadNotification.kt
@@ -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)
}
diff --git a/app/src/main/java/org/koitharu/kotatsu/ui/main/MainActivity.kt b/app/src/main/java/org/koitharu/kotatsu/ui/main/MainActivity.kt
index 5be3bd96a..302e2ac1f 100644
--- a/app/src/main/java/org/koitharu/kotatsu/ui/main/MainActivity.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/ui/main/MainActivity.kt
@@ -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() {
diff --git a/app/src/main/java/org/koitharu/kotatsu/ui/settings/AboutSettingsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/ui/settings/AboutSettingsFragment.kt
new file mode 100644
index 000000000..aa687a78f
--- /dev/null
+++ b/app/src/main/java/org/koitharu/kotatsu/ui/settings/AboutSettingsFragment.kt
@@ -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)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/ui/settings/UpdateService.kt b/app/src/main/java/org/koitharu/kotatsu/ui/settings/UpdateService.kt
new file mode 100644
index 000000000..9ff734e80
--- /dev/null
+++ b/app/src/main/java/org/koitharu/kotatsu/ui/settings/UpdateService.kt
@@ -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)
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/JsoupExt.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/JsoupExt.kt
index 75e3eada3..cee7c55b2 100644
--- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/JsoupExt.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/utils/ext/JsoupExt.kt
@@ -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()
\ No newline at end of file
diff --git a/app/src/main/res/drawable-hdpi/ic_stat_update.png b/app/src/main/res/drawable-hdpi/ic_stat_update.png
new file mode 100644
index 000000000..0c727d5b0
Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_stat_update.png differ
diff --git a/app/src/main/res/drawable-mdpi/ic_stat_update.png b/app/src/main/res/drawable-mdpi/ic_stat_update.png
new file mode 100644
index 000000000..433e2b882
Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_stat_update.png differ
diff --git a/app/src/main/res/drawable-xhdpi/ic_stat_update.png b/app/src/main/res/drawable-xhdpi/ic_stat_update.png
new file mode 100644
index 000000000..1ebbddc98
Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_stat_update.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_stat_update.png b/app/src/main/res/drawable-xxhdpi/ic_stat_update.png
new file mode 100644
index 000000000..1c712b6ab
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_stat_update.png differ
diff --git a/app/src/main/res/drawable-xxxhdpi/ic_stat_update.png b/app/src/main/res/drawable-xxxhdpi/ic_stat_update.png
new file mode 100644
index 000000000..f341fd426
Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_stat_update.png differ
diff --git a/app/src/main/res/drawable/ic_information.xml b/app/src/main/res/drawable/ic_information.xml
index 8364044a5..f34c14224 100644
--- a/app/src/main/res/drawable/ic_information.xml
+++ b/app/src/main/res/drawable/ic_information.xml
@@ -1,8 +1,11 @@
-
-
-
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml
index 065acbd5e..eb4597986 100644
--- a/app/src/main/res/values-ru/strings.xml
+++ b/app/src/main/res/values-ru/strings.xml
@@ -104,4 +104,7 @@
Внешнее хранилище
Домен
По умолчанию
+ Обновление приложения
+ Доступно обновление приложения
+ О программе
\ No newline at end of file
diff --git a/app/src/main/res/values/constants.xml b/app/src/main/res/values/constants.xml
index 16dd185c8..faba55904 100644
--- a/app/src/main/res/values/constants.xml
+++ b/app/src/main/res/values/constants.xml
@@ -10,6 +10,7 @@
reading_history_clear
grid_size
reader_switchers
+ app_update
domain
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 8313b024c..b96150489 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -105,4 +105,7 @@
External storage
Domain
Default
+ Application update
+ Application update is available
+ About
\ 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
new file mode 100644
index 000000000..5699e19c1
--- /dev/null
+++ b/app/src/main/res/xml/pref_about.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/xml/pref_headers.xml b/app/src/main/res/xml/pref_headers.xml
index a71f6ece5..53e6ccad8 100644
--- a/app/src/main/res/xml/pref_headers.xml
+++ b/app/src/main/res/xml/pref_headers.xml
@@ -22,4 +22,9 @@
android:icon="@drawable/ic_history"
android:title="@string/history_and_cache" />
+
+
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
index 98c3a4a01..aaa2d2e8f 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,12 +1,12 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
- ext.kotlin_version = "1.3.61"
+ ext.kotlin_version = "1.3.70"
repositories {
google()
jcenter()
}
dependencies {
- classpath 'com.android.tools.build:gradle:4.0.0-beta01'
+ classpath 'com.android.tools.build:gradle:4.0.0-beta02'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
// NOTE: Do not place your application dependencies here; they belong