Skip to content

Commit 21cc016

Browse files
committed
Rework detailed error dialog
1 parent 8d6bc09 commit 21cc016

File tree

12 files changed

+337
-109
lines changed

12 files changed

+337
-109
lines changed

app/src/main/kotlin/org/equeim/tremotesf/ui/NavigationActivity.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,10 @@ import kotlinx.coroutines.launch
3333
import org.equeim.tremotesf.NavMainDirections
3434
import org.equeim.tremotesf.R
3535
import org.equeim.tremotesf.databinding.NavigationActivityBinding
36+
import org.equeim.tremotesf.rpc.GlobalRpcClient
3637
import org.equeim.tremotesf.rpc.getErrorString
3738
import org.equeim.tremotesf.service.ForegroundService
38-
import org.equeim.tremotesf.torrentfile.rpc.makeDetailedErrorString
39+
import org.equeim.tremotesf.torrentfile.rpc.makeDetailedError
3940
import org.equeim.tremotesf.ui.utils.hideKeyboard
4041
import org.equeim.tremotesf.ui.utils.launchAndCollectWhenStarted
4142
import org.equeim.tremotesf.ui.utils.showSnackbar
@@ -113,7 +114,7 @@ class NavigationActivity : AppCompatActivity(), NavControllerProvider {
113114
lifecycleOwner = this,
114115
activity = this,
115116
actionText = R.string.see_detailed_error_message,
116-
action = { navigate(NavMainDirections.toDetailedConnectionErrorDialogFragment(error.error.makeDetailedErrorString())) }
117+
action = { navigate(NavMainDirections.toDetailedConnectionErrorDialogFragment(error.error.makeDetailedError(GlobalRpcClient))) }
117118
)
118119
model.rpcErrorDismissed()
119120
}

app/src/main/kotlin/org/equeim/tremotesf/ui/torrentslistfragment/DetailedConnectionErrorDialogFragment.kt

Lines changed: 151 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,19 @@
55
package org.equeim.tremotesf.ui.torrentslistfragment
66

77
import android.app.Dialog
8+
import android.graphics.Typeface
89
import android.os.Bundle
910
import android.view.LayoutInflater
11+
import android.view.ViewGroup
12+
import android.widget.TextView
1013
import com.google.android.material.dialog.MaterialAlertDialogBuilder
14+
import com.google.android.material.divider.MaterialDivider
1115
import org.equeim.tremotesf.R
16+
import org.equeim.tremotesf.common.causes
1217
import org.equeim.tremotesf.databinding.DetailedConnectionErrorDialogBinding
18+
import org.equeim.tremotesf.databinding.DetailedConnectionErrorExpandedDialogBinding
19+
import org.equeim.tremotesf.torrentfile.rpc.DetailedRpcRequestError
20+
import org.equeim.tremotesf.torrentfile.rpc.redactHeader
1321
import org.equeim.tremotesf.ui.NavigationDialogFragment
1422
import org.equeim.tremotesf.ui.utils.Utils
1523

@@ -18,17 +26,157 @@ class DetailedConnectionErrorDialogFragment : NavigationDialogFragment() {
1826
val builder = MaterialAlertDialogBuilder(requireContext())
1927
val binding = DetailedConnectionErrorDialogBinding.inflate(LayoutInflater.from(builder.context))
2028
val error = DetailedConnectionErrorDialogFragmentArgs.fromBundle(requireArguments()).error
21-
binding.wrappedText.text = error.detailedError
22-
binding.unwrappedText.text = error.certificates
29+
30+
binding.addItem("Error: ${error.error}", "Error") { error.error.details() }
31+
32+
error.suppressedErrors.forEach {
33+
binding.addItem("Suppressed: error: $it", "Suppressed error") { it.details() }
34+
}
35+
36+
error.responseInfo?.let { response ->
37+
binding.addItem("HTTP response: ${response.status}", "HTTP response") { response.details() }
38+
}
39+
40+
if (error.serverCertificates.isNotEmpty()) {
41+
binding.addItem("Server certificates", showDetailsMonospaceAndUnwrapped = true) {
42+
error.serverCertificates.joinToString("\n")
43+
}
44+
}
45+
if (error.clientCertificates.isNotEmpty()) {
46+
binding.addItem("Client certificates", showDetailsMonospaceAndUnwrapped = true) {
47+
error.clientCertificates.joinToString("\n")
48+
}
49+
}
50+
if (error.requestHeaders.isNotEmpty()) {
51+
binding.addItem("HTTP request headers") {
52+
error.requestHeaders.joinToString("\n") { header ->
53+
val (name, value) = header.redactHeader()
54+
"$name: $value"
55+
}
56+
}
57+
}
58+
2359
return builder.setView(binding.root)
2460
.setTitle(R.string.detailed_error_message)
2561
.setNeutralButton(R.string.share) { _, _ ->
2662
Utils.shareText(
27-
error.detailedError + error.certificates,
63+
error.makeShareString(),
2864
requireContext().getText(R.string.share),
2965
requireContext()
3066
)
3167
}
3268
.setNegativeButton(R.string.close, null).create()
3369
}
70+
71+
private fun DetailedConnectionErrorDialogBinding.addItem(
72+
summary: String,
73+
detailsTitle: String = summary,
74+
showDetailsMonospaceAndUnwrapped: Boolean = false,
75+
detailsText: () -> String,
76+
) {
77+
items.addView(
78+
MaterialDivider(requireContext()),
79+
ViewGroup.LayoutParams.MATCH_PARENT,
80+
ViewGroup.LayoutParams.WRAP_CONTENT
81+
)
82+
val view = LayoutInflater.from(requireContext())
83+
.inflate(R.layout.detailed_connection_error_dialog_item, items, false) as TextView
84+
items.addView(view)
85+
view.text = summary
86+
view.setOnClickListener {
87+
navigate(
88+
DetailedConnectionErrorDialogFragmentDirections.toExpandedErrorDialogFragment(
89+
detailsTitle,
90+
detailsText(),
91+
showDetailsMonospaceAndUnwrapped
92+
)
93+
)
94+
}
95+
}
96+
97+
private companion object {
98+
fun Throwable.details(): String = buildString {
99+
append("${this@details}\n")
100+
for (cause in causes) {
101+
append("\nCaused by:\n$cause\n")
102+
}
103+
}
104+
105+
fun DetailedRpcRequestError.ResponseInfo.details(): String = buildString {
106+
append("Status: $status\n")
107+
append("Protocol: $protocol\n")
108+
tlsHandshakeInfo?.let { handshake ->
109+
append("TLS version: ${handshake.tlsVersion}\n")
110+
append("Cipher suite: ${handshake.cipherSuite}\n")
111+
}
112+
append("Headers:\n")
113+
headers.forEach { header ->
114+
val (name, value) = header.redactHeader()
115+
append(" $name: $value\n")
116+
}
117+
}
118+
119+
fun DetailedRpcRequestError.makeShareString(): String = buildString {
120+
append("Error:\n")
121+
append(error.details().indent())
122+
appendLine()
123+
suppressedErrors.forEach {
124+
append("Suppressed error:\n")
125+
append(it.details().indent())
126+
appendLine()
127+
}
128+
responseInfo?.let {
129+
append("HTTP response:\n")
130+
append(it.details().indent())
131+
appendLine()
132+
}
133+
if (serverCertificates.isNotEmpty()) {
134+
append("Server certificates:\n")
135+
append(serverCertificates.joinToString("\n").indent())
136+
appendLine()
137+
}
138+
if (clientCertificates.isNotEmpty()) {
139+
append("Client certificates:\n")
140+
append(clientCertificates.joinToString("\n").indent())
141+
appendLine()
142+
}
143+
if (requestHeaders.isNotEmpty()) {
144+
append("HTTP request headers:\n")
145+
append(requestHeaders.joinToString("\n") { header ->
146+
val (name, value) = header.redactHeader()
147+
"$name: $value"
148+
}.indent())
149+
appendLine()
150+
}
151+
}
152+
153+
fun String.indent(): String =
154+
lineSequence()
155+
.map {
156+
when {
157+
it.isBlank() -> it
158+
else -> " $it"
159+
}
160+
}
161+
.joinToString("\n")
162+
}
163+
}
164+
165+
class DetailedConnectionErrorExpandedDialogFragment : NavigationDialogFragment() {
166+
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
167+
val builder = MaterialAlertDialogBuilder(requireContext())
168+
val args = DetailedConnectionErrorExpandedDialogFragmentArgs.fromBundle(requireArguments())
169+
val view = DetailedConnectionErrorExpandedDialogBinding.inflate(LayoutInflater.from(builder.context))
170+
.run {
171+
text.apply {
172+
text = args.text
173+
if (args.monospaceAndUnwrapped) {
174+
setTypeface(Typeface.MONOSPACE, Typeface.NORMAL)
175+
setHorizontallyScrolling(true)
176+
}
177+
}
178+
root
179+
}
180+
return builder.setTitle(args.title).setView(view).setNegativeButton(R.string.close, null).create()
181+
}
34182
}

app/src/main/kotlin/org/equeim/tremotesf/ui/torrentslistfragment/TorrentsListFragment.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ import org.equeim.tremotesf.rpc.PeriodicServerStateUpdater
3939
import org.equeim.tremotesf.torrentfile.rpc.RpcRequestState
4040
import org.equeim.tremotesf.torrentfile.rpc.Server
4141
import org.equeim.tremotesf.torrentfile.rpc.isRecoverable
42-
import org.equeim.tremotesf.torrentfile.rpc.makeDetailedErrorString
42+
import org.equeim.tremotesf.torrentfile.rpc.makeDetailedError
4343
import org.equeim.tremotesf.torrentfile.rpc.requests.Torrent
4444
import org.equeim.tremotesf.ui.NavigationFragment
4545
import org.equeim.tremotesf.ui.RemoveTorrentDialogFragment
@@ -115,7 +115,7 @@ class TorrentsListFragment : NavigationFragment(
115115

116116
binding.placeholderView.detailedErrorMessageButton.setOnClickListener {
117117
(model.torrentsListState.value as? RpcRequestState.Error)?.let { error ->
118-
navigate(NavMainDirections.toDetailedConnectionErrorDialogFragment(error.error.makeDetailedErrorString()))
118+
navigate(NavMainDirections.toDetailedConnectionErrorDialogFragment(error.error.makeDetailedError(GlobalRpcClient)))
119119
}
120120
}
121121

app/src/main/kotlin/org/equeim/tremotesf/ui/utils/PlaceholderLayoutUtils.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@ import androidx.core.view.isVisible
88
import org.equeim.tremotesf.NavMainDirections
99
import org.equeim.tremotesf.R
1010
import org.equeim.tremotesf.databinding.PlaceholderLayoutBinding
11+
import org.equeim.tremotesf.rpc.GlobalRpcClient
1112
import org.equeim.tremotesf.rpc.getErrorString
1213
import org.equeim.tremotesf.torrentfile.rpc.RpcRequestError
13-
import org.equeim.tremotesf.torrentfile.rpc.makeDetailedErrorString
14+
import org.equeim.tremotesf.torrentfile.rpc.makeDetailedError
1415
import org.equeim.tremotesf.ui.NavigationActivity
1516

1617
fun PlaceholderLayoutBinding.showError(error: RpcRequestError) {
@@ -27,7 +28,7 @@ fun PlaceholderLayoutBinding.showError(error: RpcRequestError) {
2728
setOnClickListener {
2829
(context.activity as NavigationActivity).navigate(
2930
NavMainDirections.toDetailedConnectionErrorDialogFragment(
30-
error.makeDetailedErrorString()
31+
error.makeDetailedError(GlobalRpcClient)
3132
)
3233
)
3334
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<!--
2+
SPDX-FileCopyrightText: 2017-2023 Alexey Rochev <[email protected]>
3+
4+
SPDX-License-Identifier: GPL-3.0-or-later
5+
-->
6+
7+
<vector xmlns:android="http://schemas.android.com/apk/res/android"
8+
android:width="24dp"
9+
android:height="24dp"
10+
android:tint="@color/icon_color"
11+
android:viewportWidth="24"
12+
android:viewportHeight="24">
13+
<path
14+
android:fillColor="#000000"
15+
android:pathData="M11,7h2v2h-2zM11,11h2v6h-2zM12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8z" />
16+
</vector>

app/src/main/res/layout/detailed_connection_error_dialog.xml

Lines changed: 2 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ SPDX-License-Identifier: GPL-3.0-or-later
77
-->
88

99
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
10-
xmlns:tools="http://schemas.android.com/tools"
1110
android:layout_width="match_parent"
1211
android:layout_height="match_parent"
1312
android:paddingTop="@dimen/vertical_edge_padding">
@@ -18,36 +17,9 @@ SPDX-License-Identifier: GPL-3.0-or-later
1817
android:scrollbars="vertical">
1918

2019
<LinearLayout
20+
android:id="@+id/items"
2121
android:layout_width="match_parent"
2222
android:layout_height="wrap_content"
23-
android:orientation="vertical">
24-
25-
<TextView
26-
android:id="@+id/wrapped_text"
27-
android:layout_width="match_parent"
28-
android:layout_height="wrap_content"
29-
android:fontFamily="monospace"
30-
android:paddingHorizontal="?dialogPreferredPadding"
31-
android:paddingTop="@dimen/vertical_edge_padding"
32-
android:textAppearance="?textAppearanceBodyMedium"
33-
android:textIsSelectable="true" />
34-
35-
<HorizontalScrollView
36-
android:layout_width="match_parent"
37-
android:layout_height="wrap_content"
38-
android:scrollbars="horizontal"
39-
tools:ignore="UselessParent">
40-
41-
<TextView
42-
android:id="@+id/unwrapped_text"
43-
android:layout_width="wrap_content"
44-
android:layout_height="wrap_content"
45-
android:fontFamily="monospace"
46-
android:paddingHorizontal="?dialogPreferredPadding"
47-
android:paddingBottom="@dimen/vertical_edge_padding"
48-
android:textAppearance="?textAppearanceBodyMedium"
49-
android:textIsSelectable="true" />
50-
</HorizontalScrollView>
51-
</LinearLayout>
23+
android:orientation="vertical" />
5224
</androidx.core.widget.NestedScrollView>
5325
</FrameLayout>
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
3+
<!--
4+
SPDX-FileCopyrightText: 2017-2023 Alexey Rochev <[email protected]>
5+
6+
SPDX-License-Identifier: GPL-3.0-or-later
7+
-->
8+
9+
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
10+
android:layout_width="match_parent"
11+
android:layout_height="wrap_content"
12+
android:background="?selectableItemBackground"
13+
android:clickable="true"
14+
android:focusable="true"
15+
android:maxLines="3"
16+
android:ellipsize="end"
17+
xmlns:app="http://schemas.android.com/apk/res-auto"
18+
android:textAppearance="?textAppearanceBodyMedium"
19+
android:paddingHorizontal="?dialogPreferredPadding"
20+
android:paddingVertical="@dimen/linear_layout_vertical_spacing_double"
21+
app:drawableEndCompat="@drawable/outline_info_24" />
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
3+
<!--
4+
SPDX-FileCopyrightText: 2017-2023 Alexey Rochev <[email protected]>
5+
6+
SPDX-License-Identifier: GPL-3.0-or-later
7+
-->
8+
9+
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
10+
android:layout_width="match_parent"
11+
android:layout_height="match_parent"
12+
android:paddingTop="@dimen/vertical_edge_padding">
13+
14+
<androidx.core.widget.NestedScrollView
15+
android:layout_width="match_parent"
16+
android:layout_height="match_parent"
17+
android:scrollbars="vertical">
18+
19+
<TextView
20+
android:id="@+id/text"
21+
android:layout_width="match_parent"
22+
android:layout_height="wrap_content"
23+
android:scrollHorizontally="true"
24+
android:paddingHorizontal="?dialogPreferredPadding"
25+
android:paddingVertical="@dimen/vertical_edge_padding"
26+
android:textAppearance="?textAppearanceBodyMedium"
27+
android:textIsSelectable="true" />
28+
</androidx.core.widget.NestedScrollView>
29+
</FrameLayout>

app/src/main/res/navigation/nav_main.xml

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -380,7 +380,26 @@
380380

381381
<argument
382382
android:name="error"
383-
app:argType="org.equeim.tremotesf.torrentfile.rpc.DetailedRpcRequestErrorString" />
383+
app:argType="org.equeim.tremotesf.torrentfile.rpc.DetailedRpcRequestError" />
384+
385+
<action
386+
android:id="@+id/to_expanded_error_dialog_fragment"
387+
app:destination="@id/detailed_connection_error_expanded_dialog_fragment" />
388+
</dialog>
389+
390+
<dialog
391+
android:id="@+id/detailed_connection_error_expanded_dialog_fragment"
392+
android:name="org.equeim.tremotesf.ui.torrentslistfragment.DetailedConnectionErrorExpandedDialogFragment">
393+
394+
<argument
395+
android:name="title"
396+
app:argType="string" />
397+
<argument
398+
android:name="text"
399+
app:argType="string" />
400+
<argument
401+
android:name="monospace_and_unwrapped"
402+
app:argType="boolean" />
384403
</dialog>
385404

386405
<action
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
// SPDX-FileCopyrightText: 2017-2023 Alexey Rochev <[email protected]>
2+
//
3+
// SPDX-License-Identifier: GPL-3.0-or-later
4+
5+
package org.equeim.tremotesf.common
6+
7+
val Throwable.causes: Sequence<Throwable> get() = generateSequence(cause, Throwable::cause)

0 commit comments

Comments
 (0)