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