feat(support): Add contact support form and enhanced debug recorder

Add structured contact form with category selection (Question/Feature/Bug), description with word count validation, expected behavior field for bugs, and debug log picker with inline recording.

Enhance RecorderActivity with full-screen hero layout, file list, Share/Keep/Discard actions. Migrate debug logs from flat files to session directories in external files dir. Add DebugLogZipper, EmailTool attachment support, log folder size display, and clear stored logs.
This commit is contained in:
darken
2026-03-03 13:20:31 +01:00
committed by Matthias Urhahn
parent b3b55f1ca3
commit b609c1aced
25 changed files with 1667 additions and 215 deletions
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="?attr/colorSecondaryContainer" />
<corners android:radius="12dp" />
</shape>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M20,8h-2.81c-0.45,-0.78 -1.07,-1.45 -1.82,-1.96L17,4.41 15.59,3l-2.17,2.17C12.96,5.06 12.49,5 12,5c-0.49,0 -0.96,0.06 -1.41,0.17L8.41,3 7,4.41l1.62,1.63C7.88,6.55 7.26,7.22 6.81,8L4,8v2h2.09c-0.05,0.33 -0.09,0.66 -0.09,1v1L4,12v2h2v1c0,0.34 0.04,0.67 0.09,1L4,16v2h2.81c1.04,1.79 2.97,3 5.19,3s4.15,-1.21 5.19,-3L20,18v-2h-2.09c0.05,-0.33 0.09,-0.66 0.09,-1v-1h2v-2h-2v-1c0,-0.34 -0.04,-0.67 -0.09,-1L20,10L20,8zM14,16h-4v-2h4v2zM14,12h-4v-2h4v2z" />
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M11.5,2C6.81,2 3,5.81 3,10.5S6.81,19 11.5,19h0.5v3c4.86,-2.34 8,-7 8,-11.5C20,5.81 16.19,2 11.5,2zM12.5,16.5h-2v-2h2v2zM12.5,13h-2c0,-3.25 3,-3 3,-5 0,-1.1 -0.9,-2 -2,-2s-2,0.9 -2,2h-2c0,-2.21 1.79,-4 4,-4s4,1.79 4,4c0,2.5 -3,2.75 -3,5z" />
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M15,16h4v2h-4zM15,8h7v2h-7zM15,12h6v2h-6zM3,18c0,1.1 0.9,2 2,2h6c1.1,0 2,-0.9 2,-2V8H3v10zM14,5h-3.5l-1,-1h-5l-1,1H0v2h14V5z" />
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M14,2H6C4.9,2 4.01,2.9 4.01,4L4,20c0,1.1 0.89,2 1.99,2H18c1.1,0 2,-0.9 2,-2V8l-6,-6zM16,18H8v-2h8v2zM16,14H8v-2h8v2zM13,9V3.5L18.5,9H13z" />
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M10,4H4c-1.1,0 -1.99,0.9 -1.99,2L2,18c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2V8c0,-1.1 -0.9,-2 -2,-2h-8l-2,-2z" />
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M11,7h2v2h-2zM11,11h2v6h-2zM12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8z" />
</vector>
@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingVertical="8dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
android:id="@+id/file_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textAppearance="?textAppearanceBodyMedium"
tools:text="core.log" />
<TextView
android:id="@+id/file_size"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?textAppearanceBodySmall"
android:textColor="?colorOnSurfaceVariant"
tools:text="2.4 MB" />
</LinearLayout>
<TextView
android:id="@+id/file_path"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="?textAppearanceBodySmall"
android:textColor="?colorOnSurfaceVariant"
android:textIsSelectable="true"
tools:text="/storage/emulated/0/.../core.log" />
</LinearLayout>
@@ -1,119 +1,305 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView style="@style/MyCardView"
xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true">
<androidx.constraintlayout.widget.ConstraintLayout
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="88dp"
android:clipToPadding="false">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<!-- Hero section -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:orientation="vertical"
android:paddingTop="24dp"
android:paddingBottom="24dp">
<com.google.android.material.card.MaterialCardView
style="@style/Widget.Material3.CardView.Filled"
android:layout_width="64dp"
android:layout_height="64dp"
app:cardCornerRadius="16dp"
app:cardBackgroundColor="?colorPrimaryContainer">
<ImageView
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_gravity="center"
android:src="@drawable/ic_bug_report_24"
app:tint="?colorOnPrimaryContainer" />
</com.google.android.material.card.MaterialCardView>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/debug_debuglog_screen_title"
android:textAppearance="?textAppearanceHeadlineMedium"
android:textStyle="bold" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="@string/debug_debuglog_screen_subtitle"
android:textAppearance="?textAppearanceBodyMedium"
android:textColor="?colorOnSurfaceVariant" />
</LinearLayout>
<!-- Warning card -->
<com.google.android.material.card.MaterialCardView
style="@style/Widget.Material3.CardView.Filled"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
app:cardBackgroundColor="?colorSecondaryContainer">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="16dp">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginEnd="12dp"
android:src="@drawable/ic_info_outline_24"
app:tint="?colorOnSecondaryContainer" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/debug_debuglog_screen_sensitive_title"
android:textAppearance="?textAppearanceTitleSmall"
android:textColor="?colorOnSecondaryContainer"
android:textStyle="bold" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="@string/debug_debuglog_sensitive_information_message"
android:textAppearance="?textAppearanceBodySmall"
android:textColor="?colorOnSecondaryContainer" />
<com.google.android.material.button.MaterialButton
android:id="@+id/privacy_policy"
style="@style/Widget.Material3.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/settings_privacy_policy_label"
android:textColor="?colorOnSecondaryContainer" />
</LinearLayout>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<!-- Session Info card -->
<com.google.android.material.card.MaterialCardView
style="@style/Widget.Material3.CardView.Filled"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
app:cardBackgroundColor="?colorSurfaceContainer">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="16dp">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginEnd="12dp"
android:src="@drawable/ic_folder_24"
app:tint="?colorOnSurface" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/debug_debuglog_screen_session_path_label"
android:textAppearance="?textAppearanceTitleSmall" />
<com.google.android.material.card.MaterialCardView
style="@style/Widget.Material3.CardView.Filled"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
app:cardBackgroundColor="?colorSurfaceVariant"
app:cardCornerRadius="8dp">
<TextView
android:id="@+id/session_path"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fontFamily="monospace"
android:padding="12dp"
android:textAppearance="?textAppearanceBodySmall"
android:textIsSelectable="true"
tools:text="/storage/emulated/0/Android/data/.../debug/logs/capod_1.0_123_abc12345" />
</com.google.android.material.card.MaterialCardView>
</LinearLayout>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<!-- Log Files header card -->
<com.google.android.material.card.MaterialCardView
style="@style/Widget.Material3.CardView.Filled"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
app:cardBackgroundColor="?colorSurfaceContainer">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginEnd="12dp"
android:src="@drawable/ic_description_24"
app:tint="?colorOnSurface" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/debug_debuglog_screen_log_files_label"
android:textAppearance="?textAppearanceTitleSmall"
android:textStyle="bold" />
<TextView
android:id="@+id/file_count_badge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/badge_background"
android:paddingHorizontal="10dp"
android:paddingVertical="4dp"
android:textAppearance="?textAppearanceLabelSmall"
android:textColor="?colorOnSecondaryContainer"
tools:text="2" />
</LinearLayout>
<TextView
android:id="@+id/log_files_caption"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="36dp"
android:layout_marginTop="4dp"
android:textAppearance="?textAppearanceBodySmall"
android:textColor="?colorOnSurfaceVariant"
tools:text="2 files ready (ZIP: 745 KB)" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/log_files_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:nestedScrollingEnabled="false" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
<!-- Loading indicator -->
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/loading_indicator"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minWidth="320dp">
android:indeterminate="true"
android:visibility="gone" />
<TextView
android:id="@+id/label_path"
style="@style/TextAppearance.Material3.TitleSmall"
android:layout_width="wrap_content"
<!-- Bottom action bar -->
<com.google.android.material.card.MaterialCardView
android:id="@+id/bottom_bar"
style="@style/Widget.Material3.CardView.Elevated"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
app:cardCornerRadius="0dp"
app:cardElevation="8dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:text="@string/debug_debuglog_file_label"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="HardcodedText" />
android:orientation="horizontal"
android:padding="12dp">
<TextView
android:id="@+id/recording_path"
style="@style/TextAppearance.Material3.BodyMedium"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:layout_marginStart="16dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/label_path"
tools:text="/storage/emulated/0/Android/data/eu.darken.capod/cache/log_files/some_log_file_123124.log" />
<com.google.android.material.button.MaterialButton
android:id="@+id/action_discard"
style="@style/Widget.Material3.Button.OutlinedButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_weight="1"
android:text="@string/debug_debuglog_screen_discard_action" />
<TextView
android:id="@+id/label_size"
style="@style/TextAppearance.Material3.TitleSmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="32dp"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:text="@string/debug_debuglog_size_label"
app:layout_constraintEnd_toStartOf="@+id/label_compressed_size"
app:layout_constraintHorizontal_chainStyle="spread_inside"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/recording_path" />
<com.google.android.material.button.MaterialButton
android:id="@+id/action_keep"
style="@style/Widget.Material3.Button.TonalButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_weight="1"
android:text="@string/general_save_action" />
<TextView
android:id="@+id/recording_size"
style="@style/TextAppearance.Material3.BodyMedium"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="32dp"
app:layout_constraintEnd_toStartOf="@+id/recording_size_compressed"
app:layout_constraintHorizontal_chainStyle="spread_inside"
app:layout_constraintStart_toStartOf="@+id/label_size"
app:layout_constraintTop_toBottomOf="@+id/label_size"
tools:text="72 MB" />
<FrameLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1">
<TextView
android:id="@+id/label_compressed_size"
style="@style/TextAppearance.Material3.TitleSmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:layout_marginTop="16dp"
android:text="@string/debug_debuglog_size_compressed_label"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="1.0"
app:layout_constraintStart_toEndOf="@+id/label_size"
app:layout_constraintTop_toBottomOf="@+id/recording_path" />
<com.google.android.material.button.MaterialButton
android:id="@+id/action_share"
style="@style/Widget.Material3.Button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/general_share_action" />
<TextView
android:id="@+id/recording_size_compressed"
style="@style/TextAppearance.Material3.BodyMedium"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="@+id/label_compressed_size"
app:layout_constraintStart_toEndOf="@+id/recording_size"
app:layout_constraintTop_toBottomOf="@+id/label_compressed_size"
tools:text="745 KB" />
<com.google.android.material.progressindicator.CircularProgressIndicator
android:id="@+id/share_loading"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="center"
android:indeterminate="true"
android:visibility="gone" />
</FrameLayout>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<androidx.core.widget.ContentLoadingProgressBar
android:id="@+id/loading_indicator"
style="@style/Widget.Material3.CircularProgressIndicator.Medium"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="@+id/share"
app:layout_constraintEnd_toEndOf="@+id/share"
app:layout_constraintStart_toStartOf="@+id/share"
app:layout_constraintTop_toTopOf="@+id/share" />
<com.google.android.material.button.MaterialButton
android:id="@+id/share"
style="@style/Widget.Material3.Button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:layout_marginEnd="16dp"
android:layout_marginTop="16dp"
android:text="@string/general_share_action"
android:visibility="invisible"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/recording_size_compressed" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
+49
View File
@@ -308,5 +308,54 @@
<string name="general_discard_action">Discard</string>
<string name="general_keep_editing_action">Keep Editing</string>
<!-- Short recording warning -->
<string name="debug_debuglog_short_recording_title">Recording too short</string>
<string name="debug_debuglog_short_recording_message">A debug log needs to capture the issue while it happens. Keep recording, reproduce the problem, then stop the recording.</string>
<string name="debug_debuglog_short_recording_continue">Continue recording</string>
<string name="debug_debuglog_short_recording_stop">Stop anyway</string>
<!-- Recorder screen -->
<string name="debug_debuglog_screen_title">Debug Log</string>
<string name="debug_debuglog_screen_subtitle">Export debug information for troubleshooting</string>
<string name="debug_debuglog_screen_sensitive_title">Sensitive Information</string>
<string name="debug_debuglog_screen_session_path_label">Session Path</string>
<string name="debug_debuglog_screen_log_files_label">Log Files</string>
<plurals name="debug_debuglog_screen_log_files_ready">
<item quantity="one">%1$d file ready (%2$s)</item>
<item quantity="other">%1$d files ready (%2$s)</item>
</plurals>
<string name="debug_debuglog_screen_discard_action">Discard</string>
<!-- Log management -->
<string name="support_debuglog_folder_size">Debug logs (%s)</string>
<string name="support_debuglog_clear_action">Clear stored debug logs</string>
<!-- Contact form -->
<string name="support_contact_label">Contact developer</string>
<string name="support_contact_desc">Fill out a form to send the developer an email.</string>
<string name="support_contact_welcome">I read every message myself and do my best to reply. Since I handle development, bug fixes, and support on my own, it can take a little time. Thanks for your patience.</string>
<string name="support_contact_footer">Your message will be sent via email. Device and setup information is attached automatically. You can attach screenshots or a video to the email. Some parts of the email will be in English so the developer can easily understand it.</string>
<string name="support_contact_category_label">Category</string>
<string name="support_contact_category_question_label">Question</string>
<string name="support_contact_category_feature_label">Feature request</string>
<string name="support_contact_category_bug_label">Bug report</string>
<string name="support_contact_description_label">Description</string>
<string name="support_contact_description_question_hint">Describe your question in detail. Please be specific (minimum 20 words).</string>
<string name="support_contact_description_feature_hint">Describe the feature you would like to see. Please be specific (minimum 20 words).</string>
<string name="support_contact_description_bug_hint">Describe what happened and how to reproduce the issue. Please be specific (minimum 20 words).</string>
<string name="support_contact_expected_label">Expected behavior</string>
<string name="support_contact_expected_hint">Describe what you expected to happen (minimum 10 words).</string>
<plurals name="support_contact_word_count">
<item quantity="one">%1$d / %2$d word</item>
<item quantity="other">%1$d / %2$d words</item>
</plurals>
<string name="support_contact_debuglog_label">Debug log</string>
<string name="support_contact_debuglog_picker_hint">Attach an existing debug log, or record a new one.</string>
<string name="support_contact_debuglog_picker_empty">No debug logs found. Record one first.</string>
<string name="support_contact_debuglog_zip_error">Failed to compress debug log</string>
<string name="support_contact_send_action">Open email app</string>
<string name="support_contact_no_email_app">No email app found on this device.</string>
<string name="support_contact_debuglog_delete_title">Delete debug log?</string>
<string name="support_contact_debuglog_delete_message">This debug log will be permanently deleted.</string>
</resources>
@@ -3,4 +3,7 @@
<cache-path
name="debug_logs"
path="debug/logs" />
<external-files-path
name="debug_logs_external"
path="debug/logs" />
</paths>