Improve bug report screen (#8025)

This commit is contained in:
Shecks
2025-11-12 21:00:22 +00:00
committed by GitHub
parent 89cce3abc2
commit b0efc992a6
24 changed files with 968 additions and 238 deletions

View File

@ -76,6 +76,7 @@ dependencies {
implementation project(':storage:database-maintenance-service')
implementation project(':storage:importexport')
implementation project(':storage:preferences')
implementation project(':system')
implementation project(':ui:app-start-intent')
implementation project(':ui:common')
implementation project(':ui:discovery')

View File

@ -164,13 +164,6 @@
<data android:scheme="https"/>
</intent-filter>
</activity>
<activity
android:name=".ui.screen.preferences.BugReportActivity"
android:label="@string/bug_report_title">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="de.danoeh.antennapod.ui.screen.preferences.PreferenceActivity"/>
</activity>
<activity
android:name=".ui.screen.playback.video.VideoplayerActivity"

View File

@ -0,0 +1,20 @@
package de.danoeh.antennapod;
import androidx.annotation.NonNull;
import de.danoeh.antennapod.system.CrashReportWriter;
public class CrashReportExceptionHandler implements Thread.UncaughtExceptionHandler {
private final Thread.UncaughtExceptionHandler defaultUncaughtExceptionHandler;
public CrashReportExceptionHandler() {
defaultUncaughtExceptionHandler = Thread.getDefaultUncaughtExceptionHandler();
}
@Override
public void uncaughtException(@NonNull Thread thread, @NonNull Throwable throwable) {
CrashReportWriter.write(throwable);
defaultUncaughtExceptionHandler.uncaughtException(thread, throwable);
}
}

View File

@ -1,67 +0,0 @@
package de.danoeh.antennapod;
import android.os.Build;
import android.util.Log;
import de.danoeh.antennapod.BuildConfig;
import org.apache.commons.io.IOUtils;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import de.danoeh.antennapod.storage.preferences.UserPreferences;
public class CrashReportWriter implements Thread.UncaughtExceptionHandler {
private static final String TAG = "CrashReportWriter";
private final Thread.UncaughtExceptionHandler defaultHandler;
public CrashReportWriter() {
defaultHandler = Thread.getDefaultUncaughtExceptionHandler();
}
public static File getFile() {
return new File(UserPreferences.getDataFolder(null), "crash-report.log");
}
@Override
public void uncaughtException(Thread thread, Throwable ex) {
write(ex);
defaultHandler.uncaughtException(thread, ex);
}
public static void write(Throwable exception) {
File path = getFile();
PrintWriter out = null;
try {
out = new PrintWriter(path, "UTF-8");
out.println("## Crash info");
out.println("Time: " + new SimpleDateFormat("dd-MM-yyyy HH:mm:ss", Locale.getDefault()).format(new Date()));
out.println("AntennaPod version: " + BuildConfig.VERSION_NAME);
out.println();
out.println("## StackTrace");
out.println("```");
exception.printStackTrace(out);
out.println("```");
} catch (IOException e) {
Log.e(TAG, Log.getStackTraceString(e));
} finally {
IOUtils.closeQuietly(out);
}
}
public static String getSystemInfo() {
return "## Environment"
+ "\nAndroid version: " + Build.VERSION.RELEASE
+ "\nOS version: " + System.getProperty("os.version")
+ "\nAntennaPod version: " + BuildConfig.VERSION_NAME
+ "\nModel: " + Build.MODEL
+ "\nDevice: " + Build.DEVICE
+ "\nProduct: " + Build.PRODUCT;
}
}

View File

@ -16,7 +16,7 @@ public class PodcastApp extends Application {
@Override
public void onCreate() {
super.onCreate();
Thread.setDefaultUncaughtExceptionHandler(new CrashReportWriter());
Thread.setDefaultUncaughtExceptionHandler(new CrashReportExceptionHandler());
RxJavaErrorHandlerSetup.setupRxJavaErrorHandler();
if (BuildConfig.DEBUG) {

View File

@ -6,6 +6,7 @@ import android.view.KeyEvent;
import androidx.core.app.NotificationManagerCompat;
import androidx.preference.PreferenceManager;
import de.danoeh.antennapod.system.CrashReportWriter;
import de.danoeh.antennapod.net.download.serviceinterface.FeedUpdateManager;
import org.apache.commons.lang3.StringUtils;

View File

@ -1,6 +1,8 @@
package de.danoeh.antennapod;
import android.util.Log;
import de.danoeh.antennapod.system.CrashReportWriter;
import io.reactivex.rxjava3.exceptions.UndeliverableException;
import io.reactivex.rxjava3.plugins.RxJavaPlugins;

View File

@ -7,7 +7,8 @@ import android.os.Bundle;
import android.view.View;
import android.widget.Toast;
import androidx.annotation.Nullable;
import de.danoeh.antennapod.CrashReportWriter;
import de.danoeh.antennapod.system.CrashReportWriter;
import de.danoeh.antennapod.storage.database.PodDBAdapter;
import io.reactivex.rxjava3.core.Completable;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;

View File

@ -1,126 +0,0 @@
package de.danoeh.antennapod.ui.screen.preferences;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import com.google.android.material.snackbar.Snackbar;
import androidx.annotation.NonNull;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import androidx.core.app.ShareCompat;
import androidx.core.content.FileProvider;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.TextView;
import de.danoeh.antennapod.CrashReportWriter;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.storage.preferences.UserPreferences;
import de.danoeh.antennapod.ui.common.IntentUtils;
import de.danoeh.antennapod.ui.common.ToolbarActivity;
import org.apache.commons.io.IOUtils;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.charset.Charset;
/**
* Displays the 'crash report' screen
*/
public class BugReportActivity extends ToolbarActivity {
private static final String TAG = "BugReportActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getSupportActionBar().setDisplayShowHomeEnabled(true);
setContentView(R.layout.bug_report);
String stacktrace = "No crash report recorded";
try {
File crashFile = CrashReportWriter.getFile();
if (crashFile.exists()) {
stacktrace = IOUtils.toString(new FileInputStream(crashFile), Charset.forName("UTF-8"));
} else {
Log.d(TAG, stacktrace);
}
} catch (IOException e) {
e.printStackTrace();
}
TextView crashDetailsTextView = findViewById(R.id.crash_report_logs);
crashDetailsTextView.setText(CrashReportWriter.getSystemInfo() + "\n\n" + stacktrace);
findViewById(R.id.btn_open_bug_tracker).setOnClickListener(v -> IntentUtils.openInBrowser(
BugReportActivity.this, "https://github.com/AntennaPod/AntennaPod/issues"));
findViewById(R.id.btn_copy_log).setOnClickListener(v -> {
ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
ClipData clip = ClipData.newPlainText(getString(R.string.bug_report_title), crashDetailsTextView.getText());
clipboard.setPrimaryClip(clip);
if (Build.VERSION.SDK_INT < 32) {
Snackbar.make(findViewById(android.R.id.content), R.string.copied_to_clipboard,
Snackbar.LENGTH_SHORT).show();
}
});
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.bug_report_options, menu);
return super.onCreateOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
if (item.getItemId() == R.id.export_logcat) {
MaterialAlertDialogBuilder alertBuilder = new MaterialAlertDialogBuilder(this);
alertBuilder.setMessage(R.string.confirm_export_log_dialog_message);
alertBuilder.setPositiveButton(R.string.confirm_label, (dialog, which) -> {
exportLog();
dialog.dismiss();
});
alertBuilder.setNegativeButton(R.string.cancel_label, null);
alertBuilder.show();
return true;
}
return super.onOptionsItemSelected(item);
}
private void exportLog() {
try {
File filename = new File(UserPreferences.getDataFolder(null), "full-logs.txt");
String cmd = "logcat -d -f " + filename.getAbsolutePath();
Runtime.getRuntime().exec(cmd);
//share file
try {
String authority = getString(R.string.provider_authority);
Uri fileUri = FileProvider.getUriForFile(this, authority, filename);
new ShareCompat.IntentBuilder(this)
.setType("text/*")
.addStream(fileUri)
.setChooserTitle(R.string.share_file_label)
.startChooser();
} catch (Exception e) {
e.printStackTrace();
int strResId = R.string.log_file_share_exception;
Snackbar.make(findViewById(android.R.id.content), strResId, Snackbar.LENGTH_LONG)
.show();
}
} catch (IOException e) {
e.printStackTrace();
Snackbar.make(findViewById(android.R.id.content), e.getMessage(), Snackbar.LENGTH_LONG).show();
}
}
}

View File

@ -1,6 +1,5 @@
package de.danoeh.antennapod.ui.screen.preferences;
import android.content.Intent;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffColorFilter;
import android.os.Bundle;
@ -15,6 +14,7 @@ import de.danoeh.antennapod.R;
import de.danoeh.antennapod.ui.common.IntentUtils;
import de.danoeh.antennapod.ui.preferences.screen.AnimatedPreferenceFragment;
import de.danoeh.antennapod.ui.preferences.screen.about.AboutFragment;
import de.danoeh.antennapod.ui.preferences.screen.bugreport.BugReportFragment;
public class MainPreferencesFragment extends AnimatedPreferenceFragment {
@ -118,7 +118,9 @@ public class MainPreferencesFragment extends AnimatedPreferenceFragment {
return true;
});
findPreference(PREF_SEND_BUG_REPORT).setOnPreferenceClickListener(preference -> {
startActivity(new Intent(getActivity(), BugReportActivity.class));
getParentFragmentManager().beginTransaction()
.replace(R.id.settingsContainer, new BugReportFragment())
.addToBackStack(getString(R.string.report_bug_title)).commit();
return true;
});
}

View File

@ -1,30 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp">
<Button
android:id="@+id/btn_open_bug_tracker"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/open_bug_tracker" />
<Button
android:id="@+id/btn_copy_log"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/copy_to_clipboard" />
<TextView
android:id="@+id/crash_report_logs"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginTop="8dp"
android:textIsSelectable="true"
android:textSize="12sp"
android:layout_weight="1" />
</LinearLayout>

View File

@ -20,6 +20,7 @@ dependencyResolutionManagement {
include ':app'
include ':event'
include ':model'
include ':system'
include ':net:common'
include ':net:discovery'

14
system/build.gradle Normal file
View File

@ -0,0 +1,14 @@
plugins {
id 'com.android.library'
}
apply from: "../common.gradle"
android {
namespace "de.danoeh.antennapod.system"
}
dependencies {
implementation project(":storage:preferences")
implementation "commons-io:commons-io:$commonsioVersion"
}

View File

@ -0,0 +1,63 @@
package de.danoeh.antennapod.system;
import android.util.Log;
import org.apache.commons.io.IOUtils;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import de.danoeh.antennapod.storage.preferences.UserPreferences;
public class CrashReportWriter {
private static final String TAG = "CrashReportWriter";
public static File getFile() {
return new File(UserPreferences.getDataFolder(null), "crash-report.log");
}
public static void write(Throwable exception) {
File path = getFile();
PrintWriter out = null;
try {
out = new PrintWriter(path, "UTF-8");
exception.printStackTrace(out);
} catch (IOException e) {
Log.e(TAG, Log.getStackTraceString(e));
} finally {
IOUtils.closeQuietly(out);
}
}
public static Date getTimestamp() {
Date timestamp = null;
try {
File file = getFile();
if (file.exists()) {
timestamp = new Date(file.lastModified());
}
} catch (SecurityException e) {
Log.e(TAG, Log.getStackTraceString(e));
}
return timestamp;
}
public static String read() {
String content = "";
try {
File file = getFile();
if (file.exists()) {
try (FileInputStream fin = new FileInputStream(file)) {
content = IOUtils.toString(fin, StandardCharsets.UTF_8);
}
}
} catch (SecurityException | IOException e) {
Log.e(TAG, Log.getStackTraceString(e));
}
return content;
}
}

View File

@ -0,0 +1,34 @@
package de.danoeh.antennapod.system.utils;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.Objects;
/**
* Utilities for accessing the package information.
*/
public final class PackageUtils {
public static String getApplicationVersion(@NonNull Context context) {
return Objects.requireNonNull(getPackageInfo(context),
"Call to getPackageInfo() returned Null.").versionName;
}
@Nullable
public static PackageInfo getPackageInfo(@NonNull Context context) {
try {
return context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
} catch (PackageManager.NameNotFoundException e) {
return null;
}
}
private PackageUtils() {
/* Utility classes should not instantiated */
}
}

View File

@ -0,0 +1,20 @@
package de.danoeh.antennapod.ui.common;
import android.os.Bundle;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import com.google.android.material.transition.MaterialSharedAxis;
public abstract class AnimatedFragment extends Fragment {
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setEnterTransition(new MaterialSharedAxis(MaterialSharedAxis.X, true));
setReturnTransition(new MaterialSharedAxis(MaterialSharedAxis.X, false));
setExitTransition(new MaterialSharedAxis(MaterialSharedAxis.X, true));
setReenterTransition(new MaterialSharedAxis(MaterialSharedAxis.X, false));
}
}

View File

@ -0,0 +1,64 @@
package de.danoeh.antennapod.ui.common;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.os.Build;
import android.view.View;
import android.widget.TextView;
import androidx.annotation.StringRes;
import com.google.android.material.snackbar.Snackbar;
/**
* Utilities for commonly used clipboard functionality.
*/
public final class ClipboardUtils {
/**
* Utility function used to copy the text from the given TextView to the clipboard.
* @param textView TextView to copy the text from.
* @param labelId ID of string resource to used as label for the copied text within the clipboard.
*/
public static void copyText(TextView textView, @StringRes int labelId) {
copyText(textView, labelId, R.string.copied_to_clipboard);
}
/**
* Utility function used to copy the text from the given TextView to the clipboard.
* @param textView TextView to copy the text from.
* @param labelId ID of string resource to used as label for the copied text within the clipboard.
* @param messageId ID of string resource use to display confirmation to user on SDK versions prior to 32.
*/
public static void copyText(TextView textView, @StringRes int labelId, @StringRes int messageId) {
Context context = textView.getContext();
copyText(textView, context.getString(labelId), context.getString(messageId), textView.getText().toString());
}
/**
* Utility function used to copy the given text to the clipboard. The give View is used for context
* and to display a confirmation Toast to the user on completion where the SDK level is prior to 32.
* @param view View to copy the text from.
* @param labelId ID of string resource to use as label for the copied text within the clipboard buffer.
* @param text Text to copy to the clipboard.
*/
public static void copyText(View view, @StringRes int labelId, String text) {
Context context = view.getContext();
copyText(view, context.getString(labelId), context.getString(R.string.copied_to_clipboard), text);
}
private static void copyText(View view, String label, String message, String text) {
ClipboardManager clipboard = (ClipboardManager) view.getContext()
.getSystemService(Context.CLIPBOARD_SERVICE);
clipboard.setPrimaryClip(ClipData.newPlainText(label, text));
if (Build.VERSION.SDK_INT < 32) {
Snackbar.make(view, message, Snackbar.LENGTH_SHORT).show();
}
}
private ClipboardUtils() {
/* Utility classes should not instantiated */
}
}

View File

@ -3,6 +3,10 @@
xmlns:tools="http://schemas.android.com/tools"
tools:ignore="MissingTranslation">
<!-- Shared strings that are, or can, be shared throughout the UI -->
<string name="general_expand_button">Expand</string>
<string name="general_collapse_button">Collapse</string>
<!-- Activity and fragment titles -->
<string name="provider_authority" translatable="false">de.danoeh.antennapod.provider</string>
<string name="feed_update_receiver_name">Update subscriptions</string>
@ -546,8 +550,6 @@
<string name="pref_smart_mark_as_played_disabled">Disabled</string>
<string name="documentation_support">Documentation &amp; support</string>
<string name="visit_user_forum">User forum</string>
<string name="bug_report_title">Report bug</string>
<string name="open_bug_tracker">Open bug tracker</string>
<string name="copy_to_clipboard">Copy to clipboard</string>
<string name="copied_to_clipboard">Copied to clipboard</string>
<string name="pref_proxy_title">Proxy</string>
@ -581,6 +583,18 @@
<string name="pref_new_episodes_action_sum">Action to take for new episodes</string>
<string name="episode_information">Episode information</string>
<!-- Report bug -->
<string name="report_bug_title">Report bug</string>
<string name="report_bug_message">Sorry to hear something isnt working. Wed love your help to improve AntennaPod. Please check our forum or GitHub to see if the issue has already been reported. If not, let us know and well investigate.</string>
<string name="report_bug_device_info_title">Device</string>
<string name="report_bug_attrib_app_version">App version</string>
<string name="report_bug_attrib_android_version">Android version</string>
<string name="report_bug_attrib_device_name">Device Name</string>
<string name="report_bug_crash_log_title">Crash log</string>
<string name="report_bug_crash_log_message">Created: %1$s.</string>
<string name="report_bug_forum_title">Forum</string>
<string name="report_bug_github_title">GitHub</string>
<!-- About screen -->
<string name="about_pref">About</string>
<string name="antennapod_version">AntennaPod version</string>

View File

@ -28,6 +28,7 @@ dependencies {
implementation project(":net:sync:gpoddernet")
implementation project(":storage:preferences")
implementation project(":storage:importexport")
implementation project(':system')
implementation project(":ui:common")
implementation project(":ui:i18n")
implementation project(':net:sync:service-interface')

View File

@ -36,7 +36,7 @@ public class AboutFragment extends AnimatedPreferenceFragment {
"%s (%s)", versionName, BuildConfig.COMMIT_HASH));
findPreference("about_version").setOnPreferenceClickListener((preference) -> {
ClipboardManager clipboard = (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE);
ClipData clip = ClipData.newPlainText(getString(R.string.bug_report_title),
ClipData clip = ClipData.newPlainText(getString(R.string.about_pref),
findPreference("about_version").getSummary());
clipboard.setPrimaryClip(clip);
if (Build.VERSION.SDK_INT <= 32) {

View File

@ -0,0 +1,199 @@
package de.danoeh.antennapod.ui.preferences.screen.bugreport;
import android.net.Uri;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ShareCompat;
import androidx.core.content.FileProvider;
import androidx.core.view.MenuProvider;
import androidx.lifecycle.ViewModelProvider;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.android.material.snackbar.Snackbar;
import java.io.File;
import java.io.IOException;
import java.util.Objects;
import de.danoeh.antennapod.storage.preferences.UserPreferences;
import de.danoeh.antennapod.ui.common.AnimatedFragment;
import de.danoeh.antennapod.ui.common.ClipboardUtils;
import de.danoeh.antennapod.ui.common.IntentUtils;
import de.danoeh.antennapod.ui.preferences.R;
import de.danoeh.antennapod.ui.preferences.databinding.BugReportFragmentBinding;
/**
* UI fragment to allow the user to submit a bug report via the AntennaPod forum or GitHub page.
*/
public class BugReportFragment extends AnimatedFragment {
private BugReportFragmentBinding viewBinding;
private BugReportViewModel viewModel;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
viewModel = new ViewModelProvider(this).get(BugReportViewModel.class);
postponeEnterTransition();
}
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
viewBinding = BugReportFragmentBinding.inflate(inflater, container, false);
return viewBinding.getRoot();
}
@Override
public void onDestroyView() {
super.onDestroyView();
viewBinding = null;
}
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
setupContextMenu();
viewModel.getState().observe(getViewLifecycleOwner(), uiState -> {
refreshEnvironmentInfo(uiState.getEnvironmentInfo());
refreshCrashLogInfo(uiState);
startPostponedEnterTransition();
});
viewBinding.expandCrashLogButton.setOnClickListener(v -> {
switch (viewModel.requireCurrentState().getCrashLogState()) {
case SHOWN_COLLAPSED:
viewModel.setCrashLogState(BugReportViewModel.UiState.CrashLogState.SHOWN_EXPANDED);
break;
case SHOWN_EXPANDED:
viewModel.setCrashLogState(BugReportViewModel.UiState.CrashLogState.SHOWN_COLLAPSED);
break;
default: // UNAVAILABLE
break;
}
});
viewBinding.openForumButton.setOnClickListener(v ->
IntentUtils.openInBrowser(requireContext(), "https://forum.antennapod.org/search"));
viewBinding.openGithubButton.setOnClickListener(v ->
IntentUtils.openInBrowser(requireContext(), "https://github.com/AntennaPod/AntennaPod/issues"));
viewBinding.attribAppVersionLabel.setOnClickListener(v ->
ClipboardUtils.copyText((TextView) v, R.string.report_bug_attrib_app_version));
viewBinding.attribAndroidVersionLabel.setOnClickListener(v ->
ClipboardUtils.copyText((TextView) v, R.string.report_bug_attrib_android_version));
viewBinding.attribDeviceNameLabel.setOnClickListener(v ->
ClipboardUtils.copyText((TextView) v, R.string.report_bug_attrib_device_name));
viewBinding.crashLogContentText.setOnClickListener(v ->
ClipboardUtils.copyText(v, R.string.report_bug_title,
viewModel.requireCurrentState().getCrashInfoWithMarkup()));
viewBinding.copyToClipboardButton.setOnClickListener(v ->
ClipboardUtils.copyText(v, R.string.report_bug_title,
viewModel.requireCurrentState().getBugReportWithMarkup()));
}
@Override
public void onStart() {
super.onStart();
Objects.requireNonNull(((AppCompatActivity) requireActivity()).getSupportActionBar())
.setTitle(R.string.report_bug_title);
}
private void refreshEnvironmentInfo(@NonNull BugReportViewModel.EnvironmentInfo info) {
viewBinding.attribAppVersionLabel.setText(info.applicationVersion);
viewBinding.attribAndroidVersionLabel.setText(info.androidVersion);
viewBinding.attribDeviceNameLabel.setText(info.getFriendlyDeviceName());
}
private void refreshCrashLogInfo(@NonNull BugReportViewModel.UiState uiState) {
BugReportViewModel.UiState.CrashLogState state = uiState.getCrashLogState();
BugReportViewModel.CrashLogInfo crashLogInfo = uiState.getCrashLogInfo();
switch (state) {
case SHOWN_COLLAPSED:
case SHOWN_EXPANDED:
viewBinding.crashLogToggleGroup.setVisibility(View.VISIBLE);
viewBinding.crashLogContentText.setText(crashLogInfo.getContent());
viewBinding.crashLogMessageLabel.setText(getString(
R.string.report_bug_crash_log_message, uiState.getFormattedCrashLogTimestamp()));
if (state == BugReportViewModel.UiState.CrashLogState.SHOWN_COLLAPSED) {
viewBinding.expandCrashLogButton.setText(R.string.general_expand_button);
viewBinding.crashLogContentText.setMaxLines(4);
} else {
viewBinding.expandCrashLogButton.setText(R.string.general_collapse_button);
viewBinding.crashLogContentText.setMaxLines(Integer.MAX_VALUE);
}
break;
default: // UNAVAILABLE
viewBinding.crashLogToggleGroup.setVisibility(View.GONE);
break;
}
}
private void setupContextMenu() {
requireActivity().addMenuProvider(new MenuProvider() {
@Override
public void onCreateMenu(@NonNull Menu menu, @NonNull MenuInflater menuInflater) {
menuInflater.inflate(R.menu.bug_report_options, menu);
}
@Override
public boolean onMenuItemSelected(@NonNull MenuItem menuItem) {
if (menuItem.getItemId() == R.id.export_logcat) {
showExportLogcatDialog();
return true;
}
return false;
}
}, getViewLifecycleOwner());
}
private void showExportLogcatDialog() {
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(requireContext());
builder.setTitle(R.string.export_logs_menu_title);
builder.setMessage(R.string.confirm_export_log_dialog_message);
builder.setPositiveButton(R.string.confirm_label, (dialog, which) -> exportLogcat());
builder.setNegativeButton(R.string.cancel_label, null);
builder.show();
}
private void exportLogcat() {
try {
File filename = new File(UserPreferences.getDataFolder(null), "full-logs.txt");
String cmd = "logcat -d -f " + filename.getAbsolutePath();
Runtime.getRuntime().exec(cmd);
//share file
try {
String authority = getString(R.string.provider_authority);
Uri fileUri = FileProvider.getUriForFile(requireContext(), authority, filename);
new ShareCompat.IntentBuilder(requireContext())
.setType("text/*")
.addStream(fileUri)
.setChooserTitle(R.string.share_file_label)
.startChooser();
} catch (Exception e) {
e.printStackTrace();
Snackbar.make(viewBinding.getRoot(), R.string.log_file_share_exception, Snackbar.LENGTH_LONG).show();
}
} catch (IOException e) {
e.printStackTrace();
Snackbar.make(viewBinding.getRoot(), e.getMessage(), Snackbar.LENGTH_LONG).show();
}
}
}

View File

@ -0,0 +1,204 @@
package de.danoeh.antennapod.ui.preferences.screen.bugreport;
import android.app.Application;
import android.os.Build;
import android.text.format.DateUtils;
import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.Objects;
import de.danoeh.antennapod.system.CrashReportWriter;
import de.danoeh.antennapod.system.utils.PackageUtils;
import io.reactivex.rxjava3.core.Observable;
import io.reactivex.rxjava3.disposables.Disposable;
import io.reactivex.rxjava3.schedulers.Schedulers;
/**
* Viewmodel encapsulating all data and business logic required
* to present the report bug UI.
*/
public class BugReportViewModel extends AndroidViewModel {
/**
* Device runtime environment information
*/
public static class EnvironmentInfo {
String applicationVersion;
String androidVersion;
String androidOsVersion;
String deviceManufacturer;
String deviceModel;
String deviceName;
String productName;
private EnvironmentInfo(Application application) {
this.applicationVersion = PackageUtils.getApplicationVersion(application);
this.androidVersion = Build.VERSION.RELEASE;
this.androidOsVersion = System.getProperty("os.version");
this.deviceManufacturer = Build.MANUFACTURER;
this.deviceModel = Build.MODEL;
this.deviceName = Build.DEVICE;
this.productName = Build.PRODUCT;
}
public String getFriendlyDeviceName() {
if (Build.MODEL.toLowerCase(Locale.getDefault()).startsWith(Build.MANUFACTURER
.toLowerCase(Locale.getDefault()))) {
return Build.MODEL;
}
return Build.MANUFACTURER + " " + Build.MODEL;
}
}
/**
* Contents of the latest crash log / stacktrace file
*/
public static class CrashLogInfo {
private final Date timestamp;
private final String content;
private CrashLogInfo() {
this.timestamp = CrashReportWriter.getTimestamp();
this.content = CrashReportWriter.read();
}
public Date getTimestamp() {
return timestamp;
}
public String getContent() {
return content;
}
public Boolean isAvailable() {
return timestamp != null && !content.isEmpty();
}
}
/**
* Full UI state required by the report bug presentation layer
*/
public static class UiState {
public enum CrashLogState {
UNAVAILABLE,
SHOWN_COLLAPSED,
SHOWN_EXPANDED
}
private final EnvironmentInfo environmentInfo;
private final CrashLogInfo crashLogInfo;
private CrashLogState crashLogState;
private final String formattedEnvironmentInfo;
private String formattedCrashLogTimestamp;
private String formattedCrashLog;
private UiState(Application application) {
this.environmentInfo = new EnvironmentInfo(application);
this.crashLogInfo = new CrashLogInfo();
this.formattedEnvironmentInfo = "## Environment"
+ "\nAndroid version: " + environmentInfo.androidVersion
+ "\nOS version: " + environmentInfo.androidOsVersion
+ "\nAntennaPod version: " + environmentInfo.applicationVersion
+ "\nModel: " + environmentInfo.deviceModel
+ "\nDevice: " + environmentInfo.deviceName
+ "\nProduct: " + environmentInfo.productName
+ "\nManufacturer: " + environmentInfo.deviceManufacturer;
if (crashLogInfo.isAvailable()) {
this.formattedCrashLogTimestamp = DateUtils.formatDateTime(
application, crashLogInfo.timestamp.getTime(),
DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_TIME);
SimpleDateFormat df = new SimpleDateFormat("dd-MM-yyyy HH:mm:ss", Locale.getDefault());
this.formattedCrashLog = "## Crash info"
+ "\nTime: " + df.format(crashLogInfo.getTimestamp())
+ "\nAntennaPod version: " + environmentInfo.applicationVersion
+ "\n"
+ "\nStackTrace"
+ "\n```"
+ "\n" + crashLogInfo.getContent()
+ "\n```";
this.crashLogState = CrashLogState.SHOWN_COLLAPSED;
} else {
this.crashLogState = CrashLogState.UNAVAILABLE;
}
}
public EnvironmentInfo getEnvironmentInfo() {
return this.environmentInfo;
}
public CrashLogInfo getCrashLogInfo() {
return this.crashLogInfo;
}
public CrashLogState getCrashLogState() {
return this.crashLogState;
}
public String getFormattedCrashLogTimestamp() {
return this.formattedCrashLogTimestamp;
}
public String getBugReportWithMarkup() {
if (crashLogInfo.isAvailable()) {
return getEnvironmentInfoWithMarkup() + "\n\n" + getCrashInfoWithMarkup();
}
return formattedEnvironmentInfo;
}
public String getEnvironmentInfoWithMarkup() {
return formattedEnvironmentInfo;
}
public String getCrashInfoWithMarkup() {
return this.formattedCrashLog;
}
private void setCrashLogState(CrashLogState crashLogState) {
this.crashLogState = crashLogState;
}
}
private final MutableLiveData<UiState> uiState = new MutableLiveData<>();
private final Disposable disposable;
public BugReportViewModel(Application application) {
super(application);
// Does file I/O, so we have to use a background thread
this.disposable = Observable.fromCallable(() -> new UiState(application))
.subscribeOn(Schedulers.io())
.subscribe(this.uiState::postValue);
}
@Override
protected void onCleared() {
super.onCleared();
disposable.dispose();
}
public LiveData<UiState> getState() {
return uiState;
}
public UiState requireCurrentState() {
return Objects.requireNonNull(uiState.getValue(), "UiState is NULL!");
}
public void setCrashLogState(UiState.CrashLogState crashLogState) {
UiState currentUiState = uiState.getValue();
if (currentUiState != null) {
if (currentUiState.getCrashLogState() != crashLogState) {
currentUiState.setCrashLogState(crashLogState);
uiState.setValue(currentUiState);
}
}
}
}

View File

@ -0,0 +1,319 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
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="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:orientation="vertical">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/userMessageLabel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/report_bug_message"
android:layout_marginTop="8dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
style="@style/TextAppearance.Material3.BodyMedium"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<com.google.android.material.divider.MaterialDivider
android:id="@+id/topDivider"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
app:layout_constraintTop_toBottomOf="@+id/userMessageLabel"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<!-- Environment / Device Information -->
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/deviceInfoGroup"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_marginStart="8dp"
android:layout_marginTop="16dp"
app:layout_constraintTop_toBottomOf="@id/topDivider">
<ImageView
android:id="@+id/deviceInfoImage"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_info"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="ContentDescription" />
<TextView
android:id="@+id/deviceInformationLabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/report_bug_device_info_title"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
style="@style/TextAppearance.Material3.TitleMedium"
app:layout_constraintStart_toEndOf="@+id/deviceInfoImage"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/appVersionLabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/report_bug_attrib_app_version"
android:layout_marginTop="8dp"
android:layout_marginStart="16dp"
style="@style/TextAppearance.Material3.TitleSmall"
app:layout_constraintTop_toBottomOf="@+id/deviceInformationLabel"
app:layout_constraintStart_toEndOf="@+id/deviceInfoImage" />
<TextView
android:id="@+id/androidVersionLabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/report_bug_attrib_android_version"
android:layout_marginTop="8dp"
android:layout_marginStart="16dp"
style="@style/TextAppearance.Material3.TitleSmall"
app:layout_constraintTop_toBottomOf="@+id/appVersionLabel"
app:layout_constraintStart_toEndOf="@+id/deviceInfoImage" />
<TextView
android:id="@+id/deviceNameLabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/report_bug_attrib_device_name"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
style="@style/TextAppearance.Material3.TitleSmall"
app:layout_constraintStart_toEndOf="@+id/deviceInfoImage"
app:layout_constraintTop_toBottomOf="@+id/androidVersionLabel" />
<androidx.constraintlayout.widget.Barrier
android:id="@+id/reportDetailsBarrier"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="end"
app:constraint_referenced_ids="appVersionLabel, androidVersionLabel, deviceNameLabel" />
<TextView
android:id="@+id/attribAppVersionLabel"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:gravity="end"
android:paddingVertical="4dp"
android:layout_marginEnd="8dp"
android:layout_marginStart="8dp"
style="@style/TextAppearance.Material3.BodySmall"
app:layout_constraintHorizontal_weight="1"
tools:text="[v1.0.0]"
app:layout_constraintStart_toEndOf="@+id/reportDetailsBarrier"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/appVersionLabel"
app:layout_constraintBottom_toBottomOf="@+id/appVersionLabel" />
<TextView
android:id="@+id/attribAndroidVersionLabel"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:gravity="end"
android:paddingVertical="4dp"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
style="@style/TextAppearance.Material3.BodySmall"
app:layout_constraintHorizontal_weight="1"
tools:text="[16]"
app:layout_constraintStart_toEndOf="@+id/reportDetailsBarrier"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="@+id/androidVersionLabel"
app:layout_constraintTop_toTopOf="@+id/androidVersionLabel" />
<TextView
android:id="@+id/attribDeviceNameLabel"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:gravity="end"
android:paddingVertical="4dp"
android:layout_marginEnd="8dp"
android:layout_marginStart="8dp"
style="@style/TextAppearance.Material3.BodySmall"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_weight="1"
app:layout_constraintStart_toEndOf="@+id/reportDetailsBarrier"
tools:text="[Device Name]"
app:layout_constraintTop_toTopOf="@+id/deviceNameLabel"
app:layout_constraintBottom_toBottomOf="@+id/deviceNameLabel" />
</androidx.constraintlayout.widget.ConstraintLayout>
<!-- Crash Log Information (Optional) -->
<com.google.android.material.divider.MaterialDivider
android:id="@+id/middleDivider"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
app:layout_constraintTop_toBottomOf="@id/deviceInfoGroup"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/crashLogGroup"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
app:layout_constraintTop_toBottomOf="@id/middleDivider"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent">
<ImageView
android:id="@+id/crashLogImage"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_bug"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="ContentDescription" />
<TextView
android:id="@+id/crashLogTitleLabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/report_bug_crash_log_title"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
style="@style/TextAppearance.Material3.TitleMedium"
app:layout_constraintStart_toEndOf="@+id/crashLogImage"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/expandCrashLogButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/general_expand_button"
style="@style/Widget.MaterialComponents.Button.TextButton"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/crashLogTitleLabel"
app:layout_constraintBottom_toBottomOf="@+id/crashLogTitleLabel" />
<TextView
android:id="@+id/crashLogMessageLabel"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="8dp"
tools:text="@string/report_bug_crash_log_message"
app:layout_constraintTop_toBottomOf="@+id/crashLogTitleLabel"
app:layout_constraintStart_toEndOf="@+id/crashLogImage"
app:layout_constraintEnd_toEndOf="parent" />
<HorizontalScrollView
android:id="@+id/crashLogContentContainer"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:background="?attr/colorSurfaceContainer"
android:layout_marginTop="16dp"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:clickable="true"
android:layout_marginBottom="8dp"
android:scrollbars="none"
app:layout_constraintTop_toBottomOf="@+id/crashLogMessageLabel"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent">
<TextView
android:id="@+id/crashLogContentText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:maxLines="4"
android:minLines="4"
android:padding="8dp"
android:textSize="12sp"
android:typeface="monospace"
android:scrollbars="none"
tools:text="[The content of the latest crash log will be displayed here!]" />
</HorizontalScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.Group
android:id="@+id/crashLogToggleGroup"
android:layout_width="0dp"
android:layout_height="0dp"
android:visibility="gone"
tools:visibility="visible"
app:constraint_referenced_ids="middleDivider, crashLogGroup" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>
<!-- Buttons -->
<Button
android:id="@+id/copyToClipboardButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
android:layout_marginBottom="8dp"
android:text="@string/copy_to_clipboard"
style="@style/Widget.MaterialComponents.Button.TextButton" />
<com.google.android.material.divider.MaterialDivider
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="end"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:orientation="horizontal">
<Button
android:id="@+id/openGithubButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/report_bug_github_title"
android:layout_marginEnd="8dp"
android:textColor="?attr/colorPrimary"
style="?android:attr/borderlessButtonStyle" />
<Button
android:id="@+id/openForumButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/report_bug_forum_title"
android:layout_marginEnd="8dp"
tools:ignore="ButtonStyle" />
</LinearLayout>
</LinearLayout>

View File

@ -63,7 +63,7 @@
android:icon="@drawable/ic_contribute" />
<Preference
android:key="prefSendBugReport"
android:title="@string/bug_report_title"
android:title="@string/report_bug_title"
android:icon="@drawable/ic_bug" />
<Preference
android:key="prefAbout"