Commit 9a35ee6e authored by Christophe Henry's avatar Christophe Henry
Browse files

Add possibility to send reports when refresh has failed

parent 4afc5a27
Pipeline #4375 passed with stage
in 0 seconds
# Development
## Features
* Add possibility to enable a debug mode ([!108](https://git.feneas.org/christophehenry/freshrss-android/-/merge_requests/108))
* Add possibility to send a report of failed refreshed when debug mode is enabled ([!108](https://git.feneas.org/christophehenry/freshrss-android/-/merge_requests/108))
## Bug fixes
* Fix [#103](https://git.feneas.org/christophehenry/freshrss-android/-/issues/103): retrieving articles restults in 404 ([!110](https://git.feneas.org/christophehenry/freshrss-android/-/merge_requests/110))
......
......@@ -4,10 +4,13 @@
xmlns:dist="http://schemas.android.com/apk/distribution"
xmlns:tools="http://schemas.android.com/tools"
package="fr.chenry.android.freshrss">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<dist:module dist:instant="true" />
<application
android:name=".FreshRSSApplication"
android:allowBackup="true"
......@@ -20,28 +23,37 @@
android:theme="@style/AppTheme.Bright"
android:usesCleartextTraffic="true"
tools:ignore="GoogleAppIndexingWarning,UnusedAttribute">
<receiver
android:name=".utils.RefreshErrorDetails"
android:enabled="true"
android:exported="false"
/>
<activity
android:name=".activities.StartActivity"
android:label="@string/app_name"
android:theme="@style/AppTheme.NoActionBar">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".activities.LoginActivity"
android:label="@string/app_name"
android:persistableMode="persistAcrossReboots">
</activity>
android:persistableMode="persistAcrossReboots" />
<activity
android:name=".activities.MainActivity"
android:label="@string/app_name">
android:name=".activities.MainActivity" android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/plain" />
</intent-filter>
</activity>
</application>
</manifest>
\ No newline at end of file
......@@ -6,7 +6,6 @@ import android.content.Context
import android.content.SharedPreferences
import android.os.Build
import androidx.core.app.NotificationManagerCompat
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.room.Room
import androidx.work.*
......@@ -42,8 +41,6 @@ import kotlin.math.max
open class FreshRSSApplication: Application(), SharedPreferences.OnSharedPreferenceChangeListener {
val debugMode: LiveData<Boolean> = MutableLiveData(false)
open val preferences: FreshRSSPreferences by lazy {FreshRSSPreferences(this)}
open val workManager by lazy {WorkManager.getInstance(this)}
open val notificationManager: NotificationManagerCompat by lazy {NotificationManagerCompat.from(this)}
......@@ -67,15 +64,14 @@ open class FreshRSSApplication: Application(), SharedPreferences.OnSharedPrefere
preferences.apply {
initDefaults()
registerChangeListener(this@FreshRSSApplication)
debugMode = runCatching {
Properties().let {
it.load(baseContext.assets.open("config.properties"))
it.getProperty("debug")!!.toBoolean()
}
}.getOrDefault(false)
}
(debugMode as MutableLiveData).value = runCatching {
Properties().let {
it.load(baseContext.assets.open("config.properties"))
it.getProperty("debug")!!.toBoolean()
}
}.getOrDefault(false)
// Debug
//Stetho.initializeWithDefaults(this)
}
......
package fr.chenry.android.freshrss
import android.app.Notification
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.os.Build
import android.util.TypedValue
import android.widget.Toast
......@@ -14,7 +16,8 @@ import fr.chenry.android.freshrss.store.api.ALL_ITEMS_ID
import fr.chenry.android.freshrss.utils.*
import kotlinx.coroutines.*
import org.joda.time.DateTime
import org.joda.time.format.*
import org.joda.time.format.DateTimeFormatterBuilder
import org.joda.time.format.ISODateTimeFormat
import java.util.concurrent.TimeUnit
class RefreshWorker(appContext: Context, workerParams: WorkerParameters): CoroutineWorker(appContext, workerParams) {
......@@ -30,17 +33,7 @@ class RefreshWorker(appContext: Context, workerParams: WorkerParameters): Corout
.setProgress(0, 0, true)
.setOngoing(true)
.setPriority(NotificationCompat.PRIORITY_LOW)
.build()
private val failNotification =
createNotification(
NotificationChannels.ERRORS,
R.string.notification_refresh_failed_title,
R.string.notification_refresh_failed_description,
R.attr.colorError
)
.setCategory(Notification.CATEGORY_ERROR)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setVisibility(NotificationCompat.VISIBILITY_SECRET)
.build()
private val foregroundInfo = ForegroundInfo(NotificationHelper.ONGOING_REFRESH_NOTIFICATION, refreshNotification)
......@@ -87,7 +80,7 @@ class RefreshWorker(appContext: Context, workerParams: WorkerParameters): Corout
this.e(it)
requireF().notificationManager.notify(
NotificationHelper.FAIL_REFRESH_NOTIFICATION,
failNotification
failNotification(it)
)
}
}
......@@ -116,6 +109,38 @@ class RefreshWorker(appContext: Context, workerParams: WorkerParameters): Corout
requireF().notificationManager.cancel(NotificationHelper.FAIL_REFRESH_NOTIFICATION)
}
private fun failNotification(exception: Throwable): Notification {
val notificationBuilder = createNotification(
NotificationChannels.ERRORS,
R.string.notification_refresh_failed_title,
R.string.notification_refresh_failed_description,
R.attr.colorError
)
val debugMode = requireF().preferences.debugMode
if(debugMode) {
val intent = Intent(applicationContext, RefreshErrorDetails::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
putExtra(RefreshErrorDetails.EXEPTION_EXTRA_KEY, exception)
}
val pendingIntent: PendingIntent =
PendingIntent.getBroadcast(applicationContext, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
val sendReportButtonTxt = applicationContext.getString(R.string.send_report)
val detailMessage = applicationContext.getString(R.string.detail_error_message, sendReportButtonTxt)
notificationBuilder
.setContentText(detailMessage)
.addAction(R.drawable.ic_send_black_24dp, sendReportButtonTxt, pendingIntent)
}
return notificationBuilder
.setCategory(Notification.CATEGORY_ERROR)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setAutoCancel(true)
.build()
}
private fun createNotification(
chan: NotificationChannels,
@StringRes title: Int,
......
package fr.chenry.android.freshrss.activities
import android.content.SharedPreferences
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
import android.content.res.AssetManager
import android.os.Bundle
import android.view.MenuItem
......@@ -9,6 +11,7 @@ import androidx.activity.viewModels
import androidx.annotation.VisibleForTesting
import androidx.appcompat.app.AppCompatActivity
import androidx.core.text.HtmlCompat
import androidx.core.view.size
import androidx.drawerlayout.widget.DrawerLayout
import androidx.lifecycle.observe
import androidx.navigation.NavController
......@@ -23,9 +26,8 @@ import fr.chenry.android.freshrss.store.viewmodels.AccountVM
import fr.chenry.android.freshrss.utils.*
import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.coroutines.*
import org.joda.time.DateTime
class MainActivity: AppCompatActivity() {
class MainActivity: AppCompatActivity(), OnSharedPreferenceChangeListener {
private val accountVM by viewModels<AccountVM>()
@VisibleForTesting
......@@ -59,17 +61,8 @@ class MainActivity: AppCompatActivity() {
.text = it.login
}
requireF().debugMode.observe(this) {
val submenu = application_left_menu.menu.findItem(R.id.app_left_menu_app_sub)?.subMenu
if(it && submenu is SubMenu) submenu.add(R.string.debug_infos).apply {
icon = getDrawable(R.drawable.ic_build_black_24dp)
setOnMenuItemClickListener {
drawerLayout.closeDrawers()
DebugAlertDialog().show(supportFragmentManager, DebugAlertDialog::class.simpleName)
true
}
}
}
enableDebugSection()
requireF().preferences.registerChangeListener(this)
}
override fun onResume() {
......@@ -106,6 +99,26 @@ class MainActivity: AppCompatActivity() {
override fun onSupportNavigateUp(): Boolean =
navigation.navigateUp(appBarConfiguration) || super.onSupportNavigateUp()
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
if(key != requireF().preferences.debugModeKey) return
enableDebugSection()
}
private fun enableDebugSection() {
val debugMode = requireF().preferences.debugMode
val submenu = application_left_menu.menu.findItem(R.id.app_left_menu_app_sub)?.subMenu
if(submenu !is SubMenu) return
if(debugMode) submenu.add(0, R.id.debug_info_section_id, submenu.size, R.string.debug_infos).apply {
icon = getDrawable(R.drawable.ic_build_black_24dp)
setOnMenuItemClickListener {
drawerLayout.closeDrawers()
DebugAlertDialog().show(supportFragmentManager, DebugAlertDialog::class.simpleName)
true
}
} else submenu.removeItem(R.id.debug_info_section_id)
}
private fun isDrawerOpen() = drawerLayout.isDrawerOpen(application_left_menu)
private fun okCallback(input: String) {
......@@ -144,9 +157,4 @@ class MainActivity: AppCompatActivity() {
}
}
}
private fun shouldForceRefresh() = requireF().isPeriodicRefreshEnabled() && accountVM.account.value?.let {
val refreshRequency = requireF().preferences.refreshFrequency.toInt()
it.lastFetchDate.plusMinutes(refreshRequency).isBefore(DateTime.now())
} ?: false
}
......@@ -94,8 +94,7 @@ class ArticleDetailFragment: Fragment(), View.OnClickListener {
startActivity(Intent.createChooser(ShareIntent.create(feedTitle, article), chooserTitle))
}
R.id.fab_open_browser -> startActivity(Intent(Intent.ACTION_VIEW, article.url))
else -> {
}
else -> NOOP()
}
private fun setUpReadStatusButton(menu: Menu) {
......@@ -131,6 +130,8 @@ class ArticleDetailFragment: Fragment(), View.OnClickListener {
return
}
val ctx = requireContext()
GlobalScope.launch {
val result = Store.postReadStatus(article, readStatus)
......@@ -139,16 +140,16 @@ class ArticleDetailFragment: Fragment(), View.OnClickListener {
this.e(it)
val readText = when(readStatus) {
ReadStatus.READ -> this.getString(R.string.read)
ReadStatus.UNREAD -> this.getString(R.string.unread)
ReadStatus.READ -> ctx.getString(R.string.read)
ReadStatus.UNREAD -> ctx.getString(R.string.unread)
}.let {msg ->
this.getString(R.string.mark_read_status_authorization, msg)
ctx.getString(R.string.mark_read_status_authorization, msg)
}
val toastText = this.getString(R.string.unable_to, readText)
val toastText = ctx.getString(R.string.unable_to, readText)
withContext(Dispatchers.Main) {
Toast.makeText(this@ArticleDetailFragment.context, toastText, LENGTH_SHORT).show()
Toast.makeText(ctx, toastText, LENGTH_SHORT).show()
}
}
}
......
......@@ -146,20 +146,21 @@ class ArticlesFragment: Fragment() {
it.article.readStatusRequestOnGoing = true
val ctx = requireContext()
GlobalScope.launch {
val expected = it.article.readStatus.toggle()
Store.postReadStatus(it.article, expected).onFailure {err ->
this@ArticleAdapter.e(err)
val fragment = this@ArticlesFragment
val message = when(expected) {
READ -> fragment.getString(R.string.read)
UNREAD -> fragment.getString(R.string.unread)
}.let {msg -> fragment.getString(R.string.mark_read_status_authorization, msg)}
READ -> ctx.getString(R.string.read)
UNREAD -> ctx.getString(R.string.unread)
}.let {msg -> ctx.getString(R.string.mark_read_status_authorization, msg)}
val toastText = fragment.getString(R.string.unable_to, message)
val toastText = ctx.getString(R.string.unable_to, message)
withContext(Dispatchers.Main) {
Toast.makeText(fragment.context, toastText, LENGTH_SHORT).show()
Toast.makeText(ctx, toastText, LENGTH_SHORT).show()
}
}
......
......@@ -14,13 +14,19 @@ class SettingsFragment: PreferenceFragmentCompat(), Preference.OnPreferenceChang
@VisibleForTesting
val retainScrollPositionPreference by lazy {
findPreference<CheckBoxPreference>(requireF().preferences.retainScrollPositionKey)!!
findPreference<SwitchPreferenceCompat>(requireF().preferences.retainScrollPositionKey)!!
}
@VisibleForTesting
val debugModePreference by lazy {
findPreference<SwitchPreferenceCompat>(requireF().preferences.debugModeKey)!!
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.preference_screen)
refreshFrequencyPreference.onPreferenceChangeListener = this
retainScrollPositionPreference.onPreferenceChangeListener = this
debugModePreference.onPreferenceChangeListener = this
}
override fun onResume() {
......@@ -30,20 +36,15 @@ class SettingsFragment: PreferenceFragmentCompat(), Preference.OnPreferenceChang
}
override fun onPreferenceChange(preference: Preference, newValue: Any): Boolean = when(preference.key) {
refreshFrequencyPreference.key -> {
requireF().preferences.refreshFrequency = newValue.toString().toLong()
setRefreshFrequencyPreference(newValue.toString())
}
retainScrollPositionPreference.key -> {
requireF().preferences.retainScrollPosition = newValue as Boolean
setRetainScrollPositionPreference(newValue)
}
refreshFrequencyPreference.key -> setRefreshFrequencyPreference(newValue.toString())
retainScrollPositionPreference.key -> setRetainScrollPositionPreference(newValue as Boolean)
debugModePreference.key -> setDebugModePreference(newValue as Boolean)
else -> false
}
private fun setRefreshFrequencyPreference(newValue: String): Boolean {
requireF().preferences.refreshFrequency = newValue.toLong()
refreshFrequencyPreference.findIndexOfValue(newValue).let {
val defaultValue = requireF().getString(R.string.refresh_frequency_30m)
val newValueStr = refreshFrequencyPreference.entries.getOrNull(it) ?: defaultValue
......@@ -53,7 +54,14 @@ class SettingsFragment: PreferenceFragmentCompat(), Preference.OnPreferenceChang
}
private fun setRetainScrollPositionPreference(newValue: Boolean): Boolean {
requireF().preferences.retainScrollPosition = newValue
retainScrollPositionPreference.isChecked = newValue
return true
}
private fun setDebugModePreference(newValue: Boolean): Boolean {
requireF().preferences.debugMode = newValue
debugModePreference.isChecked = newValue
return true
}
}
......@@ -20,6 +20,7 @@ class FreshRSSPreferences(private val application: FreshRSSApplication) {
private val subscriptionSectionKey = getKey(R.string.subscription_section_preference)
val refreshFrequencyKey = getKey(R.string.refresh_frequency_preference)
val retainScrollPositionKey = getKey(R.string.retain_scroll_position_preference)
val debugModeKey = getKey(R.string.debug_mode_preference)
var refreshFrequency: Long
get() {
......@@ -43,6 +44,10 @@ class FreshRSSPreferences(private val application: FreshRSSApplication) {
get() = sharedPreferences.getBoolean(retainScrollPositionKey, true)
set(value) = sharedPreferences.edit {putBoolean(retainScrollPositionKey, value)}
var debugMode: Boolean
get() = sharedPreferences.getBoolean(debugModeKey, false)
set(value) = sharedPreferences.edit {putBoolean(debugModeKey, value)}
fun initDefaults() {
PreferenceManager.setDefaultValues(application, R.xml.preference_screen, false)
}
......
......@@ -37,7 +37,7 @@ interface Grouper {
object TitleGrouper: Grouper {
@SuppressLint("DefaultLocale")
override operator fun invoke(subscription: Subscription): String =
subscription.title[0].toString().toUpperCase()
subscription.title.getOrElse(0) {'#'}.toString().toUpperCase()
}
object DateGrouper: Grouper {
......
......@@ -3,6 +3,7 @@
package fr.chenry.android.freshrss.utils
import android.app.Activity
import android.content.BroadcastReceiver
import android.content.Context
import android.net.ConnectivityManager
import android.net.Uri
......@@ -22,7 +23,10 @@ import java.net.URL
import java.util.Locale
import java.util.TimeZone
fun Any?.unit() = Unit
@Suppress("NOTHING_TO_INLINE", "FunctionName")
inline fun NOOP(): Unit = Unit
fun Any?.unit() = NOOP()
fun Any.v(message: String) = Log.v(this::class.qualifiedName, message).unit()
fun Any.v(message: Throwable) = Log.v(this::class.qualifiedName, "VERBOSE", message).unit()
fun Any.d(message: String) = Log.d(this::class.qualifiedName, message).unit()
......@@ -85,6 +89,7 @@ fun Activity.requireF() = this.application as FreshRSSApplication
fun Fragment.requireF() = this.requireActivity().application as FreshRSSApplication
fun AndroidViewModel.requireF() = this.getApplication<FreshRSSApplication>()
fun RefreshWorker.requireF() = this.applicationContext as FreshRSSApplication
fun Context.requireF() = this.applicationContext as FreshRSSApplication
fun WorkManager.getRefreshWorkLiveData() = this.getWorkInfosByTagLiveData(RefreshWorker.REFRESH_WORK_TAG)
fun WorkManager.getPeriodicRefreshWorksLiveData() = this.getWorkInfosByTagLiveData(RefreshWorker.PERIODIC_WORK_TAG)
......
package fr.chenry.android.freshrss.utils
import android.content.*
import org.acra.ACRA
class RefreshErrorDetails: BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) = intent.getSerializableExtra(EXEPTION_EXTRA_KEY)?.let {
if(it !is Throwable) return@let
ACRA.getErrorReporter().handleSilentException(RefreshFailedException(it))
context.requireF().notificationManager.cancel(NotificationHelper.FAIL_REFRESH_NOTIFICATION)
}.unit()
companion object {
const val EXEPTION_EXTRA_KEY = "EXEPTION_EXTRA_KEY"
}
private class RefreshFailedException(cause: Throwable): Exception(cause)
}
<vector android:height="24dp" android:tint="#7D7D7D"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M2.01,21L23,12 2.01,3 2,10l15,2 -15,2z"/>
</vector>
<?xml version="1.0" encoding="utf-8"?>
<resources>
<item name="debug_info_section_id" type="id" />
</resources>
\ No newline at end of file
......@@ -40,6 +40,7 @@
<string name="refresh_frequency_preference" translatable="false">refresh_frequency_preference</string>
<string name="subscription_section_preference" translatable="false">subscription_section_preference</string>
<string name="retain_scroll_position_preference" translatable="false">retain_scroll_position_preference</string>
<string name="debug_mode_preference" translatable="false">debug_mode_preference</string>
<string name="prompt_login">Login</string>
<string name="prompt_password">Password</string>
......@@ -139,4 +140,8 @@
<string name="last_sync_absolute_date_text">Last sync on %s</string>
<string name="retain_scroll_position_preference_title">Retain article scroll positions</string>
<string name="retain_scroll_position_preference_summary">If checked, scroll positions will be remembered even after a restart.</string>
<string name="debug_mode">Debug mode</string>
<string name="advanced_preference_section">Advanced</string>
<string name="send_report">Send a report</string>
<string name="detail_error_message">\"If you think this is an error that should be reported hit the \'%s\' button\"</string>
</resources>
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen
xmlns:android="http://schemas.android.com/apk/res/android">
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto">
<PreferenceCategory android:title="@string/refresh_frequency">
<ListPreference
android:defaultValue="@string/refresh_frequency_default_value"
android:entries="@array/refresh_frequencies"
android:entryValues="@array/refresh_frequency_values"
android:key="@string/refresh_frequency_preference"
android:title="@string/refresh_frequency"
/>
android:title="@string/refresh_frequency" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/appearance_setting_title">
<CheckBoxPreference
<SwitchPreferenceCompat
android:defaultValue="true"
android:key="@string/retain_scroll_position_preference"
android:summary="@string/retain_scroll_position_preference_summary"
android:title="@string/retain_scroll_position_preference_title"
android:title="@string/retain_scroll_position_preference_title" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/advanced_preference_section" app:initialExpandedChildrenCount="0">
<SwitchPreferenceCompat
android:defaultValue="false"
android:title="@string/debug_mode"
android:key="@string/debug_mode_preference"
/>
</PreferenceCategory>
</PreferenceScreen>
\ No newline at end of file
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment