Add details to error message (#186)

This commit is contained in:
Rafał Borcz 2018-11-25 15:03:47 +01:00 committed by Mikołaj Pich
parent 7a3c0de7ad
commit 7686228e01
13 changed files with 193 additions and 26 deletions

View File

@ -2,29 +2,30 @@ package io.github.wulkanowy.data
import android.content.res.Resources import android.content.res.Resources
import io.github.wulkanowy.R import io.github.wulkanowy.R
import io.github.wulkanowy.api.interceptor.ServiceUnavailableException
import io.github.wulkanowy.api.login.NotLoggedInException import io.github.wulkanowy.api.login.NotLoggedInException
import timber.log.Timber import timber.log.Timber
import java.io.IOException
import java.net.SocketTimeoutException import java.net.SocketTimeoutException
import java.net.UnknownHostException import java.net.UnknownHostException
import javax.inject.Inject import javax.inject.Inject
open class ErrorHandler @Inject constructor(protected val resources: Resources) { open class ErrorHandler @Inject constructor(protected val resources: Resources) {
var showErrorMessage: (String) -> Unit = {} var showErrorMessage: (String, Throwable) -> Unit = { _, _ -> }
open fun proceed(error: Throwable) { open fun proceed(error: Throwable) {
Timber.e(error, "An exception occurred while the Wulkanowy was running") Timber.e(error, "An exception occurred while the Wulkanowy was running")
showErrorMessage((when (error) { showErrorMessage((when (error) {
is UnknownHostException -> resources.getString(R.string.all_no_internet) is UnknownHostException -> resources.getString(R.string.error_no_internet)
is SocketTimeoutException -> resources.getString(R.string.all_timeout) is SocketTimeoutException -> resources.getString(R.string.error_timeout)
is NotLoggedInException, is IOException -> resources.getString(R.string.all_login_failed) is NotLoggedInException -> resources.getString(R.string.error_login_failed)
else -> error.localizedMessage is ServiceUnavailableException -> resources.getString(R.string.error_service_unavaible)
})) else -> resources.getString(R.string.error_unknown)
}), error)
} }
open fun clear() { open fun clear() {
showErrorMessage = {} showErrorMessage = { _, _ -> }
} }
} }

View File

@ -6,6 +6,7 @@ import androidx.appcompat.app.AppCompatDelegate
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.google.android.material.snackbar.Snackbar.LENGTH_LONG import com.google.android.material.snackbar.Snackbar.LENGTH_LONG
import dagger.android.support.DaggerAppCompatActivity import dagger.android.support.DaggerAppCompatActivity
import io.github.wulkanowy.R
abstract class BaseActivity : DaggerAppCompatActivity(), BaseView { abstract class BaseActivity : DaggerAppCompatActivity(), BaseView {
@ -16,6 +17,12 @@ abstract class BaseActivity : DaggerAppCompatActivity(), BaseView {
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true) AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
} }
override fun showError(text: String, error: Throwable) {
Snackbar.make(messageContainer, text, LENGTH_LONG).setAction(R.string.all_details) {
ErrorDialog.newInstance(error).show(supportFragmentManager, error.toString())
}.show()
}
override fun showMessage(text: String) { override fun showMessage(text: String) {
Snackbar.make(messageContainer, text, LENGTH_LONG).show() Snackbar.make(messageContainer, text, LENGTH_LONG).show()
} }

View File

@ -2,15 +2,24 @@ package io.github.wulkanowy.ui.base
import android.view.View import android.view.View
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.google.android.material.snackbar.Snackbar.LENGTH_LONG
import dagger.android.support.DaggerFragment import dagger.android.support.DaggerFragment
import io.github.wulkanowy.R
abstract class BaseFragment : DaggerFragment(), BaseView { abstract class BaseFragment : DaggerFragment(), BaseView {
protected var messageContainer: View? = null protected var messageContainer: View? = null
override fun showError(text: String, error: Throwable) {
if (messageContainer == null) (activity as? BaseActivity)?.showError(text, error)
else messageContainer?.also {
Snackbar.make(it, text, Snackbar.LENGTH_LONG).setAction(R.string.all_details) {
ErrorDialog.newInstance(error).show(fragmentManager, error.toString())
}.show()
}
}
override fun showMessage(text: String) { override fun showMessage(text: String) {
if (messageContainer == null) (activity as? BaseActivity)?.showMessage(text) if (messageContainer == null) (activity as? BaseActivity)?.showMessage(text)
else messageContainer?.also { Snackbar.make(it, text, LENGTH_LONG).show() } else messageContainer?.also { Snackbar.make(it, text, Snackbar.LENGTH_LONG).show() }
} }
} }

View File

@ -1,7 +1,6 @@
package io.github.wulkanowy.ui.base package io.github.wulkanowy.ui.base
import io.github.wulkanowy.data.ErrorHandler import io.github.wulkanowy.data.ErrorHandler
import io.github.wulkanowy.ui.modules.main.MainView
import io.reactivex.disposables.CompositeDisposable import io.reactivex.disposables.CompositeDisposable
open class BasePresenter<T : BaseView>(private val errorHandler: ErrorHandler) { open class BasePresenter<T : BaseView>(private val errorHandler: ErrorHandler) {
@ -12,7 +11,7 @@ open class BasePresenter<T : BaseView>(private val errorHandler: ErrorHandler) {
open fun onAttachView(view: T) { open fun onAttachView(view: T) {
this.view = view this.view = view
errorHandler.showErrorMessage = { view.showMessage(it) } errorHandler.showErrorMessage = { text, error -> view.showError(text, error) }
} }
open fun onDetachView() { open fun onDetachView() {

View File

@ -2,5 +2,7 @@ package io.github.wulkanowy.ui.base
interface BaseView { interface BaseView {
fun showError(text: String, error: Throwable)
fun showMessage(text: String) fun showMessage(text: String)
} }

View File

@ -0,0 +1,60 @@
package io.github.wulkanowy.ui.base
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context.CLIPBOARD_SERVICE
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import android.widget.Toast.LENGTH_LONG
import androidx.fragment.app.DialogFragment
import io.github.wulkanowy.R
import kotlinx.android.synthetic.main.dialog_error.*
import java.io.PrintWriter
import java.io.StringWriter
class ErrorDialog : DialogFragment() {
private lateinit var error: Throwable
companion object {
private const val ARGUMENT_KEY = "Data"
fun newInstance(error: Throwable): ErrorDialog {
return ErrorDialog().apply {
arguments = Bundle().apply { putSerializable(ARGUMENT_KEY, error) }
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setStyle(STYLE_NO_TITLE, 0)
arguments?.run {
error = getSerializable(ARGUMENT_KEY) as Throwable
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.dialog_error, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
StringWriter().let { writer ->
error.printStackTrace(PrintWriter(writer))
errorDialogContent.text = writer.toString()
errorDialogCopy.setOnClickListener {
ClipData.newPlainText("wulkanowyError", writer.toString()).let { clip ->
(activity?.getSystemService(CLIPBOARD_SERVICE) as? ClipboardManager)?.primaryClip = clip
}
Toast.makeText(context, R.string.all_copied, LENGTH_LONG).show()
}
}
errorDialogCancel.setOnClickListener { dismiss() }
}
}

View File

@ -59,6 +59,10 @@ class AccountDialog : DaggerAppCompatDialogFragment(), AccountView {
accountAdapter.updateDataSet(data) accountAdapter.updateDataSet(data)
} }
override fun showError(text: String, error: Throwable) {
showMessage(text)
}
override fun showMessage(text: String) { override fun showMessage(text: String) {
Toast.makeText(context, text, LENGTH_LONG).show() Toast.makeText(context, text, LENGTH_LONG).show()
} }

View File

@ -10,10 +10,12 @@ class LoginErrorHandler(resources: Resources) : ErrorHandler(resources) {
var onBadCredentials: () -> Unit = {} var onBadCredentials: () -> Unit = {}
var onStudentDuplicate: (String) -> Unit = {}
override fun proceed(error: Throwable) { override fun proceed(error: Throwable) {
when (error) { when (error) {
is BadCredentialsException -> onBadCredentials() is BadCredentialsException -> onBadCredentials()
is SQLiteConstraintException -> showErrorMessage(resources.getString(R.string.login_duplicate_student)) is SQLiteConstraintException -> onStudentDuplicate(resources.getString(R.string.login_duplicate_student))
else -> super.proceed(error) else -> super.proceed(error)
} }
} }

View File

@ -20,7 +20,10 @@ class LoginOptionsPresenter @Inject constructor(
override fun onAttachView(view: LoginOptionsView) { override fun onAttachView(view: LoginOptionsView) {
super.onAttachView(view) super.onAttachView(view)
view.initView() view.run {
initView()
errorHandler.onStudentDuplicate = { showMessage(it) }
}
} }
fun onParentViewLoadData() { fun onParentViewLoadData() {

View File

@ -3,11 +3,11 @@ package io.github.wulkanowy.ui.modules.settings
import android.content.Context import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import android.os.Bundle import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import com.takisoft.preferencex.PreferenceFragmentCompat import com.takisoft.preferencex.PreferenceFragmentCompat
import dagger.android.support.AndroidSupportInjection import dagger.android.support.AndroidSupportInjection
import io.github.wulkanowy.R import io.github.wulkanowy.R
import io.github.wulkanowy.ui.base.BaseActivity
import io.github.wulkanowy.ui.modules.main.MainView import io.github.wulkanowy.ui.modules.main.MainView
import javax.inject.Inject import javax.inject.Inject
@ -54,8 +54,12 @@ class SettingsFragment : PreferenceFragmentCompat(), SharedPreferences.OnSharedP
} }
} }
override fun showError(text: String, error: Throwable) {
(activity as? BaseActivity)?.showError(text, error)
}
override fun showMessage(text: String) { override fun showMessage(text: String) {
Toast.makeText(context, text, Toast.LENGTH_SHORT).show() (activity as? BaseActivity)?.showMessage(text)
} }
override fun onResume() { override fun onResume() {

View File

@ -0,0 +1,70 @@
<LinearLayout 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"
android:minWidth="300dp"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:orientation="vertical">
<ScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:overScrollMode="ifContentScrolls"
tools:ignore="UselessParent">
<HorizontalScrollView
android:layout_width="350dp"
android:layout_height="wrap_content"
android:paddingLeft="24dp"
android:paddingTop="24dp"
android:paddingRight="24dp">
<TextView
android:id="@+id/errorDialogContent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:scrollHorizontally="true"
android:textIsSelectable="true"
android:textSize="12sp" />
</HorizontalScrollView>
</ScrollView>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:gravity="end"
android:minHeight="52dp"
android:orientation="horizontal">
<Button
android:id="@+id/errorDialogCancel"
style="@style/Widget.AppCompat.Button.Borderless"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"
android:gravity="center"
android:text="@android:string/cancel"
android:textColor="@color/colorPrimary" />
<Button
android:id="@+id/errorDialogCopy"
style="@style/Widget.AppCompat.Button.Borderless"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"
android:gravity="center"
android:text="@android:string/copy"
android:textColor="@color/colorPrimary" />
</LinearLayout>
</LinearLayout>

View File

@ -205,9 +205,12 @@
<!--Others--> <!--Others-->
<string name="all_no_internet">Brak połączenia z internetem</string> <string name="all_copied">Skopiowano</string>
<string name="all_sync_finish">Synchronizacja zakończona</string>
<string name="all_sync_fail">Podczas synchronizacji wystąpił błąd</string> <!--Errors-->
<string name="all_timeout">Zbyt długie oczekiwanie na połączenie</string> <string name="error_no_internet">Brak połączenia z internetem</string>
<string name="all_login_failed">Logowanie nie powiodło się. Spróbuj ponownie lub zrestartuj aplikację</string> <string name="error_timeout">Zbyt długie oczekiwanie na połączenie</string>
<string name="error_login_failed">Logowanie nie powiodło się. Spróbuj ponownie lub zrestartuj aplikację</string>
<string name="error_service_unavaible">Dziennik jest niedostępny. Spróbuj ponownie później</string>
<string name="error_unknown">Wystąpił nieoczekiwany błąd</string>
</resources> </resources>

View File

@ -190,9 +190,12 @@
<!--Others--> <!--Others-->
<string name="all_no_internet">No internet connection</string> <string name="all_copied">Copied</string>
<string name="all_sync_finish">Synchronization complete</string>
<string name="all_sync_fail">There was an error during synchronization</string> <!--Errors-->
<string name="all_timeout">Too long wait for connection</string> <string name="error_no_internet">No internet connection</string>
<string name="all_login_failed">Login is failed. Try again or restart the app</string> <string name="error_timeout">Too long wait for connection</string>
<string name="error_login_failed">Login is failed. Try again or restart the app</string>
<string name="error_service_unavaible">The log is not available. Try again later</string>
<string name="error_unknown">An unexpected error occurred</string>
</resources> </resources>