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"
package="de.mm20.launcher2.crashreporter">
<application
android:supportsRtl="true">
<application>
<provider
android:name="com.balsikandar.crashreporter.CrashReporterInitProvider"
android:authorities="${applicationId}.CrashReporterInitProvider"
android:enabled="true"
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>
</manifest>

View File

@ -3,7 +3,6 @@ package com.balsikandar.crashreporter;
import android.content.Context;
import android.content.Intent;
import com.balsikandar.crashreporter.ui.CrashReporterActivity;
import com.balsikandar.crashreporter.utils.CrashReporterNotInitializedException;
import com.balsikandar.crashreporter.utils.CrashReporterExceptionHandler;
import com.balsikandar.crashreporter.utils.CrashUtil;
@ -61,10 +60,6 @@ public class CrashReporter {
CrashUtil.logException(exception);
}
public static Intent getLaunchIntent() {
return new Intent(applicationContext, CrashReporterActivity.class).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
}
public static void disableNotification() {
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;
import android.Manifest;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageInfo;
@ -11,8 +8,6 @@ import android.content.pm.ResolveInfo;
import android.os.Build;
import android.util.Log;
import androidx.core.app.ActivityCompat;
import java.util.TimeZone;
import java.util.UUID;
@ -40,9 +35,8 @@ public class AppUtils {
public static String getDeviceDetails(Context context) {
return "Device Information\n"
+ "\nDEVICE.ID : " + getDeviceId(context)
+ "\nAPP.VERSION : " + getAppVersion(context)
return "APP.VERSION : " + getAppVersion(context)
+ "\nAPP.VERSIONCODE : " + getAppVersionCode(context)
+ "\nLAUNCHER.APP : " + getCurrentLauncherApp(context)
+ "\nTIMEZONE : " + timeZone()
+ "\nVERSION.RELEASE : " + Build.VERSION.RELEASE
@ -61,12 +55,9 @@ public class AppUtils {
+ "\nMANUFACTURER : " + Build.MANUFACTURER
+ "\nMODEL : " + Build.MODEL
+ "\nPRODUCT : " + Build.PRODUCT
+ "\nSERIAL : " + Build.SERIAL
+ "\nTAGS : " + Build.TAGS
+ "\nTIME : " + Build.TIME
+ "\nTYPE : " + Build.TYPE
+ "\nUNKNOWN : " + Build.UNKNOWN
+ "\nUSER : " + Build.USER;
+ "\nTYPE : " + Build.TYPE;
}
private static String timeZone() {
@ -94,7 +85,7 @@ public class AppUtils {
return androidId;
}
private static int getAppVersion(Context context) {
private static int getAppVersionCode(Context context) {
try {
PackageInfo packageInfo = context.getPackageManager()
.getPackageInfo(context.getPackageName(), 0);
@ -103,4 +94,14 @@ public class AppUtils {
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.NotificationManager;
import android.app.PendingIntent;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.text.TextUtils;
import android.util.Log;
import androidx.core.app.NotificationCompat;
import androidx.core.content.ContextCompat;
import com.balsikandar.crashreporter.CrashReporter;
import de.mm20.launcher2.crashreporter.R;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.io.Writer;
import java.net.URLEncoder;
import java.text.SimpleDateFormat;
import java.util.Date;
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 com.balsikandar.crashreporter.utils.Constants.CHANNEL_NOTIFICATION_ID;
@ -47,7 +49,13 @@ public class CrashUtil {
String filename = getCrashLogTime() + Constants.CRASH_SUFFIX + Constants.FILE_EXTENSION;
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) {
@ -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()) {
Context context = CrashReporter.getContext();
@ -101,8 +109,11 @@ public class CrashUtil {
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, CHANNEL_NOTIFICATION_ID);
builder.setSmallIcon(R.drawable.ic_warning_black_24dp);
Intent intent = CrashReporter.getLaunchIntent();
intent.putExtra(Constants.LANDING, isCrash);
String filePath = new File(getDefaultPath(), fileName).getAbsolutePath();
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()));
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
import android.content.Context
import android.content.Intent
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.Dispatchers
import kotlinx.coroutines.withContext
import java.io.File
object CrashReporter {
fun logException(e: Exception) {
@ -12,7 +19,23 @@ object CrashReporter {
Log.e("MM20", Log.getStackTraceString(e))
}
fun getLaunchIntent(): Intent {
return com.balsikandar.crashreporter.CrashReporter.getLaunchIntent()
suspend fun getCrashReports(): List<CrashReport> {
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.compose.foundation.layout.Box
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
@ -23,7 +24,8 @@ import de.mm20.launcher2.ui.locals.LocalNavController
fun PreferenceScreen(
title: String,
floatingActionButton: @Composable () -> Unit = {},
content: LazyListScope.() -> Unit
topBarActions: @Composable RowScope.() -> Unit = {},
content: LazyListScope.() -> Unit,
) {
val navController = LocalNavController.current
val systemUiController = rememberSystemUiController()
@ -50,6 +52,7 @@ fun PreferenceScreen(
Icon(imageVector = Icons.Rounded.ArrowBack, contentDescription = "Back")
}
},
actions = topBarActions
)
}) {
LazyColumn(

View File

@ -6,10 +6,7 @@ import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.*
import androidx.navigation.navArgument
import com.google.accompanist.navigation.animation.AnimatedNavHost
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.cards.CardsSettingsScreen
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.easteregg.EasterEggSettingsScreen
import de.mm20.launcher2.ui.settings.filesearch.FileSearchSettingsScreen
@ -55,6 +54,12 @@ class SettingsActivity : BaseActivity() {
setContent {
val navController = rememberAnimatedNavController()
LaunchedEffect(intent) {
intent.getStringExtra("de.mm20.launcher2.settings.ROUTE")
?.let { navController.navigate(it) }
}
val cardStyle by remember {
dataStore.data.map { it.cards }.distinctUntilChanged()
}.collectAsState(
@ -127,6 +132,17 @@ class SettingsActivity : BaseActivity() {
composable("settings/debug") {
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(
"settings/license?library={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.component.preferences.Preference
import de.mm20.launcher2.ui.component.preferences.PreferenceScreen
import de.mm20.launcher2.ui.locals.LocalNavController
import kotlinx.coroutines.launch
import java.io.File
@ -19,6 +20,7 @@ import java.io.File
fun DebugSettingsScreen() {
val context = LocalContext.current
val scope = rememberCoroutineScope()
val navController = LocalNavController.current
PreferenceScreen(
stringResource(R.string.preference_screen_debug)
) {
@ -27,7 +29,7 @@ fun DebugSettingsScreen() {
title = stringResource(R.string.preference_crash_reporter),
summary = stringResource(R.string.preference_crash_reporter_summary),
onClick = {
context.startActivity(CrashReporter.getLaunchIntent())
navController?.navigate("settings/debug/crashreporter")
})
Preference(