Migrate crash reporter UI to Jetpack Compose
This commit is contained in:
parent
b53aaeec4e
commit
a20707fc3c
@ -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>
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@ -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"));
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
@ -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" />
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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" />
|
||||
@ -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(
|
||||
|
||||
@ -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") {
|
||||
|
||||
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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"))
|
||||
}
|
||||
|
||||
}
|
||||
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -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(
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user