Add exit reason screen

This commit is contained in:
Jonas Lochmann 2022-02-28 01:00:00 +01:00
parent 7edb636c67
commit c3b9efa37a
No known key found for this signature in database
GPG key ID: 8B8C9AEE10FA5B36
13 changed files with 338 additions and 5 deletions

View file

@ -0,0 +1,73 @@
/*
* TimeLimit Copyright <C> 2019 - 2022 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.integration.platform
import android.app.ApplicationExitInfo
import android.os.Build
import androidx.annotation.RequiresApi
data class ExitLogItem (
val timestamp: Long,
val reason: ExitReason,
val description: String?
) {
companion object {
@RequiresApi(Build.VERSION_CODES.R)
fun fromApplicationExitInfo(item: ApplicationExitInfo): ExitLogItem = ExitLogItem(
timestamp = item.timestamp,
reason = ExitReasonConverter.fromApplicationExitInfo(item),
description = item.description
)
}
}
enum class ExitReason {
AppNotResponding,
Crash,
CrashNative,
DependencyDied,
TooMuchRessourceUsage,
Self,
InitFailure,
LowMemory,
SystemOther,
PermissionChange,
Signaled,
SystemUnknown,
UserRequest,
UserStopped,
Unknown
}
object ExitReasonConverter {
fun fromApplicationExitInfo(input: ApplicationExitInfo): ExitReason = when (input.reason) {
ApplicationExitInfo.REASON_ANR -> ExitReason.AppNotResponding
ApplicationExitInfo.REASON_CRASH -> ExitReason.Crash
ApplicationExitInfo.REASON_CRASH_NATIVE -> ExitReason.CrashNative
ApplicationExitInfo.REASON_DEPENDENCY_DIED -> ExitReason.DependencyDied
ApplicationExitInfo.REASON_EXCESSIVE_RESOURCE_USAGE -> ExitReason.TooMuchRessourceUsage
ApplicationExitInfo.REASON_EXIT_SELF -> ExitReason.Self
ApplicationExitInfo.REASON_INITIALIZATION_FAILURE -> ExitReason.InitFailure
ApplicationExitInfo.REASON_LOW_MEMORY -> ExitReason.LowMemory
ApplicationExitInfo.REASON_OTHER -> ExitReason.SystemOther
ApplicationExitInfo.REASON_PERMISSION_CHANGE -> ExitReason.PermissionChange
ApplicationExitInfo.REASON_SIGNALED -> ExitReason.Signaled
ApplicationExitInfo.REASON_UNKNOWN -> ExitReason.SystemUnknown
ApplicationExitInfo.REASON_USER_REQUESTED -> ExitReason.UserRequest
ApplicationExitInfo.REASON_USER_STOPPED -> ExitReason.UserStopped
else -> ExitReason.Unknown
}
}

View file

@ -1,5 +1,5 @@
/*
* TimeLimit Copyright <C> 2019 - 2021 Jonas Lochmann
* TimeLimit Copyright <C> 2019 - 2022 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
@ -86,6 +86,8 @@ abstract class PlatformIntegration(
confirmationLevel: SystemPermissionConfirmationLevel = SystemPermissionConfirmationLevel.None
): Boolean
abstract fun getExitLog(): List<ExitLogItem>
var installedAppsChangeListener: Runnable? = null
var systemClockChangeListener: Runnable? = null
}

View file

@ -1,5 +1,5 @@
/*
* TimeLimit Copyright <C> 2019 - 2021 Jonas Lochmann
* TimeLimit Copyright <C> 2019 - 2022 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
@ -793,4 +793,11 @@ class AndroidIntegration(context: Context): PlatformIntegration(maximumProtectio
}
}
}
override fun getExitLog(): List<ExitLogItem> {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
activityManager.getHistoricalProcessExitReasons(context.packageName, 0, 0)
.map { ExitLogItem.fromApplicationExitInfo(it) }
} else emptyList()
}
}

View file

@ -1,5 +1,5 @@
/*
* TimeLimit Copyright <C> 2019 - 2021 Jonas Lochmann
* TimeLimit Copyright <C> 2019 - 2022 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
@ -185,4 +185,6 @@ class DummyIntegration(
permission: SystemPermission,
confirmationLevel: SystemPermissionConfirmationLevel
): Boolean = false
override fun getExitLog(): List<ExitLogItem> = emptyList()
}

View file

@ -1,5 +1,5 @@
/*
* TimeLimit Copyright <C> 2019 - 2021 Jonas Lochmann
* TimeLimit Copyright <C> 2019 - 2022 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
@ -100,6 +100,13 @@ class DiagnoseMainFragment : Fragment(), FragmentWithCustomTitle {
}
}
binding.diagnoseExitReasonsButton.setOnClickListener {
navigation.safeNavigate(
DiagnoseMainFragmentDirections.actionDiagnoseMainFragmentToDiagnoseExitReasonFragment(),
R.id.diagnoseMainFragment
)
}
AuthenticationFab.manageAuthenticationFab(
fab = binding.fab,
shouldHighlight = auth.shouldHighlightAuthenticationButton,

View file

@ -0,0 +1,46 @@
/*
* TimeLimit Copyright <C> 2019 - 2022 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.exitreason
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import io.timelimit.android.databinding.DiagnoseExitReasonItemBinding
import io.timelimit.android.integration.platform.ExitLogItem
import java.text.DateFormat
import java.util.*
import kotlin.properties.Delegates
class DiagnoseExitReasonAdapter: RecyclerView.Adapter<DiagnoseExitReasonHolder>() {
var content: List<ExitLogItem> by Delegates.observable(emptyList()) { _, _, _ -> notifyDataSetChanged() }
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DiagnoseExitReasonHolder = DiagnoseExitReasonHolder(
DiagnoseExitReasonItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
)
override fun onBindViewHolder(holder: DiagnoseExitReasonHolder, position: Int) {
val item = content[position]
val view = holder.binding
view.timeString = DateFormat.getDateTimeInstance().format(Date(item.timestamp))
view.summaryString = item.reason.toString()
view.detailString = item.description
}
override fun getItemCount(): Int = content.size
}
class DiagnoseExitReasonHolder(val binding: DiagnoseExitReasonItemBinding): RecyclerView.ViewHolder(binding.root)

View file

@ -0,0 +1,48 @@
/*
* TimeLimit Copyright <C> 2019 - 2022 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.exitreason
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.lifecycle.LiveData
import androidx.recyclerview.widget.LinearLayoutManager
import io.timelimit.android.R
import io.timelimit.android.databinding.DiagnoseExitReasonFragmentBinding
import io.timelimit.android.livedata.liveDataFromNullableValue
import io.timelimit.android.logic.DefaultAppLogic
import io.timelimit.android.ui.main.FragmentWithCustomTitle
class DiagnoseExitReasonFragment: Fragment(), FragmentWithCustomTitle {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val binding = DiagnoseExitReasonFragmentBinding.inflate(inflater, container, false)
val data = DefaultAppLogic.with(requireContext()).platformIntegration.getExitLog()
val recycler = binding.recycler
val adapter = DiagnoseExitReasonAdapter()
adapter.content = data
recycler.layoutManager = LinearLayoutManager(requireContext())
recycler.adapter = adapter
binding.isEmpty = data.isEmpty()
return binding.root
}
override fun getCustomTitle(): LiveData<String?> = liveDataFromNullableValue("${getString(R.string.diagnose_er_title)} < ${getString(
R.string.about_diagnose_title)} < ${getString(R.string.main_tab_overview)}")
}

View file

@ -0,0 +1,48 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
TimeLimit Copyright <C> 2019 - 2022 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/>.
-->
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="isEmpty"
type="boolean" />
<import type="android.view.View" />
</data>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<TextView
android:visibility="@{isEmpty ? View.VISIBLE : View.GONE}"
android:textAppearance="?android:textAppearanceMedium"
android:padding="16dp"
android:gravity="center"
android:text="@string/diagnose_er_empty"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>
</layout>

View file

@ -0,0 +1,76 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
TimeLimit Copyright <C> 2019 - 2022 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/>.
-->
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="timeString"
type="String" />
<variable
name="summaryString"
type="String" />
<variable
name="detailString"
type="String" />
<import type="android.text.TextUtils" />
<import type="android.view.View" />
</data>
<androidx.cardview.widget.CardView
android:layout_marginLeft="4dp"
android:layout_marginRight="4dp"
android:layout_marginTop="2dp"
android:layout_marginBottom="2dp"
app:cardUseCompatPadding="true"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:padding="8dp"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:textAppearance="?android:textAppearanceLarge"
tools:text="vom Benutzer beendet"
android:text="@{summaryString}"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<TextView
android:textAppearance="?android:textAppearanceMedium"
tools:text="er will keine Begrenzungen"
android:text="@{detailString}"
android:visibility="@{TextUtils.isEmpty(detailString) ? View.GONE : View.VISIBLE}"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<TextView
android:textAppearance="?android:textAppearanceMedium"
tools:text="vor fünf Minuten"
android:text="@{timeString}"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
</androidx.cardview.widget.CardView>
</layout>

View file

@ -1,5 +1,5 @@
<!--
TimeLimit Copyright <C> 2019 - 2020 Jonas Lochmann
TimeLimit Copyright <C> 2019 - 2022 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.
@ -87,6 +87,13 @@
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<Button
style="?materialButtonOutlinedStyle"
android:id="@+id/diagnose_exit_reasons_button"
android:text="@string/diagnose_er_title"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
</ScrollView>

View file

@ -405,6 +405,13 @@
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
<action
android:id="@+id/action_diagnoseMainFragment_to_diagnoseExitReasonFragment"
app:destination="@id/diagnoseExitReasonFragment"
app:enterAnim="@anim/nav_default_enter_anim"
app:exitAnim="@anim/nav_default_exit_anim"
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
app:popExitAnim="@anim/nav_default_pop_exit_anim" />
</fragment>
<fragment
android:id="@+id/diagnoseClockFragment"
@ -584,4 +591,8 @@
android:name="childId"
app:argType="string" />
</fragment>
<fragment
android:id="@+id/diagnoseExitReasonFragment"
android:name="io.timelimit.android.ui.diagnose.exitreason.DiagnoseExitReasonFragment"
android:label="DiagnoseExitReasonFragment" />
</navigation>

View file

@ -530,6 +530,9 @@
<string name="diagnose_don_no_owner_toast">TimeLimit ist KEIN Geräte-Besitzer</string>
<string name="diagnose_don_not_supported_toast">Ihr System ermöglicht es nicht, einen Organisationsnamen zu hinterlegen</string>
<string name="diagnose_er_title">Gründe für die Beendung</string>
<string name="diagnose_er_empty">es liegen keine Daten vor; diese Daten sind nur bei Android 11 und neuer verfügbar</string>
<string name="dont_ask_password_on_device_title">Passwortabfrage</string>
<string name="dont_ask_password_on_device_text_enabled">
Die Passwortabfrage ist an diesem Gerät aktiviert.

View file

@ -583,6 +583,9 @@
<string name="diagnose_don_no_owner_toast">TimeLimit is NOT set as device owner</string>
<string name="diagnose_don_not_supported_toast">Your system does not support setting the organization name</string>
<string name="diagnose_er_title">Exit Reasons</string>
<string name="diagnose_er_empty">there is no data; this data is only available at Android 11 and newer</string>
<string name="dont_ask_password_on_device_title">Password prompts</string>
<string name="dont_ask_password_on_device_text_enabled">
Password prompts are enabled at the device.