Skip to content

Commit 1bd2a8f

Browse files
authored
CMM-844/CMM-842: help center and logs in the new support screen (#22290)
* Adding Help Center link * Adding version name * Creating logs application * Navigation and fixes * Navigation tests * Error handling * Adding LogsViewModelTest * Adding share button * detekt * Copilot nitpicks * Minor preview code refactor * Some PR blockers and suggestions * UI suggestions Individual sharing, no logs count, and more plan style * Detekt * Fixing tests
1 parent 651e490 commit 1bd2a8f

File tree

12 files changed

+1084
-13
lines changed

12 files changed

+1084
-13
lines changed

WordPress/src/main/AndroidManifest.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -444,6 +444,10 @@
444444
android:theme="@style/WordPress.NoActionBar"
445445
android:label="@string/support_screen_title"/>
446446

447+
<activity android:name="org.wordpress.android.support.logs.ui.LogsActivity"
448+
android:theme="@style/WordPress.NoActionBar"
449+
android:label="@string/support_screen_application_logs_title"/>
450+
447451
<activity android:name="org.wordpress.android.support.he.ui.HESupportActivity"
448452
android:theme="@style/WordPress.NoActionBar"
449453
android:label="@string/support_screen_title"/>
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package org.wordpress.android.support.logs.model
2+
3+
data class LogDay(
4+
val date: String, // e.g., "Oct-16"
5+
val displayDate: String, // e.g., "October 16"
6+
val logEntries: List<String>,
7+
val logCount: Int
8+
)
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
package org.wordpress.android.support.logs.ui
2+
3+
import android.content.res.Configuration
4+
import androidx.compose.foundation.layout.Column
5+
import androidx.compose.foundation.layout.Spacer
6+
import androidx.compose.foundation.layout.fillMaxSize
7+
import androidx.compose.foundation.layout.fillMaxWidth
8+
import androidx.compose.foundation.layout.height
9+
import androidx.compose.foundation.layout.padding
10+
import androidx.compose.foundation.lazy.LazyColumn
11+
import androidx.compose.foundation.lazy.itemsIndexed
12+
import androidx.compose.material3.Card
13+
import androidx.compose.material3.CardDefaults
14+
import androidx.compose.material3.ExperimentalMaterial3Api
15+
import androidx.compose.material3.FloatingActionButton
16+
import androidx.compose.material3.Icon
17+
import androidx.compose.material3.MaterialTheme
18+
import androidx.compose.material3.Scaffold
19+
import androidx.compose.material3.Text
20+
import androidx.compose.runtime.Composable
21+
import androidx.compose.ui.Modifier
22+
import androidx.compose.ui.res.painterResource
23+
import androidx.compose.ui.res.stringResource
24+
import androidx.compose.ui.text.font.FontFamily
25+
import androidx.compose.ui.tooling.preview.Preview
26+
import androidx.compose.ui.unit.dp
27+
import androidx.compose.ui.unit.sp
28+
import androidx.core.text.HtmlCompat
29+
import org.wordpress.android.R
30+
import org.wordpress.android.support.logs.model.LogDay
31+
import org.wordpress.android.ui.compose.components.MainTopAppBar
32+
import org.wordpress.android.ui.compose.components.NavigationIcons
33+
import org.wordpress.android.ui.compose.theme.AppThemeM3
34+
35+
@OptIn(ExperimentalMaterial3Api::class)
36+
@Composable
37+
fun LogDetailScreen(
38+
logDay: LogDay,
39+
onBackClick: () -> Unit,
40+
onShareClick: () -> Unit
41+
) {
42+
Scaffold(
43+
topBar = {
44+
MainTopAppBar(
45+
title = logDay.displayDate,
46+
navigationIcon = NavigationIcons.BackIcon,
47+
onNavigationIconClick = onBackClick
48+
)
49+
},
50+
floatingActionButton = {
51+
FloatingActionButton(
52+
onClick = onShareClick,
53+
containerColor = MaterialTheme.colorScheme.primaryContainer,
54+
contentColor = MaterialTheme.colorScheme.onPrimaryContainer
55+
) {
56+
Icon(
57+
painter = painterResource(R.drawable.ic_share_white_24dp),
58+
contentDescription = stringResource(R.string.reader_btn_share)
59+
)
60+
}
61+
}
62+
) { contentPadding ->
63+
Card(
64+
modifier = Modifier.fillMaxWidth(),
65+
colors = CardDefaults.cardColors(
66+
containerColor = MaterialTheme.colorScheme.surfaceVariant
67+
),
68+
elevation = CardDefaults.cardElevation(defaultElevation = 0.dp)
69+
) {
70+
LazyColumn(
71+
modifier = Modifier
72+
.fillMaxSize()
73+
.padding(contentPadding)
74+
.padding(horizontal = 16.dp)
75+
) {
76+
item {
77+
Spacer(modifier = Modifier.height(16.dp))
78+
}
79+
80+
itemsIndexed(
81+
items = logDay.logEntries,
82+
key = { index, _ -> "${logDay.date}_$index" }
83+
) { _, logEntry ->
84+
LogEntryItem(logEntry = logEntry)
85+
}
86+
87+
item {
88+
Spacer(modifier = Modifier.height(16.dp))
89+
}
90+
}
91+
}
92+
}
93+
}
94+
95+
@Composable
96+
private fun LogEntryItem(logEntry: String) {
97+
Column(
98+
modifier = Modifier
99+
.fillMaxWidth()
100+
) {
101+
// Strip HTML tags for display in Compose
102+
val plainText = HtmlCompat.fromHtml(
103+
logEntry,
104+
HtmlCompat.FROM_HTML_MODE_LEGACY
105+
).toString()
106+
107+
Text(
108+
text = plainText,
109+
style = MaterialTheme.typography.bodySmall.copy(
110+
fontFamily = FontFamily.Monospace,
111+
fontSize = 12.sp,
112+
lineHeight = 16.sp
113+
),
114+
color = MaterialTheme.colorScheme.onSurfaceVariant
115+
)
116+
}
117+
}
118+
119+
@Preview(showBackground = true, name = "Log Detail Screen - Light")
120+
@Composable
121+
private fun LogDetailScreenPreview() {
122+
val exampleList = getExampleLogList()
123+
AppThemeM3(isDarkTheme = false) {
124+
LogDetailScreen(
125+
logDay = LogDay(
126+
date = "Oct-16",
127+
displayDate = "October 16",
128+
logEntries = exampleList,
129+
logCount = exampleList.size
130+
),
131+
onBackClick = {},
132+
onShareClick = {}
133+
)
134+
}
135+
}
136+
137+
@Preview(showBackground = true, name = "Log Detail Screen - Dark", uiMode = Configuration.UI_MODE_NIGHT_YES)
138+
@Composable
139+
private fun LogDetailScreenPreviewDark() {
140+
val exampleList = getExampleLogList()
141+
AppThemeM3(isDarkTheme = true) {
142+
LogDetailScreen(
143+
logDay = LogDay(
144+
date = "Oct-16",
145+
displayDate = "October 16",
146+
logEntries = exampleList,
147+
logCount = exampleList.size
148+
),
149+
onBackClick = {},
150+
onShareClick = {}
151+
)
152+
}
153+
}
154+
155+
private fun getExampleLogList(): List<String> = listOf(
156+
"[Oct-16 12:34:56.789] D/MainActivity: Activity created",
157+
"[Oct-16 12:34:57.123] I/NetworkManager: Connection established",
158+
"[Oct-16 12:34:58.456] W/ImageLoader: Cache miss for image_123",
159+
"[Oct-16 12:35:00.789] E/ApiClient: Request failed with status 404",
160+
"[Oct-16 12:35:01.234] D/ViewModel: Data loaded successfully",
161+
"[Oct-16 12:35:02.567] I/Analytics: Event tracked: button_clicked",
162+
"[Oct-16 12:35:03.890] D/Database: Query executed in 45ms",
163+
"[Oct-16 12:35:04.123] W/Memory: Low memory warning detected"
164+
)
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
package org.wordpress.android.support.logs.ui
2+
3+
import android.content.Context
4+
import android.content.Intent
5+
import android.os.Build
6+
import android.os.Bundle
7+
import android.view.Gravity
8+
import androidx.activity.viewModels
9+
import androidx.appcompat.app.AppCompatActivity
10+
import androidx.compose.runtime.Composable
11+
import androidx.compose.runtime.LaunchedEffect
12+
import androidx.compose.runtime.collectAsState
13+
import androidx.compose.runtime.getValue
14+
import androidx.compose.ui.platform.ComposeView
15+
import androidx.compose.ui.platform.ViewCompositionStrategy
16+
import androidx.lifecycle.Lifecycle
17+
import androidx.lifecycle.lifecycleScope
18+
import androidx.lifecycle.repeatOnLifecycle
19+
import androidx.navigation.NavHostController
20+
import androidx.navigation.compose.NavHost
21+
import androidx.navigation.compose.composable
22+
import androidx.navigation.compose.rememberNavController
23+
import dagger.hilt.android.AndroidEntryPoint
24+
import kotlinx.coroutines.launch
25+
import org.wordpress.android.R
26+
import org.wordpress.android.fluxc.utils.AppLogWrapper
27+
import org.wordpress.android.ui.compose.theme.AppThemeM3
28+
import org.wordpress.android.util.AppLog
29+
import org.wordpress.android.util.ToastUtils
30+
import javax.inject.Inject
31+
32+
@AndroidEntryPoint
33+
class LogsActivity : AppCompatActivity() {
34+
private val viewModel by viewModels<LogsViewModel>()
35+
36+
private lateinit var composeView: ComposeView
37+
private var navController: NavHostController? = null
38+
39+
@Inject
40+
lateinit var appLogWrapper: AppLogWrapper
41+
42+
override fun onCreate(savedInstanceState: Bundle?) {
43+
super.onCreate(savedInstanceState)
44+
viewModel.init()
45+
composeView = ComposeView(this)
46+
setContentView(
47+
composeView.apply {
48+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
49+
this.isForceDarkAllowed = false
50+
}
51+
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
52+
setContent {
53+
NavigableContent()
54+
}
55+
}
56+
)
57+
observeErrorMessages()
58+
observeNavigationEvents()
59+
observeActionEvents()
60+
}
61+
62+
private fun observeErrorMessages() {
63+
lifecycleScope.launch {
64+
repeatOnLifecycle(Lifecycle.State.STARTED) {
65+
viewModel.errorMessage.collect { errorType ->
66+
val errorMessage = when (errorType) {
67+
LogsViewModel.ErrorType.GENERAL -> getString(R.string.logs_screen_general_error)
68+
null -> null
69+
}
70+
errorMessage?.let {
71+
ToastUtils.showToast(this@LogsActivity, it, ToastUtils.Duration.LONG, Gravity.CENTER)
72+
viewModel.clearError()
73+
}
74+
}
75+
}
76+
}
77+
}
78+
79+
private fun observeNavigationEvents() {
80+
lifecycleScope.launch {
81+
repeatOnLifecycle(Lifecycle.State.STARTED) {
82+
viewModel.navigationEvents.collect { event ->
83+
when (event) {
84+
is LogsViewModel.NavigationEvent.NavigateToDetail -> {
85+
navController?.navigate(LogsScreen.Detail.name)
86+
}
87+
}
88+
}
89+
}
90+
}
91+
}
92+
93+
private fun observeActionEvents() {
94+
lifecycleScope.launch {
95+
repeatOnLifecycle(Lifecycle.State.STARTED) {
96+
viewModel.actionEvents.collect { event ->
97+
when (event) {
98+
is LogsViewModel.ActionEvent.ShareLogDay -> shareLogDay(event.logDay, event.date)
99+
}
100+
}
101+
}
102+
}
103+
}
104+
105+
private enum class LogsScreen {
106+
List,
107+
Detail
108+
}
109+
110+
@Composable
111+
private fun NavigableContent() {
112+
navController = rememberNavController()
113+
114+
AppThemeM3 {
115+
NavHost(
116+
navController = navController!!,
117+
startDestination = LogsScreen.List.name
118+
) {
119+
composable(route = LogsScreen.List.name) {
120+
val logDays by viewModel.logDays.collectAsState()
121+
LogsListScreen(
122+
logDays = logDays,
123+
onLogDayClick = { logDay -> viewModel.onLogDayClick(logDay) },
124+
onBackClick = { finish() }
125+
)
126+
}
127+
128+
composable(route = LogsScreen.Detail.name) {
129+
val selectedLogDay by viewModel.selectedLogDay.collectAsState()
130+
selectedLogDay?.let { logDay ->
131+
LogDetailScreen(
132+
logDay = logDay,
133+
onBackClick = { navController?.navigateUp() },
134+
onShareClick = { viewModel.onShareClick(logDay) }
135+
)
136+
} ?: run {
137+
LaunchedEffect(Unit) {
138+
navController?.navigateUp()
139+
}
140+
}
141+
}
142+
}
143+
}
144+
}
145+
146+
private fun shareLogDay(logDay: String, date: String) {
147+
val subject = "${getString(R.string.app_name)} " +
148+
"${getString(R.string.support_screen_application_logs_title)} - $date"
149+
val intent = Intent(Intent.ACTION_SEND).apply {
150+
type = "text/plain"
151+
putExtra(Intent.EXTRA_TEXT, logDay)
152+
putExtra(Intent.EXTRA_SUBJECT, subject)
153+
}
154+
try {
155+
startActivity(Intent.createChooser(intent, getString(R.string.reader_btn_share)))
156+
} catch (ex: android.content.ActivityNotFoundException) {
157+
ToastUtils.showToast(this, R.string.reader_toast_err_share_intent)
158+
appLogWrapper.e(AppLog.T.SUPPORT, "Error sharing logs: ${ex.stackTraceToString()}")
159+
}
160+
}
161+
162+
companion object {
163+
@JvmStatic
164+
fun createIntent(context: Context): Intent = Intent(context, LogsActivity::class.java)
165+
}
166+
}

0 commit comments

Comments
 (0)