diff --git a/crashreporter/src/main/AndroidManifest.xml b/crashreporter/src/main/AndroidManifest.xml index 05f763f2..8146fcf8 100644 --- a/crashreporter/src/main/AndroidManifest.xml +++ b/crashreporter/src/main/AndroidManifest.xml @@ -1,24 +1,10 @@ - - + - - - - \ No newline at end of file diff --git a/crashreporter/src/main/java/com/balsikandar/crashreporter/CrashReporter.java b/crashreporter/src/main/java/com/balsikandar/crashreporter/CrashReporter.java index 5a1e8fdb..34831ada 100644 --- a/crashreporter/src/main/java/com/balsikandar/crashreporter/CrashReporter.java +++ b/crashreporter/src/main/java/com/balsikandar/crashreporter/CrashReporter.java @@ -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; } diff --git a/crashreporter/src/main/java/com/balsikandar/crashreporter/adapter/CrashLogAdapter.java b/crashreporter/src/main/java/com/balsikandar/crashreporter/adapter/CrashLogAdapter.java deleted file mode 100644 index 8bf7834a..00000000 --- a/crashreporter/src/main/java/com/balsikandar/crashreporter/adapter/CrashLogAdapter.java +++ /dev/null @@ -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 { - - private Context context; - private ArrayList crashFileList; - - public CrashLogAdapter(Context context, ArrayList 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 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); - } - }); - } - } -} diff --git a/crashreporter/src/main/java/com/balsikandar/crashreporter/adapter/MainPagerAdapter.java b/crashreporter/src/main/java/com/balsikandar/crashreporter/adapter/MainPagerAdapter.java deleted file mode 100644 index bf572d1c..00000000 --- a/crashreporter/src/main/java/com/balsikandar/crashreporter/adapter/MainPagerAdapter.java +++ /dev/null @@ -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(); - } -} \ No newline at end of file diff --git a/crashreporter/src/main/java/com/balsikandar/crashreporter/ui/CrashLogFragment.java b/crashreporter/src/main/java/com/balsikandar/crashreporter/ui/CrashLogFragment.java deleted file mode 100644 index 9df1a709..00000000 --- a/crashreporter/src/main/java/com/balsikandar/crashreporter/ui/CrashLogFragment.java +++ /dev/null @@ -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 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 listOfFiles = new ArrayList<>(Arrays.asList(directory.listFiles())); - for (Iterator iterator = listOfFiles.iterator(); iterator.hasNext(); ) { - if (iterator.next().getName().contains(Constants.EXCEPTION_SUFFIX)) { - iterator.remove(); - } - } - Collections.sort(listOfFiles, Collections.reverseOrder()); - return listOfFiles; - } - -} diff --git a/crashreporter/src/main/java/com/balsikandar/crashreporter/ui/CrashReporterActivity.java b/crashreporter/src/main/java/com/balsikandar/crashreporter/ui/CrashReporterActivity.java deleted file mode 100644 index 9f94c04d..00000000 --- a/crashreporter/src/main/java/com/balsikandar/crashreporter/ui/CrashReporterActivity.java +++ /dev/null @@ -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); - } - -} diff --git a/crashreporter/src/main/java/com/balsikandar/crashreporter/ui/ExceptionLogFragment.java b/crashreporter/src/main/java/com/balsikandar/crashreporter/ui/ExceptionLogFragment.java deleted file mode 100644 index 6fa4c706..00000000 --- a/crashreporter/src/main/java/com/balsikandar/crashreporter/ui/ExceptionLogFragment.java +++ /dev/null @@ -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 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 listOfFiles = new ArrayList<>(Arrays.asList(directory.listFiles())); - for (Iterator iterator = listOfFiles.iterator(); iterator.hasNext(); ) { - if (iterator.next().getName().contains(Constants.CRASH_SUFFIX)) { - iterator.remove(); - } - } - Collections.sort(listOfFiles, Collections.reverseOrder()); - return listOfFiles; - } - -} diff --git a/crashreporter/src/main/java/com/balsikandar/crashreporter/ui/LogMessageActivity.java b/crashreporter/src/main/java/com/balsikandar/crashreporter/ui/LogMessageActivity.java deleted file mode 100644 index d06fca6b..00000000 --- a/crashreporter/src/main/java/com/balsikandar/crashreporter/ui/LogMessageActivity.java +++ /dev/null @@ -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")); - } -} diff --git a/crashreporter/src/main/java/com/balsikandar/crashreporter/utils/AppUtils.java b/crashreporter/src/main/java/com/balsikandar/crashreporter/utils/AppUtils.java index c304dae1..357f59f0 100644 --- a/crashreporter/src/main/java/com/balsikandar/crashreporter/utils/AppUtils.java +++ b/crashreporter/src/main/java/com/balsikandar/crashreporter/utils/AppUtils.java @@ -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); + } + } } diff --git a/crashreporter/src/main/java/com/balsikandar/crashreporter/utils/CrashUtil.java b/crashreporter/src/main/java/com/balsikandar/crashreporter/utils/CrashUtil.java index 32004420..7d9d04f4 100644 --- a/crashreporter/src/main/java/com/balsikandar/crashreporter/utils/CrashUtil.java +++ b/crashreporter/src/main/java/com/balsikandar/crashreporter/utils/CrashUtil.java @@ -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); diff --git a/crashreporter/src/main/java/de/mm20/launcher2/crashreporter/CrashReport.kt b/crashreporter/src/main/java/de/mm20/launcher2/crashreporter/CrashReport.kt new file mode 100644 index 00000000..a2b46936 --- /dev/null +++ b/crashreporter/src/main/java/de/mm20/launcher2/crashreporter/CrashReport.kt @@ -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 +} \ No newline at end of file diff --git a/crashreporter/src/main/java/de/mm20/launcher2/crashreporter/CrashReporter.kt b/crashreporter/src/main/java/de/mm20/launcher2/crashreporter/CrashReporter.kt index 611e8609..2ae26257 100644 --- a/crashreporter/src/main/java/de/mm20/launcher2/crashreporter/CrashReporter.kt +++ b/crashreporter/src/main/java/de/mm20/launcher2/crashreporter/CrashReporter.kt @@ -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 { + 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) } } \ No newline at end of file diff --git a/crashreporter/src/main/res/layout/activity_log_message.xml b/crashreporter/src/main/res/layout/activity_log_message.xml deleted file mode 100644 index 69a6f8e7..00000000 --- a/crashreporter/src/main/res/layout/activity_log_message.xml +++ /dev/null @@ -1,45 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/crashreporter/src/main/res/layout/crash_log.xml b/crashreporter/src/main/res/layout/crash_log.xml deleted file mode 100644 index 21025eda..00000000 --- a/crashreporter/src/main/res/layout/crash_log.xml +++ /dev/null @@ -1,4 +0,0 @@ - \ No newline at end of file diff --git a/crashreporter/src/main/res/layout/crash_reporter_activity.xml b/crashreporter/src/main/res/layout/crash_reporter_activity.xml deleted file mode 100644 index 85be3d33..00000000 --- a/crashreporter/src/main/res/layout/crash_reporter_activity.xml +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/crashreporter/src/main/res/layout/custom_item.xml b/crashreporter/src/main/res/layout/custom_item.xml deleted file mode 100644 index 110486c3..00000000 --- a/crashreporter/src/main/res/layout/custom_item.xml +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/crashreporter/src/main/res/layout/exception_log.xml b/crashreporter/src/main/res/layout/exception_log.xml deleted file mode 100644 index 86d642fc..00000000 --- a/crashreporter/src/main/res/layout/exception_log.xml +++ /dev/null @@ -1,4 +0,0 @@ - \ No newline at end of file diff --git a/ui/src/main/java/de/mm20/launcher2/ui/component/preferences/PreferenceScreen.kt b/ui/src/main/java/de/mm20/launcher2/ui/component/preferences/PreferenceScreen.kt index 5963fd31..11ec5af4 100644 --- a/ui/src/main/java/de/mm20/launcher2/ui/component/preferences/PreferenceScreen.kt +++ b/ui/src/main/java/de/mm20/launcher2/ui/component/preferences/PreferenceScreen.kt @@ -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( diff --git a/ui/src/main/java/de/mm20/launcher2/ui/settings/SettingsActivity.kt b/ui/src/main/java/de/mm20/launcher2/ui/settings/SettingsActivity.kt index b1913742..46274ede 100644 --- a/ui/src/main/java/de/mm20/launcher2/ui/settings/SettingsActivity.kt +++ b/ui/src/main/java/de/mm20/launcher2/ui/settings/SettingsActivity.kt @@ -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") { diff --git a/ui/src/main/java/de/mm20/launcher2/ui/settings/crashreporter/CrashReportScreen.kt b/ui/src/main/java/de/mm20/launcher2/ui/settings/crashreporter/CrashReportScreen.kt new file mode 100644 index 00000000..f3d28597 --- /dev/null +++ b/ui/src/main/java/de/mm20/launcher2/ui/settings/crashreporter/CrashReportScreen.kt @@ -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) + ) + } + } + } +} \ No newline at end of file diff --git a/ui/src/main/java/de/mm20/launcher2/ui/settings/crashreporter/CrashReportScreenVM.kt b/ui/src/main/java/de/mm20/launcher2/ui/settings/crashreporter/CrashReportScreenVM.kt new file mode 100644 index 00000000..183c3f5a --- /dev/null +++ b/ui/src/main/java/de/mm20/launcher2/ui/settings/crashreporter/CrashReportScreenVM.kt @@ -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 { + 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", "
")}\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")) + } + +} \ No newline at end of file diff --git a/ui/src/main/java/de/mm20/launcher2/ui/settings/crashreporter/CrashReporterScreen.kt b/ui/src/main/java/de/mm20/launcher2/ui/settings/crashreporter/CrashReporterScreen.kt new file mode 100644 index 00000000..d48ac497 --- /dev/null +++ b/ui/src/main/java/de/mm20/launcher2/ui/settings/crashreporter/CrashReporterScreen.kt @@ -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()) + } + } +} \ No newline at end of file diff --git a/ui/src/main/java/de/mm20/launcher2/ui/settings/crashreporter/CrashReporterScreenVM.kt b/ui/src/main/java/de/mm20/launcher2/ui/settings/crashreporter/CrashReporterScreenVM.kt new file mode 100644 index 00000000..76f76945 --- /dev/null +++ b/ui/src/main/java/de/mm20/launcher2/ui/settings/crashreporter/CrashReporterScreenVM.kt @@ -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?>(null) + private var _reports: List? = null + + init { + viewModelScope.launch { + _reports = CrashReporter.getCrashReports() + reports.value = _reports + } + } + +} \ No newline at end of file diff --git a/ui/src/main/java/de/mm20/launcher2/ui/settings/debug/DebugSettingsScreen.kt b/ui/src/main/java/de/mm20/launcher2/ui/settings/debug/DebugSettingsScreen.kt index d1d81138..65da4443 100644 --- a/ui/src/main/java/de/mm20/launcher2/ui/settings/debug/DebugSettingsScreen.kt +++ b/ui/src/main/java/de/mm20/launcher2/ui/settings/debug/DebugSettingsScreen.kt @@ -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(