mirror of
https://github.com/AntennaPod/AntennaPod.git
synced 2025-12-01 12:31:45 +00:00
Improve bug report screen (#8025)
This commit is contained in:
@ -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')
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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) {
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
@ -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>
|
||||
@ -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
14
system/build.gradle
Normal 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"
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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 */
|
||||
}
|
||||
}
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
@ -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 */
|
||||
}
|
||||
}
|
||||
@ -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 & 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 isn’t working. We’d 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 we’ll 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>
|
||||
|
||||
@ -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')
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
319
ui/preferences/src/main/res/layout/bug_report_fragment.xml
Normal file
319
ui/preferences/src/main/res/layout/bug_report_fragment.xml
Normal 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>
|
||||
@ -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"
|
||||
|
||||
Reference in New Issue
Block a user