Fix Android 5 crashes on unsubscribing

This commit is contained in:
Philipp Heckel 2022-12-06 16:17:05 -05:00
parent 6a8d222e12
commit e64cd79c28
11 changed files with 43 additions and 59 deletions

View file

@ -14,8 +14,8 @@ android {
minSdkVersion 21 minSdkVersion 21
targetSdkVersion 33 targetSdkVersion 33
versionCode 31 versionCode 32
versionName "1.15.2" versionName "1.16.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"

View file

@ -1,5 +1,6 @@
package io.heckel.ntfy.firebase package io.heckel.ntfy.firebase
@Suppress("UNUSED_PARAMETER")
class FirebaseMessenger { class FirebaseMessenger {
fun subscribe(topic: String) { fun subscribe(topic: String) {
// Dummy to keep F-Droid flavor happy // Dummy to keep F-Droid flavor happy

View file

@ -89,7 +89,7 @@ class Backuper(val context: Context) {
private suspend fun applySubscriptions(subscriptions: List<Subscription>?) { private suspend fun applySubscriptions(subscriptions: List<Subscription>?) {
if (subscriptions == null) { if (subscriptions == null) {
return; return
} }
val appBaseUrl = context.getString(R.string.app_base_url) val appBaseUrl = context.getString(R.string.app_base_url)
subscriptions.forEach { s -> subscriptions.forEach { s ->
@ -119,7 +119,7 @@ class Backuper(val context: Context) {
private suspend fun applyNotifications(notifications: List<Notification>?) { private suspend fun applyNotifications(notifications: List<Notification>?) {
if (notifications == null) { if (notifications == null) {
return; return
} }
notifications.forEach { n -> notifications.forEach { n ->
try { try {
@ -188,7 +188,7 @@ class Backuper(val context: Context) {
private suspend fun applyUsers(users: List<User>?) { private suspend fun applyUsers(users: List<User>?) {
if (users == null) { if (users == null) {
return; return
} }
users.forEach { u -> users.forEach { u ->
try { try {

View file

@ -310,11 +310,11 @@ class SubscriberService : Service() {
override fun onTaskRemoved(rootIntent: Intent) { override fun onTaskRemoved(rootIntent: Intent) {
val restartServiceIntent = Intent(applicationContext, SubscriberService::class.java).also { val restartServiceIntent = Intent(applicationContext, SubscriberService::class.java).also {
it.setPackage(packageName) it.setPackage(packageName)
}; }
val restartServicePendingIntent: PendingIntent = PendingIntent.getService(this, 1, restartServiceIntent, PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE); val restartServicePendingIntent: PendingIntent = PendingIntent.getService(this, 1, restartServiceIntent, PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE)
applicationContext.getSystemService(Context.ALARM_SERVICE); applicationContext.getSystemService(Context.ALARM_SERVICE)
val alarmService: AlarmManager = applicationContext.getSystemService(Context.ALARM_SERVICE) as AlarmManager; val alarmService: AlarmManager = applicationContext.getSystemService(Context.ALARM_SERVICE) as AlarmManager
alarmService.set(AlarmManager.ELAPSED_REALTIME, SystemClock.elapsedRealtime() + 1000, restartServicePendingIntent); alarmService.set(AlarmManager.ELAPSED_REALTIME, SystemClock.elapsedRealtime() + 1000, restartServicePendingIntent)
} }
/* This re-starts the service on reboot; see manifest */ /* This re-starts the service on reboot; see manifest */

View file

@ -7,6 +7,7 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.Intent.ACTION_VIEW import android.content.Intent.ACTION_VIEW
import android.net.Uri import android.net.Uri
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.text.Html import android.text.Html
import android.view.ActionMode import android.view.ActionMode
@ -178,7 +179,7 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
howToExample.linksClickable = true howToExample.linksClickable = true
val howToText = getString(R.string.detail_how_to_example, topicUrl) 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) howToExample.text = Html.fromHtml(howToText, Html.FROM_HTML_MODE_LEGACY)
} else { } else {
howToExample.text = Html.fromHtml(howToText) howToExample.text = Html.fromHtml(howToText)
@ -241,7 +242,7 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
adapter.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() { adapter.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() {
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) { override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
if (positionStart == 0) { 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) mainList.scrollToPosition(positionStart)
} }
} }
@ -571,7 +572,7 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
dialog.setOnShowListener { dialog.setOnShowListener {
dialog dialog
.getButton(AlertDialog.BUTTON_POSITIVE) .getButton(AlertDialog.BUTTON_POSITIVE)
.setTextAppearance(R.style.DangerText) .dangerButton(this)
} }
dialog.show() dialog.show()
} }
@ -609,7 +610,7 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
dialog.setOnShowListener { dialog.setOnShowListener {
dialog dialog
.getButton(AlertDialog.BUTTON_POSITIVE) .getButton(AlertDialog.BUTTON_POSITIVE)
.setTextAppearance(R.style.DangerText) .dangerButton(this)
} }
dialog.show() dialog.show()
} }
@ -619,7 +620,7 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
handleActionModeClick(notification) handleActionModeClick(notification)
} else if (notification.click != "") { } else if (notification.click != "") {
try { try {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(notification.click))) startActivity(Intent(ACTION_VIEW, Uri.parse(notification.click)))
} catch (e: Exception) { } catch (e: Exception) {
Log.w(TAG, "Cannot open click URL", e) Log.w(TAG, "Cannot open click URL", e)
runOnUiThread { runOnUiThread {
@ -720,7 +721,7 @@ class DetailActivity : AppCompatActivity(), ActionMode.Callback, NotificationFra
dialog.setOnShowListener { dialog.setOnShowListener {
dialog dialog
.getButton(AlertDialog.BUTTON_POSITIVE) .getButton(AlertDialog.BUTTON_POSITIVE)
.setTextAppearance(R.style.DangerText) .dangerButton(this)
} }
dialog.show() dialog.show()
} }

View file

@ -35,7 +35,6 @@ import io.heckel.ntfy.app.Application
import io.heckel.ntfy.db.Repository import io.heckel.ntfy.db.Repository
import io.heckel.ntfy.db.Subscription import io.heckel.ntfy.db.Subscription
import io.heckel.ntfy.firebase.FirebaseMessenger import io.heckel.ntfy.firebase.FirebaseMessenger
import io.heckel.ntfy.util.Log
import io.heckel.ntfy.msg.ApiService import io.heckel.ntfy.msg.ApiService
import io.heckel.ntfy.msg.DownloadManager import io.heckel.ntfy.msg.DownloadManager
import io.heckel.ntfy.msg.DownloadType import io.heckel.ntfy.msg.DownloadType
@ -48,7 +47,6 @@ import io.heckel.ntfy.work.PollWorker
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.security.SecureRandom
import java.util.* import java.util.*
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import kotlin.random.Random import kotlin.random.Random
@ -622,7 +620,7 @@ class MainActivity : AppCompatActivity(), ActionMode.Callback, AddFragment.Subsc
dialog.setOnShowListener { dialog.setOnShowListener {
dialog dialog
.getButton(AlertDialog.BUTTON_POSITIVE) .getButton(AlertDialog.BUTTON_POSITIVE)
.setTextAppearance(R.style.DangerText) .dangerButton(this)
} }
dialog.show() dialog.show()
} }

View file

@ -119,7 +119,7 @@ class MainAdapter(private val repository: Repository, private val onClick: (Subs
if (selected.contains(subscription.id)) { if (selected.contains(subscription.id)) {
itemView.setBackgroundResource(Colors.itemSelectedBackground(context)) itemView.setBackgroundResource(Colors.itemSelectedBackground(context))
} else { } else {
itemView.setBackgroundColor(Color.TRANSPARENT); itemView.setBackgroundColor(Color.TRANSPARENT)
} }
} }
} }

View file

@ -1,7 +1,6 @@
package io.heckel.ntfy.ui package io.heckel.ntfy.ui
import android.content.Intent import android.content.Intent
import android.graphics.BitmapFactory
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.os.Parcelable import android.os.Parcelable
@ -295,7 +294,7 @@ class ShareActivity : AppCompatActivity() {
.show() .show()
} }
} catch (e: Exception) { } catch (e: Exception) {
val message = if (e is ApiService.UnauthorizedException) { val errorMessage = if (e is ApiService.UnauthorizedException) {
if (e.user != null) { if (e.user != null) {
getString(R.string.detail_test_message_error_unauthorized_user, e.user.username) getString(R.string.detail_test_message_error_unauthorized_user, e.user.username)
} else { } else {
@ -308,7 +307,7 @@ class ShareActivity : AppCompatActivity() {
} }
runOnUiThread { runOnUiThread {
progress.visibility = View.GONE progress.visibility = View.GONE
errorText.text = message errorText.text = errorMessage
errorImage.visibility = View.VISIBLE errorImage.visibility = View.VISIBLE
errorText.visibility = View.VISIBLE errorText.visibility = View.VISIBLE
} }

View file

@ -5,8 +5,6 @@ import android.app.Dialog
import android.content.Context import android.content.Context
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.view.View import android.view.View
import android.view.WindowManager import android.view.WindowManager
import android.widget.Button import android.widget.Button
@ -16,6 +14,8 @@ import androidx.fragment.app.DialogFragment
import com.google.android.material.textfield.TextInputEditText import com.google.android.material.textfield.TextInputEditText
import io.heckel.ntfy.R import io.heckel.ntfy.R
import io.heckel.ntfy.db.User import io.heckel.ntfy.db.User
import io.heckel.ntfy.util.AfterChangedTextWatcher
import io.heckel.ntfy.util.dangerButton
import io.heckel.ntfy.util.validUrl import io.heckel.ntfy.util.validUrl
class UserFragment : DialogFragment() { class UserFragment : DialogFragment() {
@ -98,28 +98,14 @@ class UserFragment : DialogFragment() {
// Delete button should be red // Delete button should be red
if (user != null) { if (user != null) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { dialog
dialog .getButton(AlertDialog.BUTTON_NEUTRAL)
.getButton(AlertDialog.BUTTON_NEUTRAL) .dangerButton(requireContext())
.setTextAppearance(R.style.DangerText)
} else {
dialog
.getButton(AlertDialog.BUTTON_NEUTRAL)
.setTextColor(ContextCompat.getColor(requireContext(), Colors.dangerText(requireContext())))
}
} }
// Validate input when typing // Validate input when typing
val textWatcher = object : TextWatcher { val textWatcher = AfterChangedTextWatcher {
override fun afterTextChanged(s: Editable?) { validateInput()
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
}
} }
baseUrlView.addTextChangedListener(textWatcher) baseUrlView.addTextChangedListener(textWatcher)
usernameView.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) // 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 return dialog
} }

View file

@ -22,13 +22,16 @@ import android.util.Base64
import android.util.TypedValue import android.util.TypedValue
import android.view.View import android.view.View
import android.view.Window import android.view.Window
import android.widget.Button
import android.widget.ImageView import android.widget.ImageView
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import androidx.core.content.ContextCompat
import io.heckel.ntfy.BuildConfig import io.heckel.ntfy.BuildConfig
import io.heckel.ntfy.R import io.heckel.ntfy.R
import io.heckel.ntfy.db.* import io.heckel.ntfy.db.*
import io.heckel.ntfy.msg.MESSAGE_ENCODING_BASE64 import io.heckel.ntfy.msg.MESSAGE_ENCODING_BASE64
import io.heckel.ntfy.ui.Colors
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay 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 // Queries the filename of a content URI
fun fileName(context: Context, contentUri: String?, fallbackName: String): String { fun fileName(context: Context, contentUri: String?, fallbackName: String): String {
return try { 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 // TextWatcher that only implements the afterTextChanged method
class AfterChangedTextWatcher(val afterTextChangedFn: (s: Editable?) -> Unit) : TextWatcher { class AfterChangedTextWatcher(val afterTextChangedFn: (s: Editable?) -> Unit) : TextWatcher {
override fun afterTextChanged(s: Editable?) { override fun afterTextChanged(s: Editable?) {
@ -506,3 +495,11 @@ fun String.sha256(): String {
val digest = md.digest(this.toByteArray()) val digest = md.digest(this.toByteArray())
return digest.fold("") { str, it -> str + "%02x".format(it) } 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)))
}
}

View file

@ -0,0 +1,2 @@
Bug fixes:
* Android 5 (SDK 21): Fix crash on unsubscribing (#528, thanks to Roger M.)