forked from github/szkolny
[Errors] Rewrite crash activity in Kotlin and add error reporting.
This commit is contained in:
parent
344da53888
commit
e9ca109c57
@ -31,6 +31,8 @@ const val CODE_SYNERGIA_NOT_ACTIVATED = 32
|
|||||||
const val CODE_LIBRUS_DISCONNECTED = 31
|
const val CODE_LIBRUS_DISCONNECTED = 31
|
||||||
const val CODE_PROFILE_ARCHIVED = 30*/
|
const val CODE_PROFILE_ARCHIVED = 30*/
|
||||||
|
|
||||||
|
const val ERROR_APP_CRASH = 1
|
||||||
|
|
||||||
const val ERROR_REQUEST_FAILURE = 50
|
const val ERROR_REQUEST_FAILURE = 50
|
||||||
const val ERROR_REQUEST_HTTP_400 = 51
|
const val ERROR_REQUEST_HTTP_400 = 51
|
||||||
const val ERROR_REQUEST_HTTP_401 = 52
|
const val ERROR_REQUEST_HTTP_401 = 52
|
||||||
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -4,6 +4,8 @@
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<resources>
|
<resources>
|
||||||
|
<string name="error_1" translatable="false">ERROR_APP_CRASH</string>
|
||||||
|
|
||||||
<string name="error_50" translatable="false">ERROR_REQUEST_FAILURE</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_51" translatable="false">ERROR_REQUEST_HTTP_400</string>
|
||||||
<string name="error_52" translatable="false">ERROR_REQUEST_HTTP_401</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_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_50_reason">Błąd odpowiedzi serwera</string>
|
||||||
<string name="error_51_reason">Błąd serwera: nieprawidłowe zapytanie</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>
|
<string name="error_52_reason">Błąd serwera: odmowa dostępu</string>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user