Migrate crash reporter UI to Jetpack Compose

This commit is contained in:
MM20 2022-03-07 16:18:33 +01:00
parent b53aaeec4e
commit a20707fc3c
No known key found for this signature in database
GPG Key ID: 0B61A8F2DEAFA389
24 changed files with 456 additions and 705 deletions

View File

@ -1,24 +1,10 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="de.mm20.launcher2.crashreporter"> package="de.mm20.launcher2.crashreporter">
<application <application>
android:supportsRtl="true">
<provider <provider
android:name="com.balsikandar.crashreporter.CrashReporterInitProvider" android:name="com.balsikandar.crashreporter.CrashReporterInitProvider"
android:authorities="${applicationId}.CrashReporterInitProvider" android:authorities="${applicationId}.CrashReporterInitProvider"
android:enabled="true" android:enabled="true"
android:exported="false" /> android:exported="false" />
<activity
android:name="com.balsikandar.crashreporter.ui.CrashReporterActivity"
android:launchMode="singleTask"
android:excludeFromRecents="true"
android:taskAffinity="com.balsikandar.android.task"
android:theme="@style/CrashReporter.Theme" />
<activity
android:name="com.balsikandar.crashreporter.ui.LogMessageActivity"
android:parentActivityName="com.balsikandar.crashreporter.ui.CrashReporterActivity"
android:theme="@style/CrashReporter.Theme" />
</application> </application>
</manifest> </manifest>

View File

@ -3,7 +3,6 @@ package com.balsikandar.crashreporter;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import com.balsikandar.crashreporter.ui.CrashReporterActivity;
import com.balsikandar.crashreporter.utils.CrashReporterNotInitializedException; import com.balsikandar.crashreporter.utils.CrashReporterNotInitializedException;
import com.balsikandar.crashreporter.utils.CrashReporterExceptionHandler; import com.balsikandar.crashreporter.utils.CrashReporterExceptionHandler;
import com.balsikandar.crashreporter.utils.CrashUtil; import com.balsikandar.crashreporter.utils.CrashUtil;
@ -61,10 +60,6 @@ public class CrashReporter {
CrashUtil.logException(exception); CrashUtil.logException(exception);
} }
public static Intent getLaunchIntent() {
return new Intent(applicationContext, CrashReporterActivity.class).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
}
public static void disableNotification() { public static void disableNotification() {
isNotificationEnabled = false; isNotificationEnabled = false;
} }

View File

@ -1,81 +0,0 @@
package com.balsikandar.crashreporter.adapter;
import android.content.Context;
import android.content.Intent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.recyclerview.widget.RecyclerView;
import com.balsikandar.crashreporter.ui.LogMessageActivity;
import com.balsikandar.crashreporter.utils.FileUtils;
import java.io.File;
import java.util.ArrayList;
import de.mm20.launcher2.crashreporter.R;
/**
* Created by bali on 10/08/17.
*/
public class CrashLogAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private Context context;
private ArrayList<File> crashFileList;
public CrashLogAdapter(Context context, ArrayList<File> allCrashLogs) {
this.context = context;
crashFileList = allCrashLogs;
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(context).inflate(R.layout.custom_item, null);
return new CrashLogViewHolder(view);
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
((CrashLogViewHolder) holder).setUpViewHolder(context, crashFileList.get(position));
}
@Override
public int getItemCount() {
return crashFileList.size();
}
public void updateList(ArrayList<File> allCrashLogs) {
crashFileList = allCrashLogs;
notifyDataSetChanged();
}
private class CrashLogViewHolder extends RecyclerView.ViewHolder {
private TextView textViewMsg, messageLogTime;
CrashLogViewHolder(View itemView) {
super(itemView);
messageLogTime = itemView.findViewById(R.id.messageLogTime);
textViewMsg = itemView.findViewById(R.id.textViewMsg);
}
void setUpViewHolder(final Context context, final File file) {
final String filePath = file.getAbsolutePath();
messageLogTime.setText(file.getName().replaceAll("[a-zA-Z_.]", ""));
textViewMsg.setText(FileUtils.readFirstLineFromFile(new File(filePath)));
textViewMsg.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(context, LogMessageActivity.class);
intent.putExtra("LogMessage", filePath);
context.startActivity(intent);
}
});
}
}
}

View File

@ -1,50 +0,0 @@
package com.balsikandar.crashreporter.adapter;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentPagerAdapter;
import com.balsikandar.crashreporter.ui.CrashLogFragment;
import com.balsikandar.crashreporter.ui.ExceptionLogFragment;
/**
* Created by bali on 11/08/17.
*/
public class MainPagerAdapter extends FragmentPagerAdapter {
private CrashLogFragment crashLogFragment;
private ExceptionLogFragment exceptionLogFragment;
private String[] titles;
public MainPagerAdapter(FragmentManager fm, String[] titles) {
super(fm);
this.titles = titles;
}
@Override
public Fragment getItem(int position) {
if (position == 0) {
return crashLogFragment = new CrashLogFragment();
} else if (position == 1) {
return exceptionLogFragment = new ExceptionLogFragment();
} else {
return new CrashLogFragment();
}
}
@Override
public int getCount() {
return 2;
}
@Override
public CharSequence getPageTitle(int position) {
return titles[position];
}
public void clearLogs() {
crashLogFragment.clearLog();
exceptionLogFragment.clearLog();
}
}

View File

@ -1,95 +0,0 @@
package com.balsikandar.crashreporter.ui;
import android.content.Context;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.balsikandar.crashreporter.CrashReporter;
import com.balsikandar.crashreporter.adapter.CrashLogAdapter;
import com.balsikandar.crashreporter.utils.Constants;
import com.balsikandar.crashreporter.utils.CrashUtil;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import de.mm20.launcher2.crashreporter.R;
/**
* Created by bali on 11/08/17.
*/
public class CrashLogFragment extends Fragment {
private CrashLogAdapter logAdapter;
private RecyclerView crashRecyclerView;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.crash_log, container, false);
crashRecyclerView = (RecyclerView) view.findViewById(R.id.crashRecyclerView);
return view;
}
@Override
public void onResume() {
super.onResume();
loadAdapter(getActivity(), crashRecyclerView);
}
private void loadAdapter(Context context, RecyclerView crashRecyclerView) {
logAdapter = new CrashLogAdapter(context, getAllCrashes());
crashRecyclerView.setLayoutManager(new LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false));
crashRecyclerView.setAdapter(logAdapter);
}
public void clearLog() {
if (logAdapter != null) {
logAdapter.updateList(getAllCrashes());
}
}
private ArrayList<File> getAllCrashes() {
String directoryPath;
String crashReportPath = CrashReporter.getCrashReportPath();
if (TextUtils.isEmpty(crashReportPath)) {
directoryPath = CrashUtil.getDefaultPath();
} else {
directoryPath = crashReportPath;
}
File directory = new File(directoryPath);
if (!directory.exists() || !directory.isDirectory()) {
throw new RuntimeException("The path provided doesn't exists : " + directoryPath);
}
ArrayList<File> listOfFiles = new ArrayList<>(Arrays.asList(directory.listFiles()));
for (Iterator<File> iterator = listOfFiles.iterator(); iterator.hasNext(); ) {
if (iterator.next().getName().contains(Constants.EXCEPTION_SUFFIX)) {
iterator.remove();
}
}
Collections.sort(listOfFiles, Collections.reverseOrder());
return listOfFiles;
}
}

View File

@ -1,115 +0,0 @@
package com.balsikandar.crashreporter.ui;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.Menu;
import android.view.MenuItem;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import androidx.viewpager.widget.ViewPager;
import com.balsikandar.crashreporter.CrashReporter;
import com.balsikandar.crashreporter.adapter.MainPagerAdapter;
import com.balsikandar.crashreporter.utils.Constants;
import com.balsikandar.crashreporter.utils.CrashUtil;
import com.balsikandar.crashreporter.utils.FileUtils;
import com.balsikandar.crashreporter.utils.SimplePageChangeListener;
import com.google.android.material.tabs.TabLayout;
import java.io.File;
import de.mm20.launcher2.crashreporter.R;
public class CrashReporterActivity extends AppCompatActivity {
private MainPagerAdapter mainPagerAdapter;
private int selectedTabPosition = 0;
//region activity callbacks
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.log_main_menu, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == R.id.delete_crash_logs) {
clearCrashLog();
return true;
} else {
return super.onOptionsItemSelected(item);
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getTheme().applyStyle(R.style.DefaultColors, true);
setContentView(R.layout.crash_reporter_activity);
Toolbar toolbar = findViewById(R.id.toolbar);
toolbar.setTitle(getString(R.string.crash_reporter));
toolbar.setSubtitle(getApplicationName());
setSupportActionBar(toolbar);
ViewPager viewPager = findViewById(R.id.viewpager);
if (viewPager != null) {
setupViewPager(viewPager);
}
TabLayout tabLayout = findViewById(R.id.tabs);
tabLayout.setupWithViewPager(viewPager);
}
//endregion
private void clearCrashLog() {
new Thread(new Runnable() {
@Override
public void run() {
String crashReportPath = TextUtils.isEmpty(CrashReporter.getCrashReportPath()) ?
CrashUtil.getDefaultPath() : CrashReporter.getCrashReportPath();
File[] logs = new File(crashReportPath).listFiles();
for (File file : logs) {
FileUtils.delete(file);
}
runOnUiThread(new Runnable() {
@Override
public void run() {
mainPagerAdapter.clearLogs();
}
});
}
}).start();
}
private void setupViewPager(ViewPager viewPager) {
String[] titles = {getString(R.string.crashes), getString(R.string.exceptions)};
mainPagerAdapter = new MainPagerAdapter(getSupportFragmentManager(), titles);
viewPager.setAdapter(mainPagerAdapter);
viewPager.addOnPageChangeListener(new SimplePageChangeListener() {
@Override
public void onPageSelected(int position) {
selectedTabPosition = position;
}
});
Intent intent = getIntent();
if (intent != null && !intent.getBooleanExtra(Constants.LANDING, false)) {
selectedTabPosition = 1;
}
viewPager.setCurrentItem(selectedTabPosition);
}
private String getApplicationName() {
ApplicationInfo applicationInfo = getApplicationInfo();
int stringId = applicationInfo.labelRes;
return stringId == 0 ? applicationInfo.nonLocalizedLabel.toString() : getString(stringId);
}
}

View File

@ -1,96 +0,0 @@
package com.balsikandar.crashreporter.ui;
import android.content.Context;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.balsikandar.crashreporter.CrashReporter;
import com.balsikandar.crashreporter.adapter.CrashLogAdapter;
import com.balsikandar.crashreporter.utils.Constants;
import com.balsikandar.crashreporter.utils.CrashUtil;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import de.mm20.launcher2.crashreporter.R;
/**
* Created by bali on 11/08/17.
*/
public class ExceptionLogFragment extends Fragment {
private CrashLogAdapter logAdapter;
private RecyclerView exceptionRecyclerView;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.exception_log, container, false);
exceptionRecyclerView = (RecyclerView) view.findViewById(R.id.exceptionRecyclerView);
return view;
}
@Override
public void onResume() {
super.onResume();
loadAdapter(getActivity(), exceptionRecyclerView);
}
private void loadAdapter(Context context, RecyclerView exceptionRecyclerView) {
logAdapter = new CrashLogAdapter(context, getAllExceptions());
exceptionRecyclerView.setLayoutManager(new LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false));
exceptionRecyclerView.setAdapter(logAdapter);
}
public void clearLog() {
if (logAdapter != null) {
logAdapter.updateList(getAllExceptions());
}
}
public ArrayList<File> getAllExceptions() {
String directoryPath;
String crashReportPath = CrashReporter.getCrashReportPath();
if (TextUtils.isEmpty(crashReportPath)){
directoryPath = CrashUtil.getDefaultPath();
} else{
directoryPath = crashReportPath;
}
File directory = new File(directoryPath);
if (!directory.exists() || !directory.isDirectory()){
throw new RuntimeException("The path provided doesn't exists : " + directoryPath);
}
ArrayList<File> listOfFiles = new ArrayList<>(Arrays.asList(directory.listFiles()));
for (Iterator<File> iterator = listOfFiles.iterator(); iterator.hasNext(); ) {
if (iterator.next().getName().contains(Constants.CRASH_SUFFIX)) {
iterator.remove();
}
}
Collections.sort(listOfFiles, Collections.reverseOrder());
return listOfFiles;
}
}

View File

@ -1,90 +0,0 @@
package com.balsikandar.crashreporter.ui;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import androidx.core.content.FileProvider;
import com.balsikandar.crashreporter.utils.AppUtils;
import com.balsikandar.crashreporter.utils.FileUtils;
import java.io.File;
import de.mm20.launcher2.crashreporter.R;
public class LogMessageActivity extends AppCompatActivity {
private TextView appInfo;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_log_message);
appInfo = findViewById(R.id.appInfo);
Intent intent = getIntent();
if (intent != null) {
String dirPath = intent.getStringExtra("LogMessage");
File file = new File(dirPath);
String crashLog = FileUtils.readFromFile(file);
TextView textView = findViewById(R.id.logMessage);
textView.setText(crashLog);
}
Toolbar myToolbar = findViewById(R.id.toolbar);
myToolbar.setTitle(getString(R.string.crash_reporter));
setSupportActionBar(myToolbar);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getAppInfo();
}
private void getAppInfo() {
appInfo.setText(AppUtils.getDeviceDetails(this));
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.crash_detail_menu, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
Intent intent = getIntent();
String filePath = null;
if (intent != null) {
filePath = intent.getStringExtra("LogMessage");
}
if (item.getItemId() == R.id.delete_log) {
if (FileUtils.delete(filePath)) {
finish();
}
return true;
} else if (item.getItemId() == R.id.share_crash_log) {
shareCrashReport(filePath);
return true;
} else {
return super.onOptionsItemSelected(item);
}
}
private void shareCrashReport(String filePath) {
Uri uri = FileProvider.getUriForFile(this,
this.getApplicationContext().getPackageName() + ".fileprovider",
new File(filePath));
Intent intent = new Intent(Intent.ACTION_SEND);
intent.setType("*/*");
intent.putExtra(Intent.EXTRA_TEXT, appInfo.getText().toString());
intent.putExtra(Intent.EXTRA_STREAM, uri);
startActivity(Intent.createChooser(intent, "Share via"));
}
}

View File

@ -1,8 +1,5 @@
package com.balsikandar.crashreporter.utils; package com.balsikandar.crashreporter.utils;
import android.Manifest;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.pm.PackageInfo; import android.content.pm.PackageInfo;
@ -11,8 +8,6 @@ import android.content.pm.ResolveInfo;
import android.os.Build; import android.os.Build;
import android.util.Log; import android.util.Log;
import androidx.core.app.ActivityCompat;
import java.util.TimeZone; import java.util.TimeZone;
import java.util.UUID; import java.util.UUID;
@ -40,9 +35,8 @@ public class AppUtils {
public static String getDeviceDetails(Context context) { public static String getDeviceDetails(Context context) {
return "Device Information\n" return "APP.VERSION : " + getAppVersion(context)
+ "\nDEVICE.ID : " + getDeviceId(context) + "\nAPP.VERSIONCODE : " + getAppVersionCode(context)
+ "\nAPP.VERSION : " + getAppVersion(context)
+ "\nLAUNCHER.APP : " + getCurrentLauncherApp(context) + "\nLAUNCHER.APP : " + getCurrentLauncherApp(context)
+ "\nTIMEZONE : " + timeZone() + "\nTIMEZONE : " + timeZone()
+ "\nVERSION.RELEASE : " + Build.VERSION.RELEASE + "\nVERSION.RELEASE : " + Build.VERSION.RELEASE
@ -61,12 +55,9 @@ public class AppUtils {
+ "\nMANUFACTURER : " + Build.MANUFACTURER + "\nMANUFACTURER : " + Build.MANUFACTURER
+ "\nMODEL : " + Build.MODEL + "\nMODEL : " + Build.MODEL
+ "\nPRODUCT : " + Build.PRODUCT + "\nPRODUCT : " + Build.PRODUCT
+ "\nSERIAL : " + Build.SERIAL
+ "\nTAGS : " + Build.TAGS + "\nTAGS : " + Build.TAGS
+ "\nTIME : " + Build.TIME + "\nTIME : " + Build.TIME
+ "\nTYPE : " + Build.TYPE + "\nTYPE : " + Build.TYPE;
+ "\nUNKNOWN : " + Build.UNKNOWN
+ "\nUSER : " + Build.USER;
} }
private static String timeZone() { private static String timeZone() {
@ -94,7 +85,7 @@ public class AppUtils {
return androidId; return androidId;
} }
private static int getAppVersion(Context context) { private static int getAppVersionCode(Context context) {
try { try {
PackageInfo packageInfo = context.getPackageManager() PackageInfo packageInfo = context.getPackageManager()
.getPackageInfo(context.getPackageName(), 0); .getPackageInfo(context.getPackageName(), 0);
@ -103,4 +94,14 @@ public class AppUtils {
throw new RuntimeException("Could not get package name: " + e); throw new RuntimeException("Could not get package name: " + e);
} }
} }
private static String getAppVersion(Context context) {
try {
PackageInfo packageInfo = context.getPackageManager()
.getPackageInfo(context.getPackageName(), 0);
return packageInfo.versionName;
} catch (PackageManager.NameNotFoundException e) {
throw new RuntimeException("Could not get package name: " + e);
}
}
} }

View File

@ -3,28 +3,30 @@ package com.balsikandar.crashreporter.utils;
import android.app.NotificationChannel; import android.app.NotificationChannel;
import android.app.NotificationManager; import android.app.NotificationManager;
import android.app.PendingIntent; import android.app.PendingIntent;
import android.content.ComponentName;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.os.Build;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.Log; import android.util.Log;
import androidx.core.app.NotificationCompat;
import androidx.core.content.ContextCompat;
import com.balsikandar.crashreporter.CrashReporter; import com.balsikandar.crashreporter.CrashReporter;
import de.mm20.launcher2.crashreporter.R;
import java.io.BufferedWriter; import java.io.BufferedWriter;
import java.io.File; import java.io.File;
import java.io.FileWriter; import java.io.FileWriter;
import java.io.PrintWriter; import java.io.PrintWriter;
import java.io.StringWriter; import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.io.Writer; import java.io.Writer;
import java.net.URLEncoder;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.Date; import java.util.Date;
import java.util.Locale; import java.util.Locale;
import androidx.core.app.NotificationCompat;
import androidx.core.content.ContextCompat;
import de.mm20.launcher2.crashreporter.R;
import static android.content.Context.NOTIFICATION_SERVICE; import static android.content.Context.NOTIFICATION_SERVICE;
import static com.balsikandar.crashreporter.utils.Constants.CHANNEL_NOTIFICATION_ID; import static com.balsikandar.crashreporter.utils.Constants.CHANNEL_NOTIFICATION_ID;
@ -47,7 +49,13 @@ public class CrashUtil {
String filename = getCrashLogTime() + Constants.CRASH_SUFFIX + Constants.FILE_EXTENSION; String filename = getCrashLogTime() + Constants.CRASH_SUFFIX + Constants.FILE_EXTENSION;
writeToFile(crashReportPath, filename, getStackTrace(throwable)); writeToFile(crashReportPath, filename, getStackTrace(throwable));
showNotification(throwable.getLocalizedMessage(), true); //if (crashReportPath.isEmpty()) crashReportPath = getDefaultPath();
try {
showNotification(throwable.getLocalizedMessage(), filename);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
} }
public static void logException(final Exception exception) { public static void logException(final Exception exception) {
@ -91,7 +99,7 @@ public class CrashUtil {
} }
} }
private static void showNotification(String localisedMsg, boolean isCrash) { private static void showNotification(String localisedMsg, String fileName) throws UnsupportedEncodingException {
if (CrashReporter.isNotificationEnabled()) { if (CrashReporter.isNotificationEnabled()) {
Context context = CrashReporter.getContext(); Context context = CrashReporter.getContext();
@ -101,8 +109,11 @@ public class CrashUtil {
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, CHANNEL_NOTIFICATION_ID); NotificationCompat.Builder builder = new NotificationCompat.Builder(context, CHANNEL_NOTIFICATION_ID);
builder.setSmallIcon(R.drawable.ic_warning_black_24dp); builder.setSmallIcon(R.drawable.ic_warning_black_24dp);
Intent intent = CrashReporter.getLaunchIntent(); String filePath = new File(getDefaultPath(), fileName).getAbsolutePath();
intent.putExtra(Constants.LANDING, isCrash);
Intent intent = new Intent();
intent.setComponent(new ComponentName(context.getPackageName(), "de.mm20.launcher2.ui.settings.SettingsActivity"));
intent.putExtra("de.mm20.launcher2.settings.ROUTE", "settings/debug/crashreporter/report?fileName=" + URLEncoder.encode(filePath, "utf8"));
intent.setAction(Long.toString(System.currentTimeMillis())); intent.setAction(Long.toString(System.currentTimeMillis()));
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE); PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE);

View File

@ -0,0 +1,48 @@
package de.mm20.launcher2.crashreporter
import android.icu.text.SimpleDateFormat
import android.icu.util.TimeZone
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.File
import java.util.*
class CrashReport(
val type: CrashReportType,
val time: Date,
val summary: String,
val stacktrace: String?,
val filePath: String
) {
companion object {
suspend fun fromFile(file: File, loadStackTrace: Boolean): CrashReport {
val df = SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
val time = df.parse(file.name.replace("[a-zA-Z_.]", ""))
val content = if (loadStackTrace) {
withContext(Dispatchers.IO) {
file.inputStream().bufferedReader().use {
it.readText()
}
}
} else null
val summary = content?.substringBefore("\n")
?: withContext(Dispatchers.IO) {
file.inputStream().bufferedReader().use {
it.readLine()
}
}
return CrashReport(
type = if (file.name.endsWith("_crash.txt")) CrashReportType.Crash else CrashReportType.Exception,
time = time,
summary = summary,
stacktrace = content,
filePath = file.absolutePath
)
}
}
}
enum class CrashReportType {
Exception,
Crash
}

View File

@ -1,8 +1,15 @@
package de.mm20.launcher2.crashreporter package de.mm20.launcher2.crashreporter
import android.content.Context
import android.content.Intent import android.content.Intent
import android.util.Log import android.util.Log
import com.balsikandar.crashreporter.CrashReporter
import com.balsikandar.crashreporter.utils.AppUtils
import com.balsikandar.crashreporter.utils.CrashUtil
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.File
object CrashReporter { object CrashReporter {
fun logException(e: Exception) { fun logException(e: Exception) {
@ -12,7 +19,23 @@ object CrashReporter {
Log.e("MM20", Log.getStackTraceString(e)) Log.e("MM20", Log.getStackTraceString(e))
} }
fun getLaunchIntent(): Intent { suspend fun getCrashReports(): List<CrashReport> {
return com.balsikandar.crashreporter.CrashReporter.getLaunchIntent() val files = withContext(Dispatchers.IO) {
val now = System.currentTimeMillis()
val path = CrashReporter.getCrashReportPath()?.takeIf { it.isEmpty() } ?: CrashUtil.getDefaultPath()
File(path).listFiles { f ->
f.lastModified() > now - 7 * 24 * 60 * 60 * 1000L
}?.sortedByDescending { it.lastModified() }
}
return files?.map { CrashReport.fromFile(it, false) } ?: emptyList()
}
suspend fun getCrashReport(filePath: String): CrashReport {
val path = CrashReporter.getCrashReportPath()?.takeIf { it.isEmpty() } ?: CrashUtil.getDefaultPath()
return CrashReport.fromFile(File(filePath), true)
}
fun getDeviceInformation(context: Context): String {
return AppUtils.getDeviceDetails(context)
} }
} }

View File

@ -1,45 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:context="com.balsikandar.crashreporter.ui.LogMessageActivity">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize" />
</com.google.android.material.appbar.AppBarLayout>
<HorizontalScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/logMessage"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="10dp"
android:textColor="?colorAccent" />
</HorizontalScrollView>
<TextView
android:id="@+id/appInfo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:padding="10dp"
android:textColor="?android:textColorPrimary" />
</LinearLayout>
</ScrollView>

View File

@ -1,4 +0,0 @@
<androidx.recyclerview.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/crashRecyclerView"
android:layout_width="match_parent"
android:layout_height="wrap_content" />

View File

@ -1,34 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 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"
tools:context="com.balsikandar.crashreporter.ui.CrashReporterActivity">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<com.google.android.material.tabs.TabLayout
android:id="@+id/tabs"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.viewpager.widget.ViewPager
android:id="@+id/viewpager"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@+id/appbar"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
</RelativeLayout>

View File

@ -1,41 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="@+id/messageLogTime"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:padding="5dp"
android:layout_marginLeft="10dp"
android:layout_marginStart="10dp"
android:layout_marginRight="10dp"
android:layout_marginEnd="10dp"
android:textColor="?android:textColorPrimary"
android:textSize="16sp" />
<TextView
android:id="@+id/textViewMsg"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@+id/messageLogTime"
android:maxLines="4"
android:orientation="vertical"
android:padding="5dp"
android:layout_marginLeft="10dp"
android:layout_marginStart="10dp"
android:layout_marginRight="10dp"
android:layout_marginEnd="10dp"
android:textColor="?android:textColorSecondary"
android:textSize="14sp" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_below="@+id/textViewMsg"
android:layout_marginTop="3dp"
android:background="#dcdada" />
</RelativeLayout>

View File

@ -1,4 +0,0 @@
<androidx.recyclerview.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/exceptionRecyclerView"
android:layout_width="match_parent"
android:layout_height="wrap_content" />

View File

@ -2,6 +2,7 @@ package de.mm20.launcher2.ui.component.preferences
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
@ -23,7 +24,8 @@ import de.mm20.launcher2.ui.locals.LocalNavController
fun PreferenceScreen( fun PreferenceScreen(
title: String, title: String,
floatingActionButton: @Composable () -> Unit = {}, floatingActionButton: @Composable () -> Unit = {},
content: LazyListScope.() -> Unit topBarActions: @Composable RowScope.() -> Unit = {},
content: LazyListScope.() -> Unit,
) { ) {
val navController = LocalNavController.current val navController = LocalNavController.current
val systemUiController = rememberSystemUiController() val systemUiController = rememberSystemUiController()
@ -50,6 +52,7 @@ fun PreferenceScreen(
Icon(imageVector = Icons.Rounded.ArrowBack, contentDescription = "Back") Icon(imageVector = Icons.Rounded.ArrowBack, contentDescription = "Back")
} }
}, },
actions = topBarActions
) )
}) { }) {
LazyColumn( LazyColumn(

View File

@ -6,10 +6,7 @@ import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut import androidx.compose.animation.fadeOut
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.*
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.navigation.navArgument import androidx.navigation.navArgument
import com.google.accompanist.navigation.animation.AnimatedNavHost import com.google.accompanist.navigation.animation.AnimatedNavHost
import com.google.accompanist.navigation.animation.composable import com.google.accompanist.navigation.animation.composable
@ -30,6 +27,8 @@ import de.mm20.launcher2.ui.settings.buildinfo.BuildInfoSettingsScreen
import de.mm20.launcher2.ui.settings.calendarwidget.CalendarWidgetSettingsScreen import de.mm20.launcher2.ui.settings.calendarwidget.CalendarWidgetSettingsScreen
import de.mm20.launcher2.ui.settings.cards.CardsSettingsScreen import de.mm20.launcher2.ui.settings.cards.CardsSettingsScreen
import de.mm20.launcher2.ui.settings.clockwidget.ClockWidgetSettingsScreen import de.mm20.launcher2.ui.settings.clockwidget.ClockWidgetSettingsScreen
import de.mm20.launcher2.ui.settings.crashreporter.CrashReportScreen
import de.mm20.launcher2.ui.settings.crashreporter.CrashReporterScreen
import de.mm20.launcher2.ui.settings.debug.DebugSettingsScreen import de.mm20.launcher2.ui.settings.debug.DebugSettingsScreen
import de.mm20.launcher2.ui.settings.easteregg.EasterEggSettingsScreen import de.mm20.launcher2.ui.settings.easteregg.EasterEggSettingsScreen
import de.mm20.launcher2.ui.settings.filesearch.FileSearchSettingsScreen import de.mm20.launcher2.ui.settings.filesearch.FileSearchSettingsScreen
@ -55,6 +54,12 @@ class SettingsActivity : BaseActivity() {
setContent { setContent {
val navController = rememberAnimatedNavController() val navController = rememberAnimatedNavController()
LaunchedEffect(intent) {
intent.getStringExtra("de.mm20.launcher2.settings.ROUTE")
?.let { navController.navigate(it) }
}
val cardStyle by remember { val cardStyle by remember {
dataStore.data.map { it.cards }.distinctUntilChanged() dataStore.data.map { it.cards }.distinctUntilChanged()
}.collectAsState( }.collectAsState(
@ -127,6 +132,17 @@ class SettingsActivity : BaseActivity() {
composable("settings/debug") { composable("settings/debug") {
DebugSettingsScreen() DebugSettingsScreen()
} }
composable("settings/debug/crashreporter") {
CrashReporterScreen()
}
composable("settings/debug/crashreporter/report?fileName={fileName}",
arguments = listOf(navArgument("fileName") {
nullable = false
})
) {
val fileName = it.arguments?.getString("fileName")
CrashReportScreen(fileName!!)
}
composable( composable(
"settings/license?library={libraryName}", "settings/license?library={libraryName}",
arguments = listOf(navArgument("libraryName") { arguments = listOf(navArgument("libraryName") {

View File

@ -0,0 +1,92 @@
package de.mm20.launcher2.ui.settings.crashreporter
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.BugReport
import androidx.compose.material.icons.rounded.Share
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.crashreporter.CrashReportType
import de.mm20.launcher2.ui.component.preferences.PreferenceScreen
@Composable
fun CrashReportScreen(fileName: String) {
val viewModel: CrashReportScreenVM = viewModel()
val context = LocalContext.current
val crashReport by remember(fileName) { viewModel.getCrashReport(fileName) }.observeAsState()
PreferenceScreen(
title = when (crashReport?.type) {
CrashReportType.Exception -> "Exception"
CrashReportType.Crash -> "Crash"
null -> ""
},
topBarActions = {
IconButton(onClick = { crashReport?.let { viewModel.shareCrashReport(context, it) } }) {
Icon(imageVector = Icons.Rounded.Share, contentDescription = null)
}
if (crashReport?.type == CrashReportType.Crash) {
IconButton(onClick = { crashReport?.let { viewModel.createIssue(context, it) } }) {
Icon(imageVector = Icons.Rounded.BugReport, contentDescription = null)
}
}
}
) {
item {
Surface(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp),
color = if (crashReport?.type == CrashReportType.Crash) {
MaterialTheme.colorScheme.errorContainer
} else {
MaterialTheme.colorScheme.primaryContainer
},
shape = RoundedCornerShape(8.dp)
) {
Box(
modifier = Modifier
.fillMaxWidth()
.horizontalScroll(
rememberScrollState()
),
) {
crashReport?.stacktrace?.let {
Text(
text = it,
modifier = Modifier.padding(16.dp),
style = MaterialTheme.typography.bodySmall
)
}
}
}
}
item {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(12.dp)
) {
Text(text = "Device Information", style = MaterialTheme.typography.titleMedium)
val deviceInformation = remember { viewModel.getDeviceInformation(context) }
Text(
text = deviceInformation,
style = MaterialTheme.typography.bodySmall,
modifier = Modifier.padding(top = 16.dp, bottom = 8.dp)
)
}
}
}
}

View File

@ -0,0 +1,63 @@
package de.mm20.launcher2.ui.settings.crashreporter
import android.content.Context
import android.content.Intent
import android.net.Uri
import androidx.core.content.FileProvider
import androidx.lifecycle.ViewModel
import androidx.lifecycle.liveData
import de.mm20.launcher2.crashreporter.CrashReport
import de.mm20.launcher2.crashreporter.CrashReporter
import java.io.File
import java.net.URLEncoder
class CrashReportScreenVM : ViewModel() {
fun getCrashReport(fileName: String) = liveData<CrashReport?> {
emit(CrashReporter.getCrashReport(fileName))
}
fun getDeviceInformation(context: Context): String {
return CrashReporter.getDeviceInformation(context)
}
fun createIssue(context: Context, crashReport: CrashReport) {
val stacktrace = crashReport.stacktrace?.lines()?.let {
if (it.size > 15) it.subList(0, 15)
.joinToString("\n") + "\n[${it.size - 15} lines truncated]"
else it.joinToString("\n")
} ?: ""
val body =
"## Description\n\n" +
"*Please provide as many information about the crash as possible (What did you do before the crash happened? Steps to reproduce?)*\n\n" +
"## Strack trace\n\n" +
"```\n" +
"${stacktrace}\n" +
"```\n\n" +
"## Device info\n" +
"${getDeviceInformation(context).replace("\n", "<br>")}\n"
val url = "https://github.com/MM2-0/Kvaesitso/issues/new?labels=crash+report&body=${
URLEncoder.encode(
body,
"utf8"
)
}"
context.startActivity(Intent(Intent.ACTION_VIEW).apply {
data = Uri.parse(url)
})
}
fun shareCrashReport(context: Context, crashReport: CrashReport) {
val uri = FileProvider.getUriForFile(
context,
context.applicationContext.packageName + ".fileprovider",
File(crashReport.filePath)
)
val intent = Intent(Intent.ACTION_SEND)
intent.type = "*/*"
intent.putExtra(Intent.EXTRA_TEXT, CrashReporter.getDeviceInformation(context))
intent.putExtra(Intent.EXTRA_STREAM, uri)
context.startActivity(Intent.createChooser(intent, "Share via"))
}
}

View File

@ -0,0 +1,121 @@
package de.mm20.launcher2.ui.settings.crashreporter
import android.text.format.DateUtils
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Error
import androidx.compose.material.icons.rounded.ErrorOutline
import androidx.compose.material.icons.rounded.Warning
import androidx.compose.material.icons.rounded.WarningAmber
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.crashreporter.CrashReportType
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.preferences.PreferenceScreen
import de.mm20.launcher2.ui.locals.LocalNavController
import java.net.URLEncoder
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun CrashReporterScreen() {
val viewModel: CrashReporterScreenVM = viewModel()
val navController = LocalNavController.current
val reports by viewModel.reports.observeAsState()
val showExceptions by viewModel.showExceptions.observeAsState(true)
val showCrashes by viewModel.showCrashes.observeAsState(true)
PreferenceScreen(title = stringResource(R.string.preference_crash_reporter)) {
reports?.let {
item {
Row(
modifier = Modifier.fillMaxWidth().padding(8.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.End
) {
IconToggleButton(checked = showExceptions, onCheckedChange = { value ->
viewModel.setShowExceptions(value)
}) {
Icon(
imageVector = if (showExceptions) Icons.Rounded.Warning else Icons.Rounded.WarningAmber,
contentDescription = null,
modifier = Modifier.alpha(if (showExceptions) 1f else 0.5f)
)
}
IconToggleButton(checked = showCrashes, onCheckedChange = { value ->
viewModel.setShowCrashes(value)
}) {
Icon(
imageVector = if (showCrashes) Icons.Rounded.Error else Icons.Rounded.ErrorOutline,
contentDescription = null,
modifier = Modifier.alpha(if (showCrashes) 1f else 0.5f)
)
}
}
}
items(it) {
OutlinedCard(
modifier = Modifier
.padding(vertical = 4.dp, horizontal = 8.dp)
,
) {
Column(
modifier = Modifier
.fillMaxWidth()
.clickable {
navController?.navigate("settings/debug/crashreporter/report?fileName=${it.filePath}")
}
.padding(16.dp)
) {
Text(
text = DateUtils.formatDateTime(
LocalContext.current,
it.time.time,
DateUtils.FORMAT_SHOW_TIME or DateUtils.FORMAT_SHOW_DATE
),
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.secondary
)
Row(
modifier = Modifier.padding(vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
CompositionLocalProvider(
LocalContentColor provides if (it.type == CrashReportType.Exception) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.error
) {
Icon(
modifier = Modifier.padding(end = 8.dp),
imageVector = if (it.type == CrashReportType.Exception) Icons.Rounded.Warning else Icons.Rounded.Error,
contentDescription = null
)
Text(
text = if (it.type == CrashReportType.Exception) "Exception" else "Crash",
style = MaterialTheme.typography.titleMedium
)
}
}
Text(
text = it.summary,
style = MaterialTheme.typography.bodySmall,
maxLines = 3,
overflow = TextOverflow.Ellipsis
)
}
}
}
} ?: item {
LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
}
}
}

View File

@ -0,0 +1,45 @@
package de.mm20.launcher2.ui.settings.crashreporter
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.liveData
import androidx.lifecycle.viewModelScope
import de.mm20.launcher2.crashreporter.CrashReport
import de.mm20.launcher2.crashreporter.CrashReportType
import de.mm20.launcher2.crashreporter.CrashReporter
import kotlinx.coroutines.launch
class CrashReporterScreenVM: ViewModel() {
fun setShowCrashes(showCrashes: Boolean) {
this.showCrashes.value = showCrashes
updateReports()
}
fun setShowExceptions(showExceptions: Boolean) {
this.showExceptions.value = showExceptions
updateReports()
}
private fun updateReports() {
val exceptions = showExceptions.value == true
val crashes = showCrashes.value == true
reports.value = _reports?.filter {
it.type == CrashReportType.Exception && exceptions ||
it.type == CrashReportType.Crash && crashes
}
}
val showExceptions = MutableLiveData(true)
val showCrashes = MutableLiveData(true)
val reports = MutableLiveData<List<CrashReport>?>(null)
private var _reports: List<CrashReport>? = null
init {
viewModelScope.launch {
_reports = CrashReporter.getCrashReports()
reports.value = _reports
}
}
}

View File

@ -12,6 +12,7 @@ import de.mm20.launcher2.ktx.tryStartActivity
import de.mm20.launcher2.ui.R import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.preferences.Preference import de.mm20.launcher2.ui.component.preferences.Preference
import de.mm20.launcher2.ui.component.preferences.PreferenceScreen import de.mm20.launcher2.ui.component.preferences.PreferenceScreen
import de.mm20.launcher2.ui.locals.LocalNavController
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.io.File import java.io.File
@ -19,6 +20,7 @@ import java.io.File
fun DebugSettingsScreen() { fun DebugSettingsScreen() {
val context = LocalContext.current val context = LocalContext.current
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val navController = LocalNavController.current
PreferenceScreen( PreferenceScreen(
stringResource(R.string.preference_screen_debug) stringResource(R.string.preference_screen_debug)
) { ) {
@ -27,7 +29,7 @@ fun DebugSettingsScreen() {
title = stringResource(R.string.preference_crash_reporter), title = stringResource(R.string.preference_crash_reporter),
summary = stringResource(R.string.preference_crash_reporter_summary), summary = stringResource(R.string.preference_crash_reporter_summary),
onClick = { onClick = {
context.startActivity(CrashReporter.getLaunchIntent()) navController?.navigate("settings/debug/crashreporter")
}) })
Preference( Preference(