Fix Android 5 crashes on unsubscribing
This commit is contained in:
parent
6a8d222e12
commit
e64cd79c28
11 changed files with 43 additions and 59 deletions
|
@ -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"
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 */
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
2
fastlane/metadata/android/en-US/changelog/32.txt
Normal file
2
fastlane/metadata/android/en-US/changelog/32.txt
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
Bug fixes:
|
||||||
|
* Android 5 (SDK 21): Fix crash on unsubscribing (#528, thanks to Roger M.)
|
Loading…
Reference in a new issue