From e64cd79c28122596c9989563af12c59916eb27e5 Mon Sep 17 00:00:00 2001 From: Philipp Heckel Date: Tue, 6 Dec 2022 16:17:05 -0500 Subject: [PATCH] Fix Android 5 crashes on unsubscribing --- app/build.gradle | 4 +-- .../heckel/ntfy/firebase/FirebaseMessenger.kt | 1 + .../java/io/heckel/ntfy/backup/Backuper.kt | 6 ++-- .../heckel/ntfy/service/SubscriberService.kt | 10 +++---- .../java/io/heckel/ntfy/ui/DetailActivity.kt | 13 ++++---- .../java/io/heckel/ntfy/ui/MainActivity.kt | 4 +-- .../java/io/heckel/ntfy/ui/MainAdapter.kt | 2 +- .../java/io/heckel/ntfy/ui/ShareActivity.kt | 5 ++-- .../java/io/heckel/ntfy/ui/UserFragment.kt | 30 +++++-------------- app/src/main/java/io/heckel/ntfy/util/Util.kt | 25 +++++++--------- .../metadata/android/en-US/changelog/32.txt | 2 ++ 11 files changed, 43 insertions(+), 59 deletions(-) create mode 100644 fastlane/metadata/android/en-US/changelog/32.txt diff --git a/app/build.gradle b/app/build.gradle index 2f42134..b79295a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -14,8 +14,8 @@ android { minSdkVersion 21 targetSdkVersion 33 - versionCode 31 - versionName "1.15.2" + versionCode 32 + versionName "1.16.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" diff --git a/app/src/fdroid/java/io/heckel/ntfy/firebase/FirebaseMessenger.kt b/app/src/fdroid/java/io/heckel/ntfy/firebase/FirebaseMessenger.kt index e121d2f..d12f90e 100644 --- a/app/src/fdroid/java/io/heckel/ntfy/firebase/FirebaseMessenger.kt +++ b/app/src/fdroid/java/io/heckel/ntfy/firebase/FirebaseMessenger.kt @@ -1,5 +1,6 @@ package io.heckel.ntfy.firebase +@Suppress("UNUSED_PARAMETER") class FirebaseMessenger { fun subscribe(topic: String) { // Dummy to keep F-Droid flavor happy diff --git a/app/src/main/java/io/heckel/ntfy/backup/Backuper.kt b/app/src/main/java/io/heckel/ntfy/backup/Backuper.kt index 8c87f90..5dbbab5 100644 --- a/app/src/main/java/io/heckel/ntfy/backup/Backuper.kt +++ b/app/src/main/java/io/heckel/ntfy/backup/Backuper.kt @@ -89,7 +89,7 @@ class Backuper(val context: Context) { private suspend fun applySubscriptions(subscriptions: List?) { if (subscriptions == null) { - return; + return } val appBaseUrl = context.getString(R.string.app_base_url) subscriptions.forEach { s -> @@ -119,7 +119,7 @@ class Backuper(val context: Context) { private suspend fun applyNotifications(notifications: List?) { if (notifications == null) { - return; + return } notifications.forEach { n -> try { @@ -188,7 +188,7 @@ class Backuper(val context: Context) { private suspend fun applyUsers(users: List?) { if (users == null) { - return; + return } users.forEach { u -> try { diff --git a/app/src/main/java/io/heckel/ntfy/service/SubscriberService.kt b/app/src/main/java/io/heckel/ntfy/service/SubscriberService.kt index 103ff1c..8577973 100644 --- a/app/src/main/java/io/heckel/ntfy/service/SubscriberService.kt +++ b/app/src/main/java/io/heckel/ntfy/service/SubscriberService.kt @@ -310,11 +310,11 @@ class SubscriberService : Service() { override fun onTaskRemoved(rootIntent: Intent) { val restartServiceIntent = Intent(applicationContext, SubscriberService::class.java).also { it.setPackage(packageName) - }; - val restartServicePendingIntent: PendingIntent = PendingIntent.getService(this, 1, restartServiceIntent, PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE); - applicationContext.getSystemService(Context.ALARM_SERVICE); - val alarmService: AlarmManager = applicationContext.getSystemService(Context.ALARM_SERVICE) as AlarmManager; - alarmService.set(AlarmManager.ELAPSED_REALTIME, SystemClock.elapsedRealtime() + 1000, restartServicePendingIntent); + } + val restartServicePendingIntent: PendingIntent = PendingIntent.getService(this, 1, restartServiceIntent, PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE) + applicationContext.getSystemService(Context.ALARM_SERVICE) + val alarmService: AlarmManager = applicationContext.getSystemService(Context.ALARM_SERVICE) as AlarmManager + alarmService.set(AlarmManager.ELAPSED_REALTIME, SystemClock.elapsedRealtime() + 1000, restartServicePendingIntent) } /* This re-starts the service on reboot; see manifest */ diff --git a/app/src/main/java/io/heckel/ntfy/ui/DetailActivity.kt b/app/src/main/java/io/heckel/ntfy/ui/DetailActivity.kt index 4fc9ec7..08ba821 100644 --- a/app/src/main/java/io/heckel/ntfy/ui/DetailActivity.kt +++ b/app/src/main/java/io/heckel/ntfy/ui/DetailActivity.kt @@ -7,6 +7,7 @@ import android.content.Context import android.content.Intent import android.content.Intent.ACTION_VIEW import android.net.Uri +import android.os.Build import android.os.Bundle import android.text.Html import android.view.ActionMode @@ -178,7 +179,7 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra howToExample.linksClickable = true val howToText = getString(R.string.detail_how_to_example, topicUrl) - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { howToExample.text = Html.fromHtml(howToText, Html.FROM_HTML_MODE_LEGACY) } else { howToExample.text = Html.fromHtml(howToText) @@ -241,7 +242,7 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra adapter.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() { override fun onItemRangeInserted(positionStart: Int, itemCount: Int) { if (positionStart == 0) { - Log.d(TAG, "$itemCount item(s) inserted at $positionStart, scrolling to the top") + Log.d(TAG, "$itemCount item(s) inserted at 0, scrolling to the top") mainList.scrollToPosition(positionStart) } } @@ -571,7 +572,7 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra dialog.setOnShowListener { dialog .getButton(AlertDialog.BUTTON_POSITIVE) - .setTextAppearance(R.style.DangerText) + .dangerButton(this) } dialog.show() } @@ -609,7 +610,7 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra dialog.setOnShowListener { dialog .getButton(AlertDialog.BUTTON_POSITIVE) - .setTextAppearance(R.style.DangerText) + .dangerButton(this) } dialog.show() } @@ -619,7 +620,7 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra handleActionModeClick(notification) } else if (notification.click != "") { try { - startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(notification.click))) + startActivity(Intent(ACTION_VIEW, Uri.parse(notification.click))) } catch (e: Exception) { Log.w(TAG, "Cannot open click URL", e) runOnUiThread { @@ -720,7 +721,7 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra dialog.setOnShowListener { dialog .getButton(AlertDialog.BUTTON_POSITIVE) - .setTextAppearance(R.style.DangerText) + .dangerButton(this) } dialog.show() } diff --git a/app/src/main/java/io/heckel/ntfy/ui/MainActivity.kt b/app/src/main/java/io/heckel/ntfy/ui/MainActivity.kt index ad928a4..2137e8c 100644 --- a/app/src/main/java/io/heckel/ntfy/ui/MainActivity.kt +++ b/app/src/main/java/io/heckel/ntfy/ui/MainActivity.kt @@ -35,7 +35,6 @@ import io.heckel.ntfy.app.Application import io.heckel.ntfy.db.Repository import io.heckel.ntfy.db.Subscription import io.heckel.ntfy.firebase.FirebaseMessenger -import io.heckel.ntfy.util.Log import io.heckel.ntfy.msg.ApiService import io.heckel.ntfy.msg.DownloadManager import io.heckel.ntfy.msg.DownloadType @@ -48,7 +47,6 @@ import io.heckel.ntfy.work.PollWorker import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch -import java.security.SecureRandom import java.util.* import java.util.concurrent.TimeUnit import kotlin.random.Random @@ -622,7 +620,7 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc dialog.setOnShowListener { dialog .getButton(AlertDialog.BUTTON_POSITIVE) - .setTextAppearance(R.style.DangerText) + .dangerButton(this) } dialog.show() } diff --git a/app/src/main/java/io/heckel/ntfy/ui/MainAdapter.kt b/app/src/main/java/io/heckel/ntfy/ui/MainAdapter.kt index bb78468..4d95d0e 100644 --- a/app/src/main/java/io/heckel/ntfy/ui/MainAdapter.kt +++ b/app/src/main/java/io/heckel/ntfy/ui/MainAdapter.kt @@ -119,7 +119,7 @@ class MainAdapter(private val repository: Repository, private val onClick: (Subs if (selected.contains(subscription.id)) { itemView.setBackgroundResource(Colors.itemSelectedBackground(context)) } else { - itemView.setBackgroundColor(Color.TRANSPARENT); + itemView.setBackgroundColor(Color.TRANSPARENT) } } } diff --git a/app/src/main/java/io/heckel/ntfy/ui/ShareActivity.kt b/app/src/main/java/io/heckel/ntfy/ui/ShareActivity.kt index 1db1c33..984f6af 100644 --- a/app/src/main/java/io/heckel/ntfy/ui/ShareActivity.kt +++ b/app/src/main/java/io/heckel/ntfy/ui/ShareActivity.kt @@ -1,7 +1,6 @@ package io.heckel.ntfy.ui import android.content.Intent -import android.graphics.BitmapFactory import android.net.Uri import android.os.Bundle import android.os.Parcelable @@ -295,7 +294,7 @@ class ShareActivity : AppCompatActivity() { .show() } } catch (e: Exception) { - val message = if (e is ApiService.UnauthorizedException) { + val errorMessage = if (e is ApiService.UnauthorizedException) { if (e.user != null) { getString(R.string.detail_test_message_error_unauthorized_user, e.user.username) } else { @@ -308,7 +307,7 @@ class ShareActivity : AppCompatActivity() { } runOnUiThread { progress.visibility = View.GONE - errorText.text = message + errorText.text = errorMessage errorImage.visibility = View.VISIBLE errorText.visibility = View.VISIBLE } diff --git a/app/src/main/java/io/heckel/ntfy/ui/UserFragment.kt b/app/src/main/java/io/heckel/ntfy/ui/UserFragment.kt index 7dd8bb9..d4a7097 100644 --- a/app/src/main/java/io/heckel/ntfy/ui/UserFragment.kt +++ b/app/src/main/java/io/heckel/ntfy/ui/UserFragment.kt @@ -5,8 +5,6 @@ import android.app.Dialog import android.content.Context import android.os.Build import android.os.Bundle -import android.text.Editable -import android.text.TextWatcher import android.view.View import android.view.WindowManager import android.widget.Button @@ -16,6 +14,8 @@ import androidx.fragment.app.DialogFragment import com.google.android.material.textfield.TextInputEditText import io.heckel.ntfy.R import io.heckel.ntfy.db.User +import io.heckel.ntfy.util.AfterChangedTextWatcher +import io.heckel.ntfy.util.dangerButton import io.heckel.ntfy.util.validUrl class UserFragment : DialogFragment() { @@ -98,28 +98,14 @@ class UserFragment : DialogFragment() { // Delete button should be red if (user != null) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - dialog - .getButton(AlertDialog.BUTTON_NEUTRAL) - .setTextAppearance(R.style.DangerText) - } else { - dialog - .getButton(AlertDialog.BUTTON_NEUTRAL) - .setTextColor(ContextCompat.getColor(requireContext(), Colors.dangerText(requireContext()))) - } + dialog + .getButton(AlertDialog.BUTTON_NEUTRAL) + .dangerButton(requireContext()) } // Validate input when typing - val textWatcher = object : TextWatcher { - override fun afterTextChanged(s: Editable?) { - validateInput() - } - override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { - // Nothing - } - override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) { - // Nothing - } + val textWatcher = AfterChangedTextWatcher { + validateInput() } baseUrlView.addTextChangedListener(textWatcher) usernameView.addTextChangedListener(textWatcher) @@ -140,7 +126,7 @@ class UserFragment : DialogFragment() { } // Show keyboard when the dialog is shown (see https://stackoverflow.com/a/19573049/1440785) - dialog.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE); + dialog.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE) return dialog } diff --git a/app/src/main/java/io/heckel/ntfy/util/Util.kt b/app/src/main/java/io/heckel/ntfy/util/Util.kt index 078bf27..86c9728 100644 --- a/app/src/main/java/io/heckel/ntfy/util/Util.kt +++ b/app/src/main/java/io/heckel/ntfy/util/Util.kt @@ -22,13 +22,16 @@ import android.util.Base64 import android.util.TypedValue import android.view.View import android.view.Window +import android.widget.Button import android.widget.ImageView import android.widget.Toast import androidx.appcompat.app.AppCompatDelegate +import androidx.core.content.ContextCompat import io.heckel.ntfy.BuildConfig import io.heckel.ntfy.R import io.heckel.ntfy.db.* import io.heckel.ntfy.msg.MESSAGE_ENCODING_BASE64 +import io.heckel.ntfy.ui.Colors import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay @@ -218,16 +221,6 @@ fun maybeAppendActionErrors(message: String, notification: Notification): String } } -// Checks in the most horrible way if a content URI exists; I couldn't find a better way -fun fileExists(context: Context, contentUri: String?): Boolean { - return try { - fileStat(context, Uri.parse(contentUri)) // Throws if the file does not exist - true - } catch (_: Exception) { - false - } -} - // Queries the filename of a content URI fun fileName(context: Context, contentUri: String?, fallbackName: String): String { return try { @@ -455,10 +448,6 @@ fun String.readBitmapFromUriOrNull(context: Context): Bitmap? { } } -fun Long.nullIfZero(): Long? { - return if (this == 0L) return null else this -} - // TextWatcher that only implements the afterTextChanged method class AfterChangedTextWatcher(val afterTextChangedFn: (s: Editable?) -> Unit) : TextWatcher { override fun afterTextChanged(s: Editable?) { @@ -506,3 +495,11 @@ fun String.sha256(): String { val digest = md.digest(this.toByteArray()) return digest.fold("") { str, it -> str + "%02x".format(it) } } + +fun Button.dangerButton(context: Context) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + setTextAppearance(R.style.DangerText) + } else { + setTextColor(ContextCompat.getColor(context, Colors.dangerText(context))) + } +} diff --git a/fastlane/metadata/android/en-US/changelog/32.txt b/fastlane/metadata/android/en-US/changelog/32.txt new file mode 100644 index 0000000..27617b8 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelog/32.txt @@ -0,0 +1,2 @@ +Bug fixes: +* Android 5 (SDK 21): Fix crash on unsubscribing (#528, thanks to Roger M.)