[Errors] Rewrite crash activity in Kotlin and add error reporting.

This commit is contained in:
Kacper Ziubryniewicz 2019-12-31 18:48:06 +01:00
parent 344da53888
commit e9ca109c57
4 changed files with 186 additions and 180 deletions

View File

@ -31,6 +31,8 @@ const val CODE_SYNERGIA_NOT_ACTIVATED = 32
const val CODE_LIBRUS_DISCONNECTED = 31
const val CODE_PROFILE_ARCHIVED = 30*/
const val ERROR_APP_CRASH = 1
const val ERROR_REQUEST_FAILURE = 50
const val ERROR_REQUEST_HTTP_400 = 51
const val ERROR_REQUEST_HTTP_401 = 52

View File

@ -1,180 +0,0 @@
package pl.szczodrzynski.edziennik.ui.modules.base;
/*
* Copyright 2014-2017 Eduard Ereza Martínez
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import android.annotation.SuppressLint;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import android.text.Html;
import android.util.Base64;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;
import com.afollestad.materialdialogs.MaterialDialog;
import cat.ereza.customactivityoncrash.CustomActivityOnCrash;
import cat.ereza.customactivityoncrash.config.CaocConfig;
import pl.szczodrzynski.edziennik.App;
import pl.szczodrzynski.edziennik.BuildConfig;
import pl.szczodrzynski.edziennik.R;
import pl.szczodrzynski.edziennik.network.ServerRequest;
import pl.szczodrzynski.edziennik.utils.Themes;
import static pl.szczodrzynski.edziennik.App.APP_URL;
import static pl.szczodrzynski.edziennik.data.db.modules.profiles.Profile.REGISTRATION_ENABLED;
public final class CrashActivity extends AppCompatActivity {
private App app;
@SuppressLint("PrivateResource")
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
this.app = (App)getApplication();
setTheme(Themes.INSTANCE.getAppTheme());
setContentView(R.layout.activity_crash);
final CaocConfig config = CustomActivityOnCrash.getConfigFromIntent(getIntent());
if (config == null) {
//This should never happen - Just finish the activity to avoid a recursive crash.
finish();
return;
}
//Close/restart button logic:
//If a class if set, use restart.
//Else, use close and just finish the app.
//It is recommended that you follow this logic if implementing a custom error activity.
Button restartButton = findViewById(R.id.crash_restart_btn);
restartButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
CustomActivityOnCrash.restartApplication(CrashActivity.this, config);
}
});
Button devMessageButton = findViewById(R.id.crash_dev_message_btn);
devMessageButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent i = new Intent(CrashActivity.this, CrashGtfoActivity.class);
startActivity(i);
}
});
final Button reportButton = findViewById(R.id.crash_report_btn);
reportButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (!app.networkUtils.isOnline())
{
new MaterialDialog.Builder(CrashActivity.this)
.title(R.string.network_you_are_offline_title)
.content(R.string.network_you_are_offline_text)
.positiveText(R.string.ok)
.show();
}
else
{
//app.networkUtils.setSelfSignedSSL(CrashActivity.this, null);
new ServerRequest(app, app.requestScheme + APP_URL + "main.php?report", "CrashActivity")
.setBodyParameter("base64_encoded", Base64.encodeToString(getErrorString(getIntent(), true).getBytes(), Base64.DEFAULT))
.run((e, result) -> {
if (result != null)
{
if (result.get("success").getAsBoolean()) {
Toast.makeText(CrashActivity.this, getString(R.string.crash_report_sent), Toast.LENGTH_SHORT).show();
reportButton.setEnabled(false);
reportButton.setTextColor(getResources().getColor(android.R.color.darker_gray));
}
else {
Toast.makeText(CrashActivity.this, getString(R.string.crash_report_cannot_send) + ": " + result.get("reason").getAsString(), Toast.LENGTH_LONG).show();
}
}
else
{
Toast.makeText(CrashActivity.this, getString(R.string.crash_report_cannot_send)+" JsonObject equals null", Toast.LENGTH_LONG).show();
}
});
}
}
});
Button moreInfoButton = findViewById(R.id.crash_details_btn);
moreInfoButton.setOnClickListener(v -> new MaterialDialog.Builder(CrashActivity.this)
.title(R.string.crash_details)
.content(Html.fromHtml(getErrorString(getIntent(), false)))
.typeface(null, "RobotoMono-Regular.ttf")
.positiveText(R.string.close)
.neutralText(R.string.copy_to_clipboard)
.onNeutral((dialog, which) -> copyErrorToClipboard())
.show());
String errorInformation = CustomActivityOnCrash.getAllErrorDetailsFromIntent(CrashActivity.this, getIntent());
if (errorInformation.contains("MANUAL CRASH"))
{
findViewById(R.id.crash_notice).setVisibility(View.GONE);
findViewById(R.id.crash_report_btn).setVisibility(View.GONE);
findViewById(R.id.crash_feature).setVisibility(View.VISIBLE);
}
else
{
findViewById(R.id.crash_notice).setVisibility(View.VISIBLE);
findViewById(R.id.crash_report_btn).setVisibility(View.VISIBLE);
findViewById(R.id.crash_feature).setVisibility(View.GONE);
}
}
private String getErrorString(Intent intent, boolean plain) {
// build a string containing the stack trace and the device name + user's registration data
String contentPlain = "Crash report:\n\n"+CustomActivityOnCrash.getStackTraceFromIntent(intent);
String content = "<small>"+contentPlain+"</small>";
content = content.replaceAll(getPackageName(), "<font color='#4caf50'>"+getPackageName()+"</font>");
content = content.replaceAll("\n", "<br>");
contentPlain += "\n"+Build.MANUFACTURER+"\n"+Build.BRAND+"\n"+Build.MODEL+"\n"+Build.DEVICE+"\n";
if (app.profile != null && app.profile.getRegistration() == REGISTRATION_ENABLED) {
contentPlain += "U: "+app.profile.getUsernameId()+"\nS: "+ app.profile.getStudentNameLong() +"\n";
}
contentPlain += BuildConfig.VERSION_NAME+" "+BuildConfig.BUILD_TYPE;
return plain ? contentPlain : content;
}
private void copyErrorToClipboard() {
String errorInformation = CustomActivityOnCrash.getAllErrorDetailsFromIntent(CrashActivity.this, getIntent());
ClipboardManager clipboard = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
//Are there any devices without clipboard...?
if (clipboard != null) {
ClipData clip = ClipData.newPlainText(getString(R.string.customactivityoncrash_error_activity_error_details_clipboard_label), errorInformation);
clipboard.setPrimaryClip(clip);
Toast.makeText(CrashActivity.this, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show();
}
}
}

View File

@ -0,0 +1,180 @@
package pl.szczodrzynski.edziennik.ui.modules.base
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.text.Html
import android.view.View
import android.widget.Button
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import cat.ereza.customactivityoncrash.CustomActivityOnCrash
import com.afollestad.materialdialogs.MaterialDialog
import kotlinx.coroutines.*
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.BuildConfig
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.data.api.ERROR_APP_CRASH
import pl.szczodrzynski.edziennik.data.api.szkolny.SzkolnyApi
import pl.szczodrzynski.edziennik.data.api.szkolny.request.ErrorReportRequest
import pl.szczodrzynski.edziennik.data.db.modules.profiles.Profile
import pl.szczodrzynski.edziennik.ifNotEmpty
import pl.szczodrzynski.edziennik.utils.Themes.appTheme
import kotlin.coroutines.CoroutineContext
/*
* Copyright 2014-2017 Eduard Ereza Martínez
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
*
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
class CrashActivity : AppCompatActivity(), CoroutineScope {
companion object {
const val TAG = "CrashActivity"
}
private val app by lazy { application as App }
private val api by lazy { SzkolnyApi(app) }
private var job = Job()
override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Main
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setTheme(appTheme)
setContentView(R.layout.activity_crash)
val config = CustomActivityOnCrash.getConfigFromIntent(intent)
if (config == null) { //This should never happen - Just finish the activity to avoid a recursive crash.
finish()
return
}
//Close/restart button logic:
//If a class if set, use restart.
//Else, use close and just finish the app.
//It is recommended that you follow this logic if implementing a custom error activity.
val restartButton = findViewById<Button>(R.id.crash_restart_btn)
restartButton.setOnClickListener { CustomActivityOnCrash.restartApplication(this@CrashActivity, config) }
val devMessageButton = findViewById<Button>(R.id.crash_dev_message_btn)
devMessageButton.setOnClickListener {
val i = Intent(this@CrashActivity, CrashGtfoActivity::class.java)
startActivity(i)
}
val reportButton = findViewById<Button>(R.id.crash_report_btn)
reportButton.setOnClickListener {
if (!app.networkUtils.isOnline) {
MaterialDialog.Builder(this@CrashActivity)
.title(R.string.network_you_are_offline_title)
.content(R.string.network_you_are_offline_text)
.positiveText(R.string.ok)
.show()
} else {
launch {
val response = withContext(Dispatchers.Default) {
api.errorReport(listOf(getReportableError(intent)))
}
response?.errors?.ifNotEmpty {
Toast.makeText(app, getString(R.string.crash_report_cannot_send) + ": " + it[0].reason, Toast.LENGTH_LONG).show()
return@launch
}
if (response != null) {
Toast.makeText(app, getString(R.string.crash_report_sent), Toast.LENGTH_SHORT).show()
reportButton.isEnabled = false
reportButton.setTextColor(resources.getColor(android.R.color.darker_gray))
} else {
Toast.makeText(app, getString(R.string.crash_report_cannot_send) + " JsonObject equals null", Toast.LENGTH_LONG).show()
}
}
}
}
val moreInfoButton = findViewById<Button>(R.id.crash_details_btn)
moreInfoButton.setOnClickListener { v: View? ->
MaterialDialog.Builder(this@CrashActivity)
.title(R.string.crash_details)
.content(Html.fromHtml(getErrorString(intent, false)))
.typeface(null, "RobotoMono-Regular.ttf")
.positiveText(R.string.close)
.neutralText(R.string.copy_to_clipboard)
.onNeutral { _, _ -> copyErrorToClipboard() }
.show()
}
val errorInformation = CustomActivityOnCrash.getAllErrorDetailsFromIntent(this@CrashActivity, intent)
if (errorInformation.contains("MANUAL CRASH")) {
findViewById<View>(R.id.crash_notice).visibility = View.GONE
findViewById<View>(R.id.crash_report_btn).visibility = View.GONE
findViewById<View>(R.id.crash_feature).visibility = View.VISIBLE
} else {
findViewById<View>(R.id.crash_notice).visibility = View.VISIBLE
findViewById<View>(R.id.crash_report_btn).visibility = View.VISIBLE
findViewById<View>(R.id.crash_feature).visibility = View.GONE
}
}
private fun getErrorString(intent: Intent, plain: Boolean): String {
var contentPlain = "Crash report:\n\n" + CustomActivityOnCrash.getStackTraceFromIntent(intent)
var content = "<small>$contentPlain</small>"
content = content.replace(packageName.toRegex(), "<font color='#4caf50'>$packageName</font>")
content = content.replace("\n".toRegex(), "<br>")
contentPlain += "\n" + Build.MANUFACTURER + "\n" + Build.BRAND + "\n" + Build.MODEL + "\n" + Build.DEVICE + "\n"
if (app.profile != null && app.profile.registration == Profile.REGISTRATION_ENABLED) {
contentPlain += "U: " + app.profile.usernameId + "\nS: " + app.profile.studentNameLong + "\n"
}
contentPlain += BuildConfig.VERSION_NAME + " " + BuildConfig.BUILD_TYPE
return if (plain) contentPlain else content
}
private fun getReportableError(intent: Intent): ErrorReportRequest.Error {
val content = CustomActivityOnCrash.getStackTraceFromIntent(intent)
val errorCode: Int = ERROR_APP_CRASH
val errorText = app.resources.getIdentifier("error_$errorCode", "string", app.packageName).let {
if (it != 0) getString(it) else "?"
}
val errorReason = app.resources.getIdentifier("error_" + errorCode + "_reason", "string", app.packageName).let {
if (it != 0) getString(it) else "?"
}
return ErrorReportRequest.Error(
System.currentTimeMillis(),
TAG,
errorCode,
errorText,
errorReason,
content,
null,
null,
null,
true
)
}
private fun copyErrorToClipboard() {
val errorInformation = CustomActivityOnCrash.getAllErrorDetailsFromIntent(this@CrashActivity, intent)
val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager
clipboard?.apply {
val clip = ClipData.newPlainText(getString(R.string.customactivityoncrash_error_activity_error_details_clipboard_label), errorInformation)
primaryClip = clip
Toast.makeText(this@CrashActivity, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show()
}
}
}

View File

@ -4,6 +4,8 @@
-->
<resources>
<string name="error_1" translatable="false">ERROR_APP_CRASH</string>
<string name="error_50" translatable="false">ERROR_REQUEST_FAILURE</string>
<string name="error_51" translatable="false">ERROR_REQUEST_HTTP_400</string>
<string name="error_52" translatable="false">ERROR_REQUEST_HTTP_401</string>
@ -156,6 +158,8 @@
<string name="error_1201" translatable="false">LOGIN_NO_ARGUMENTS</string>
<string name="error_1_reason">Aplikacja przestała działać</string>
<string name="error_50_reason">Błąd odpowiedzi serwera</string>
<string name="error_51_reason">Błąd serwera: nieprawidłowe zapytanie</string>
<string name="error_52_reason">Błąd serwera: odmowa dostępu</string>