Improve background loop exception presentation

This commit is contained in:
Jonas Lochmann 2024-07-29 02:00:00 +02:00
parent 931b884567
commit bb0decc808
No known key found for this signature in database
GPG key ID: 8B8C9AEE10FA5B36
13 changed files with 149 additions and 25 deletions

View file

@ -122,6 +122,14 @@
</intent-filter> </intent-filter>
</activity> </activity>
<activity
android:name=".ui.diagnose.exception.DiagnoseExceptionActivity"
android:theme="@style/AppTheme.Translucent"
android:exported="false"
android:excludeFromRecents="true"
android:taskAffinity=":exception"
android:launchMode="singleTop" />
<!-- system integration --> <!-- system integration -->
<receiver android:name=".integration.platform.android.receiver.BootReceiver" android:exported="false"> <receiver android:name=".integration.platform.android.receiver.BootReceiver" android:exported="false">

View file

@ -220,7 +220,8 @@ data class AppStatusMessage(
val title: String, val title: String,
val text: String, val text: String,
val subtext: String? = null, val subtext: String? = null,
val showSwitchToDefaultUserOption: Boolean = false val showSwitchToDefaultUserOption: Boolean = false,
val showErrorMessage: Boolean = false
): Parcelable ): Parcelable
data class BatteryStatus( data class BatteryStatus(

View file

@ -1,5 +1,5 @@
/* /*
* TimeLimit Copyright <C> 2019 - 2022 Jonas Lochmann * TimeLimit Copyright <C> 2019 - 2024 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -26,6 +26,7 @@ import io.timelimit.android.logic.DefaultAppLogic
import io.timelimit.android.sync.actions.SignOutAtDeviceAction import io.timelimit.android.sync.actions.SignOutAtDeviceAction
import io.timelimit.android.sync.actions.apply.ApplyActionUtil import io.timelimit.android.sync.actions.apply.ApplyActionUtil
import io.timelimit.android.ui.MainActivity import io.timelimit.android.ui.MainActivity
import io.timelimit.android.ui.diagnose.exception.DiagnoseExceptionActivity
class BackgroundActionService: Service() { class BackgroundActionService: Service() {
companion object { companion object {
@ -36,7 +37,7 @@ class BackgroundActionService: Service() {
fun prepareRevokeTemporarilyAllowed(context: Context) = Intent(context, BackgroundActionService::class.java) fun prepareRevokeTemporarilyAllowed(context: Context) = Intent(context, BackgroundActionService::class.java)
.putExtra(ACTION, ACTION_REVOKE_TEMPORARILY_ALLOWED_APPS) .putExtra(ACTION, ACTION_REVOKE_TEMPORARILY_ALLOWED_APPS)
fun getSwitchToDefaultUserIntent(context: Context) = PendingIntent.getService( fun getSwitchToDefaultUserIntent(context: Context): PendingIntent = PendingIntent.getService(
context, context,
PendingIntentIds.SWITCH_TO_DEFAULT_USER, PendingIntentIds.SWITCH_TO_DEFAULT_USER,
Intent(context, BackgroundActionService::class.java) Intent(context, BackgroundActionService::class.java)
@ -44,12 +45,22 @@ class BackgroundActionService: Service() {
PendingIntentIds.PENDING_INTENT_FLAGS PendingIntentIds.PENDING_INTENT_FLAGS
) )
fun getOpenAppIntent(context: Context) = PendingIntent.getActivity( fun getOpenAppIntent(context: Context): PendingIntent = PendingIntent.getActivity(
context, context,
PendingIntentIds.OPEN_MAIN_APP, PendingIntentIds.OPEN_MAIN_APP,
Intent(context, MainActivity::class.java), Intent(context, MainActivity::class.java),
PendingIntentIds.PENDING_INTENT_FLAGS PendingIntentIds.PENDING_INTENT_FLAGS
) )
fun getOpenAppWithErrorIntent(context: Context): PendingIntent = PendingIntent.getActivities(
context,
PendingIntentIds.OPEN_MAIN_APP_WITH_ERROR,
arrayOf(
Intent(context, MainActivity::class.java).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
Intent(context, DiagnoseExceptionActivity::class.java).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
),
PendingIntentIds.PENDING_INTENT_FLAGS
)
} }
private val notificationManager: NotificationManager by lazy { private val notificationManager: NotificationManager by lazy {

View file

@ -84,7 +84,12 @@ class BackgroundService: Service() {
.setContentTitle(appStatusMessage.title) .setContentTitle(appStatusMessage.title)
.setContentText(appStatusMessage.text) .setContentText(appStatusMessage.text)
.setSubText(appStatusMessage.subtext) .setSubText(appStatusMessage.subtext)
.setContentIntent(BackgroundActionService.getOpenAppIntent(context)) .setContentIntent(
if (appStatusMessage.showErrorMessage)
BackgroundActionService.getOpenAppWithErrorIntent(context)
else
BackgroundActionService.getOpenAppIntent(context)
)
.setWhen(0) .setWhen(0)
.setShowWhen(false) .setShowWhen(false)
.setSound(null) .setSound(null)

View file

@ -29,6 +29,7 @@ object NotificationIds {
const val REVOKE_TEMPORARILY_ALLOWED_APPS = 3 const val REVOKE_TEMPORARILY_ALLOWED_APPS = 3
const val TIME_WARNING = 4 const val TIME_WARNING = 4
const val EXTRA_TIME_STARTED = 5 const val EXTRA_TIME_STARTED = 5
const val OPEN_MAIN_APP_WITH_ERROR = 6
} }
object NotificationChannels { object NotificationChannels {
@ -133,6 +134,7 @@ object PendingIntentIds {
const val SWITCH_TO_DEFAULT_USER = 3 const val SWITCH_TO_DEFAULT_USER = 3
const val U2F_NFC_DISCOVERY = 4 const val U2F_NFC_DISCOVERY = 4
const val U2F_USB_RESPONSE = 5 const val U2F_USB_RESPONSE = 5
const val OPEN_MAIN_APP_WITH_ERROR = 6
val PENDING_INTENT_FLAGS = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { val PENDING_INTENT_FLAGS = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE

View file

@ -777,7 +777,8 @@ class BackgroundTaskLogic(val appLogic: AppLogic) {
appLogic.platformIntegration.setAppStatusMessage(AppStatusMessage( appLogic.platformIntegration.setAppStatusMessage(AppStatusMessage(
appLogic.context.getString(R.string.background_logic_error), appLogic.context.getString(R.string.background_logic_error),
appLogic.context.getString(R.string.background_logic_error_internal), appLogic.context.getString(R.string.background_logic_error_internal),
showSwitchToDefaultUserOption = deviceRelatedData.canSwitchToDefaultUser showSwitchToDefaultUserOption = deviceRelatedData.canSwitchToDefaultUser,
showErrorMessage = true
)) ))
appLogic.platformIntegration.setShowBlockingOverlay(false) appLogic.platformIntegration.setShowBlockingOverlay(false)
} }

View file

@ -1,5 +1,5 @@
/* /*
* Open TimeLimit Copyright <C> 2019 - 2022 Jonas Lochmann * Open TimeLimit Copyright <C> 2019 - 2024 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -29,6 +29,7 @@ import io.timelimit.android.extensions.safeNavigate
import io.timelimit.android.livedata.liveDataFromNonNullValue import io.timelimit.android.livedata.liveDataFromNonNullValue
import io.timelimit.android.livedata.liveDataFromNullableValue import io.timelimit.android.livedata.liveDataFromNullableValue
import io.timelimit.android.logic.DefaultAppLogic import io.timelimit.android.logic.DefaultAppLogic
import io.timelimit.android.ui.diagnose.exception.DiagnoseExceptionDialogFragment
import io.timelimit.android.ui.main.ActivityViewModelHolder import io.timelimit.android.ui.main.ActivityViewModelHolder
import io.timelimit.android.ui.main.AuthenticationFab import io.timelimit.android.ui.main.AuthenticationFab
import io.timelimit.android.ui.main.FragmentWithCustomTitle import io.timelimit.android.ui.main.FragmentWithCustomTitle

View file

@ -0,0 +1,34 @@
/*
* TimeLimit Copyright <C> 2019 - 2024 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.timelimit.android.ui.diagnose.exception
import android.os.Bundle
import androidx.fragment.app.FragmentActivity
import io.timelimit.android.logic.DefaultAppLogic
class DiagnoseExceptionActivity: FragmentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val ex = DefaultAppLogic.with(this).backgroundTaskLogic.lastLoopException.value
if (ex != null) {
if (savedInstanceState == null) {
DiagnoseExceptionDialogFragment.newInstance(ex, true).show(supportFragmentManager)
}
} else finish()
}
}

View file

@ -1,5 +1,5 @@
/* /*
* TimeLimit Copyright <C> 2019 Jonas Lochmann * TimeLimit Copyright <C> 2019 - 2024 Jonas Lochmann
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -13,12 +13,13 @@
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package io.timelimit.android.ui.diagnose package io.timelimit.android.ui.diagnose.exception
import android.app.Dialog import android.app.Dialog
import android.content.ClipData import android.content.ClipData
import android.content.ClipboardManager import android.content.ClipboardManager
import android.content.Context import android.content.Context
import android.content.DialogInterface
import android.os.Bundle import android.os.Bundle
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
@ -26,35 +27,26 @@ import androidx.fragment.app.DialogFragment
import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentManager
import io.timelimit.android.R import io.timelimit.android.R
import io.timelimit.android.extensions.showSafe import io.timelimit.android.extensions.showSafe
import java.io.PrintWriter
import java.io.StringWriter
class DiagnoseExceptionDialogFragment: DialogFragment() { class DiagnoseExceptionDialogFragment: DialogFragment() {
companion object { companion object {
private const val DIALOG_TAG = "DiagnoseExceptionDialogFragment" private const val DIALOG_TAG = "DiagnoseExceptionDialogFragment"
private const val EXCEPTION = "ex" private const val EXCEPTION = "ex"
private const val FINISH_ON_DISMISS = "finish"
fun newInstance(exception: Exception) = DiagnoseExceptionDialogFragment().apply { fun newInstance(exception: Exception, finishOnDismiss: Boolean = false) = DiagnoseExceptionDialogFragment().apply {
arguments = Bundle().apply { arguments = Bundle().apply {
putSerializable(EXCEPTION, exception) putSerializable(EXCEPTION, exception)
putBoolean(FINISH_ON_DISMISS, finishOnDismiss)
} }
} }
fun getStackTraceString(tr: Throwable): String = StringWriter().let { sw ->
PrintWriter(sw).let { pw ->
tr.printStackTrace(pw)
pw.flush()
}
sw.toString()
}
} }
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val message = getStackTraceString(arguments!!.getSerializable(EXCEPTION) as Exception) val message = ExceptionUtil.formatInterpreted(requireContext(), requireArguments().getSerializable(EXCEPTION) as Exception)
val clipboard = context!!.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager val clipboard = requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
return AlertDialog.Builder(context!!, theme) return AlertDialog.Builder(requireContext(), theme)
.setMessage(message) .setMessage(message)
.setNeutralButton(R.string.diagnose_copy_to_clipboard) { _, _ -> .setNeutralButton(R.string.diagnose_copy_to_clipboard) { _, _ ->
clipboard.setPrimaryClip(ClipData.newPlainText("TimeLimit", message)) clipboard.setPrimaryClip(ClipData.newPlainText("TimeLimit", message))
@ -65,5 +57,13 @@ class DiagnoseExceptionDialogFragment: DialogFragment() {
.create() .create()
} }
override fun onDismiss(dialog: DialogInterface) {
super.onDismiss(dialog)
if (requireArguments().getBoolean(FINISH_ON_DISMISS) && isResumed) {
requireActivity().finish()
}
}
fun show(fragmentManager: FragmentManager) = showSafe(fragmentManager, DIALOG_TAG) fun show(fragmentManager: FragmentManager) = showSafe(fragmentManager, DIALOG_TAG)
} }

View file

@ -0,0 +1,45 @@
/*
* TimeLimit Copyright <C> 2019 - 2024 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package io.timelimit.android.ui.diagnose.exception
import android.content.Context
import io.timelimit.android.R
import io.timelimit.android.integration.platform.android.foregroundapp.InstanceIdForegroundAppHelper.InstanceIdException
import java.io.PrintWriter
import java.io.StringWriter
object ExceptionUtil {
fun formatInterpreted(context: Context, tr: Throwable): String {
val explain = when (tr) {
is InstanceIdException.EventsNotSortedByTimestamp -> context.getString(R.string.background_logic_errpr_detailed_instanceid_sorting)
else -> null
}
val tr2 = formatSimple(tr)
return if (explain != null) "$explain\n\n$tr2"
else tr2
}
private fun formatSimple(tr: Throwable): String = StringWriter().let { sw ->
PrintWriter(sw).let { pw ->
tr.printStackTrace(pw)
pw.flush()
}
sw.toString()
}
}

View file

@ -123,6 +123,12 @@
<string name="background_logic_opening_lockscreen">Sperrbildschirm wird geöffnet</string> <string name="background_logic_opening_lockscreen">Sperrbildschirm wird geöffnet</string>
<string name="background_logic_permission_sanction_title">Berechtigung fehlt</string> <string name="background_logic_permission_sanction_title">Berechtigung fehlt</string>
<string name="background_logic_permission_sanction_text">nehme an, dass alles verwendet wird</string> <string name="background_logic_permission_sanction_text">nehme an, dass alles verwendet wird</string>
<string name="background_logic_errpr_detailed_instanceid_sorting">Es gibt ein Problem mit der
Nutzungsstatisitkdatenbank Ihres Gerätes.
Deshalb kann TimeLimit nicht zuverlässig erkennen, welche App verwendet wird.
Dieses Problem kann nur behoben werden durch vier Tagen Geduld oder das Zurücksetzen des Gerätes.
Eine Neuinstallation von TimeLimit hilft NICHT.
</string>
<string name="background_logic_temporarily_allowed_title">Apps wurden vorübergehend erlaubt</string> <string name="background_logic_temporarily_allowed_title">Apps wurden vorübergehend erlaubt</string>
<string name="background_logic_temporarily_allowed_text">Zum Rückgängig machen hier tippen oder Bildschirm ausschalten</string> <string name="background_logic_temporarily_allowed_text">Zum Rückgängig machen hier tippen oder Bildschirm ausschalten</string>

View file

@ -154,6 +154,12 @@
<string name="background_logic_opening_lockscreen">Opening lock screen</string> <string name="background_logic_opening_lockscreen">Opening lock screen</string>
<string name="background_logic_permission_sanction_title">Missing permission</string> <string name="background_logic_permission_sanction_title">Missing permission</string>
<string name="background_logic_permission_sanction_text">assuming that everything is used</string> <string name="background_logic_permission_sanction_text">assuming that everything is used</string>
<string name="background_logic_errpr_detailed_instanceid_sorting">There is an issue with
the data in the usage stats database of your device.
Due to that, TimeLimit can not reliable detect the running Apps.
You can do nothing to solve this except waiting for four days or doing a reset of the whole device.
Reinstalling TimeLimit does NOT help.
</string>
<string name="background_logic_temporarily_allowed_title">Apps are temporarily allowed</string> <string name="background_logic_temporarily_allowed_title">Apps are temporarily allowed</string>
<string name="background_logic_temporarily_allowed_text">Tap here or turn screen off to undo</string> <string name="background_logic_temporarily_allowed_text">Tap here or turn screen off to undo</string>

View file

@ -1,5 +1,5 @@
<!-- <!--
Open TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann Open TimeLimit Copyright <C> 2019 - 2024 Jonas Lochmann
This program is free software: you can redistribute it and/or modify This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by it under the terms of the GNU General Public License as published by
@ -34,6 +34,10 @@
<item name="colorAccent">@color/white</item> <item name="colorAccent">@color/white</item>
</style> </style>
<style name="AppTheme.Translucent" parent="AppTheme">
<item name="android:windowIsFloating">true</item>
</style>
<!-- from https://stackoverflow.com/a/46286184 --> <!-- from https://stackoverflow.com/a/46286184 -->
<style name="BottomSheetDialog" parent="Theme.MaterialComponents.DayNight.BottomSheetDialog"> <style name="BottomSheetDialog" parent="Theme.MaterialComponents.DayNight.BottomSheetDialog">
<item name="colorPrimary">@color/colorPrimary</item> <item name="colorPrimary">@color/colorPrimary</item>