Skip to content

Commit 60a52ba

Browse files
committed
feat(diagnosis): add "Share diagnosis report" to Diagnosis and Logs menus
Extract the share/download/create-issue dialog from SettingsActivity into a shared ShareDiagnosisReportDialog so DiagnosisActivity and LogsActivity can open the same dialog from their overflow menu instead of only being reachable via Settings. Assisted-by: Claude Code:claude-sonnet-5 Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
1 parent 4bed50a commit 60a52ba

4 files changed

Lines changed: 316 additions & 172 deletions

File tree

app/src/main/java/com/nextcloud/talk/diagnosis/DiagnosisActivity.kt

Lines changed: 98 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -11,21 +11,26 @@ import android.content.ClipboardManager
1111
import android.os.Bundle
1212
import android.widget.Toast
1313
import androidx.activity.compose.setContent
14+
import androidx.activity.result.contract.ActivityResultContracts
1415
import androidx.compose.foundation.background
1516
import androidx.compose.foundation.layout.Column
1617
import androidx.compose.foundation.layout.displayCutoutPadding
1718
import androidx.compose.foundation.layout.fillMaxSize
1819
import androidx.compose.foundation.layout.padding
1920
import androidx.compose.foundation.layout.statusBarsPadding
21+
import androidx.compose.material3.ColorScheme
2022
import androidx.compose.material3.MaterialTheme
2123
import androidx.compose.material3.Scaffold
24+
import androidx.compose.runtime.Composable
25+
import androidx.compose.runtime.MutableState
2226
import androidx.compose.runtime.collectAsState
2327
import androidx.compose.runtime.mutableStateOf
2428
import androidx.compose.ui.Modifier
2529
import androidx.compose.ui.res.colorResource
2630
import androidx.compose.ui.res.stringResource
2731
import androidx.compose.ui.unit.dp
2832
import androidx.lifecycle.ViewModelProvider
33+
import androidx.lifecycle.lifecycleScope
2934
import autodagger.AutoInjector
3035
import com.nextcloud.talk.R
3136
import com.nextcloud.talk.activities.BaseActivity
@@ -35,11 +40,14 @@ import com.nextcloud.talk.arbitrarystorage.ArbitraryStorageManager
3540
import com.nextcloud.talk.components.ColoredStatusBar
3641
import com.nextcloud.talk.components.StandardAppBar
3742
import com.nextcloud.talk.data.network.NetworkMonitor
43+
import com.nextcloud.talk.errorhandling.saveLogsAsZip
3844
import com.nextcloud.talk.logger.LogsRepository
3945
import com.nextcloud.talk.users.UserManager
4046
import com.nextcloud.talk.utils.ClosedInterfaceImpl
4147
import com.nextcloud.talk.utils.UnifiedPushUtils
4248
import com.nextcloud.talk.utils.permissions.PlatformPermissionUtil
49+
import kotlinx.coroutines.Dispatchers
50+
import kotlinx.coroutines.launch
4351
import javax.inject.Inject
4452

4553
@AutoInjector(NextcloudTalkApplication::class)
@@ -69,6 +77,18 @@ class DiagnosisActivity : BaseActivity() {
6977
private val diagnosisData = mutableListOf<DiagnosisElement>()
7078
private val diagnosisDataState = mutableStateOf(emptyList<DiagnosisElement>())
7179

80+
private val saveZipLauncher =
81+
registerForActivityResult(ActivityResultContracts.CreateDocument("application/zip")) { uri ->
82+
if (uri != null) {
83+
val diagnosisText = buildDiagnosisReportText(this, userManager, appPreferences, logsRepository)
84+
lifecycleScope.launch(Dispatchers.IO) {
85+
contentResolver.openOutputStream(uri)?.use { os ->
86+
saveLogsAsZip(this@DiagnosisActivity, os, diagnosisText)
87+
}
88+
}
89+
}
90+
}
91+
7292
override fun onCreate(savedInstanceState: Bundle?) {
7393
super.onCreate(savedInstanceState)
7494
NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this)
@@ -81,59 +101,19 @@ class DiagnosisActivity : BaseActivity() {
81101
val isGooglePlayServicesAvailable = ClosedInterfaceImpl().isGooglePlayServicesAvailable
82102
val useUnifiedPush = appPreferences.useUnifiedPush
83103
val useEmbeddedDistrib = UnifiedPushUtils.hasEmbeddedDistributor(context) && !useUnifiedPush
104+
val showTestPushButton = isGooglePlayServicesAvailable || useUnifiedPush || useEmbeddedDistrib
84105

85106
setContent {
86-
val backgroundColor = colorResource(id = R.color.bg_default)
87-
88-
val menuItems = listOf(
89-
stringResource(R.string.nc_common_copy) to { copyToClipboard(diagnosisData.toMarkdown()) }
107+
DiagnosisScreen(
108+
colorScheme = colorScheme,
109+
networkMonitor = networkMonitor,
110+
diagnosisData = diagnosisData,
111+
diagnosisDataState = diagnosisDataState,
112+
diagnosisViewModel = diagnosisViewModel,
113+
showTestPushButton = showTestPushButton,
114+
onCopyClick = ::copyToClipboard,
115+
onShareReportClick = ::openShareReportDialog
90116
)
91-
92-
MaterialTheme(
93-
colorScheme = colorScheme
94-
) {
95-
val isOnline = networkMonitor.isOnline.collectAsState().value
96-
ColoredStatusBar()
97-
Scaffold(
98-
modifier = Modifier
99-
.statusBarsPadding()
100-
.displayCutoutPadding(),
101-
topBar = {
102-
StandardAppBar(
103-
title = stringResource(R.string.nc_settings_diagnosis_title),
104-
menuItems
105-
)
106-
},
107-
content = { paddingValues ->
108-
val viewState = diagnosisViewModel.notificationViewState.collectAsState().value
109-
110-
Column(
111-
Modifier
112-
.background(backgroundColor)
113-
.padding(
114-
0.dp,
115-
paddingValues.calculateTopPadding(),
116-
0.dp,
117-
paddingValues.calculateBottomPadding()
118-
)
119-
.fillMaxSize()
120-
) {
121-
DiagnosisContentComposable(
122-
diagnosisDataState,
123-
isLoading = diagnosisViewModel.isLoading.value,
124-
showDialog = diagnosisViewModel.showDialog.value,
125-
viewState = viewState,
126-
onTestPushClick = { diagnosisViewModel.fetchTestPushResult() },
127-
onDismissDialog = { diagnosisViewModel.dismissDialog() },
128-
showTestPushButton = isGooglePlayServicesAvailable ||
129-
useUnifiedPush ||
130-
useEmbeddedDistrib,
131-
isOnline = isOnline
132-
)
133-
}
134-
}
135-
)
136-
}
137117
}
138118
}
139119

@@ -154,6 +134,10 @@ class DiagnosisActivity : BaseActivity() {
154134
diagnosisDataState.value = diagnosisData.toList()
155135
}
156136

137+
private fun openShareReportDialog() {
138+
showShareReportDialog(this, userManager, appPreferences, logsRepository, saveZipLauncher)
139+
}
140+
157141
private fun copyToClipboard(text: String) {
158142
val clipboardManager =
159143
getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
@@ -174,3 +158,67 @@ class DiagnosisActivity : BaseActivity() {
174158
val TAG = DiagnosisActivity::class.java.simpleName
175159
}
176160
}
161+
162+
@Suppress("LongParameterList")
163+
@Composable
164+
private fun DiagnosisScreen(
165+
colorScheme: ColorScheme,
166+
networkMonitor: NetworkMonitor,
167+
diagnosisData: List<DiagnosisElement>,
168+
diagnosisDataState: MutableState<List<DiagnosisElement>>,
169+
diagnosisViewModel: DiagnosisViewModel,
170+
showTestPushButton: Boolean,
171+
onCopyClick: (String) -> Unit,
172+
onShareReportClick: () -> Unit
173+
) {
174+
val backgroundColor = colorResource(id = R.color.bg_default)
175+
176+
val menuItems = listOf(
177+
stringResource(R.string.nc_common_copy) to { onCopyClick(diagnosisData.toMarkdown()) },
178+
stringResource(R.string.nc_settings_share_report_title) to onShareReportClick
179+
)
180+
181+
MaterialTheme(
182+
colorScheme = colorScheme
183+
) {
184+
val isOnline = networkMonitor.isOnline.collectAsState().value
185+
ColoredStatusBar()
186+
Scaffold(
187+
modifier = Modifier
188+
.statusBarsPadding()
189+
.displayCutoutPadding(),
190+
topBar = {
191+
StandardAppBar(
192+
title = stringResource(R.string.nc_settings_diagnosis_title),
193+
menuItems
194+
)
195+
},
196+
content = { paddingValues ->
197+
val viewState = diagnosisViewModel.notificationViewState.collectAsState().value
198+
199+
Column(
200+
Modifier
201+
.background(backgroundColor)
202+
.padding(
203+
0.dp,
204+
paddingValues.calculateTopPadding(),
205+
0.dp,
206+
paddingValues.calculateBottomPadding()
207+
)
208+
.fillMaxSize()
209+
) {
210+
DiagnosisContentComposable(
211+
diagnosisDataState,
212+
isLoading = diagnosisViewModel.isLoading.value,
213+
showDialog = diagnosisViewModel.showDialog.value,
214+
viewState = viewState,
215+
onTestPushClick = { diagnosisViewModel.fetchTestPushResult() },
216+
onDismissDialog = { diagnosisViewModel.dismissDialog() },
217+
showTestPushButton = showTestPushButton,
218+
isOnline = isOnline
219+
)
220+
}
221+
}
222+
)
223+
}
224+
}
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
/*
2+
* Nextcloud Talk - Android Client
3+
*
4+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
5+
* SPDX-License-Identifier: GPL-3.0-or-later
6+
*/
7+
package com.nextcloud.talk.diagnosis
8+
9+
import android.app.Activity
10+
import android.content.ClipData
11+
import android.content.ClipboardManager
12+
import android.content.Context.CLIPBOARD_SERVICE
13+
import android.content.Intent
14+
import android.util.TypedValue
15+
import android.view.View
16+
import android.widget.LinearLayout
17+
import android.widget.ScrollView
18+
import android.widget.TextView
19+
import android.widget.Toast
20+
import androidx.activity.result.ActivityResultLauncher
21+
import androidx.appcompat.app.AlertDialog
22+
import androidx.core.net.toUri
23+
import com.google.android.material.color.MaterialColors
24+
import com.google.android.material.dialog.MaterialAlertDialogBuilder
25+
import com.nextcloud.talk.R
26+
import com.nextcloud.talk.errorhandling.shareLogsAndDiagnosis
27+
import com.nextcloud.talk.logger.LogsRepository
28+
import com.nextcloud.talk.users.UserManager
29+
import com.nextcloud.talk.utils.BrandingUtils
30+
import com.nextcloud.talk.utils.preferences.AppPreferences
31+
32+
private const val DIALOG_PADDING_H_DP = 24
33+
private const val DIALOG_PADDING_V_DP = 16
34+
private const val DIALOG_SPACING_DP = 8
35+
36+
fun showShareReportDialog(
37+
activity: Activity,
38+
userManager: UserManager,
39+
appPreferences: AppPreferences,
40+
logsRepository: LogsRepository,
41+
saveZipLauncher: ActivityResultLauncher<String>
42+
) {
43+
val options = buildShareReportOptions(activity, userManager, appPreferences, logsRepository, saveZipLauncher)
44+
var dialog: AlertDialog? = null
45+
val view = buildShareDialogContentView(activity, options) { dialog?.dismiss() }
46+
dialog = MaterialAlertDialogBuilder(activity)
47+
.setTitle(R.string.nc_settings_share_report_title)
48+
.setView(view)
49+
.show()
50+
}
51+
52+
private fun buildShareReportOptions(
53+
activity: Activity,
54+
userManager: UserManager,
55+
appPreferences: AppPreferences,
56+
logsRepository: LogsRepository,
57+
saveZipLauncher: ActivityResultLauncher<String>
58+
): List<Pair<String, () -> Unit>> {
59+
val options = mutableListOf(
60+
activity.getString(R.string.nc_logs_share) to {
61+
val diagnosisText = buildDiagnosisReportText(activity, userManager, appPreferences, logsRepository)
62+
shareLogsAndDiagnosis(
63+
context = activity,
64+
subject = activity.getString(
65+
R.string.nc_logs_share_subject,
66+
activity.getString(R.string.nc_app_product_name)
67+
),
68+
diagnosisText = diagnosisText
69+
)
70+
},
71+
activity.getString(R.string.nc_logs_download_zip) to {
72+
saveZipLauncher.launch("nc_talk_logs.zip")
73+
}
74+
)
75+
if (BrandingUtils.isOriginalNextcloudClient(activity.applicationContext)) {
76+
options.add(
77+
activity.getString(R.string.create_issue) to {
78+
val diagnosisText = buildDiagnosisReportText(activity, userManager, appPreferences, logsRepository)
79+
val clipboard = activity.getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
80+
clipboard.setPrimaryClip(
81+
ClipData.newPlainText(activity.getString(R.string.nc_app_product_name), diagnosisText)
82+
)
83+
Toast.makeText(
84+
activity,
85+
activity.getString(R.string.nc_common_copy_success),
86+
Toast.LENGTH_LONG
87+
).show()
88+
activity.startActivity(
89+
Intent(Intent.ACTION_VIEW, activity.getString(R.string.nc_talk_android_issues_url).toUri())
90+
)
91+
}
92+
)
93+
}
94+
return options
95+
}
96+
97+
@Suppress("Detekt.LongMethod")
98+
fun buildDiagnosisReportText(
99+
activity: Activity,
100+
userManager: UserManager,
101+
appPreferences: AppPreferences,
102+
logsRepository: LogsRepository
103+
): String =
104+
buildDiagnosisElements(
105+
context = activity,
106+
userManager = userManager,
107+
appPreferences = appPreferences,
108+
logsRepository = logsRepository
109+
).toMarkdown()
110+
111+
@Suppress("LongMethod")
112+
private fun buildShareDialogContentView(
113+
activity: Activity,
114+
options: List<Pair<String, () -> Unit>>,
115+
onDismiss: () -> Unit
116+
): ScrollView {
117+
val density = activity.resources.displayMetrics.density
118+
fun dp(value: Int) = (value * density).toInt()
119+
val selectableBackground = with(TypedValue()) {
120+
activity.theme.resolveAttribute(android.R.attr.selectableItemBackground, this, true)
121+
resourceId
122+
}
123+
val list = LinearLayout(activity).apply {
124+
orientation = LinearLayout.VERTICAL
125+
addView(
126+
TextView(activity).apply {
127+
text = activity.getString(R.string.nc_logs_advanced_logging_privacy_warning)
128+
val h = dp(DIALOG_PADDING_H_DP)
129+
val v = dp(DIALOG_PADDING_V_DP)
130+
setPadding(h, v, h, v)
131+
setTextAppearance(com.google.android.material.R.style.TextAppearance_Material3_BodyMedium)
132+
}
133+
)
134+
addView(
135+
View(activity).apply {
136+
layoutParams = LinearLayout.LayoutParams(
137+
LinearLayout.LayoutParams.MATCH_PARENT,
138+
dp(1)
139+
).also { it.setMargins(0, 0, 0, dp(DIALOG_SPACING_DP)) }
140+
setBackgroundColor(
141+
MaterialColors.getColor(
142+
activity,
143+
com.google.android.material.R.attr.colorOutlineVariant,
144+
0
145+
)
146+
)
147+
}
148+
)
149+
options.forEach { (label, action) ->
150+
addView(
151+
TextView(activity).apply {
152+
text = label
153+
val h = dp(DIALOG_PADDING_H_DP)
154+
val v = dp(DIALOG_PADDING_V_DP)
155+
setPadding(h, v, h, v)
156+
setTextAppearance(com.google.android.material.R.style.TextAppearance_Material3_BodyLarge)
157+
setBackgroundResource(selectableBackground)
158+
isClickable = true
159+
isFocusable = true
160+
setOnClickListener {
161+
onDismiss()
162+
action()
163+
}
164+
}
165+
)
166+
}
167+
addView(
168+
View(activity).apply {
169+
layoutParams = LinearLayout.LayoutParams(
170+
LinearLayout.LayoutParams.MATCH_PARENT,
171+
dp(DIALOG_SPACING_DP)
172+
)
173+
}
174+
)
175+
}
176+
return ScrollView(activity).apply { addView(list) }
177+
}

0 commit comments

Comments
 (0)