Skip to content

Commit c55c636

Browse files
committed
add new compose activity and navdisplay
1 parent 5a6dbfd commit c55c636

File tree

6 files changed

+321
-0
lines changed

6 files changed

+321
-0
lines changed

app/src/main/AndroidManifest.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,10 @@
8080
</intent-filter>
8181
</service>
8282

83+
<activity
84+
android:name=".ComposeActivity"
85+
android:exported="false" />
86+
8387
<activity
8488
android:name=".player.PlayQueueActivity"
8589
android:exported="false"
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/*
2+
* SPDX-FileCopyrightText: 2025-2026 NewPipe e.V. <https://newpipe-ev.de>
3+
* SPDX-License-Identifier: GPL-3.0-or-later
4+
*/
5+
6+
package org.schabi.newpipe
7+
8+
import android.content.Context
9+
import android.content.Intent
10+
import android.graphics.Color
11+
import android.os.Build
12+
import android.os.Bundle
13+
import androidx.activity.ComponentActivity
14+
import androidx.activity.SystemBarStyle
15+
import androidx.activity.compose.setContent
16+
import androidx.activity.enableEdgeToEdge
17+
import androidx.core.content.IntentCompat
18+
import androidx.navigation3.runtime.NavKey
19+
import dagger.hilt.android.AndroidEntryPoint
20+
import org.schabi.newpipe.error.ErrorInfo
21+
import org.schabi.newpipe.navigation.NavDisplay
22+
import org.schabi.newpipe.navigation.Screen
23+
import org.schabi.newpipe.ui.theme.AppTheme
24+
25+
/**
26+
* Single host activity for all Compose-based screens.
27+
* Other parts of the app (including legacy View-based code) launch this activity
28+
* via Intent with extras specifying which screen to display.
29+
*/
30+
@AndroidEntryPoint
31+
class ComposeActivity: ComponentActivity() {
32+
33+
override fun onCreate(savedInstanceState: Bundle?) {
34+
enableEdgeToEdge(
35+
navigationBarStyle = SystemBarStyle.Companion.auto(Color.TRANSPARENT, Color.TRANSPARENT)
36+
)
37+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
38+
window.isNavigationBarContrastEnforced = false
39+
}
40+
super.onCreate(savedInstanceState)
41+
42+
val startDestination: NavKey = resolveStartDestination(intent)
43+
44+
setContent {
45+
AppTheme {
46+
NavDisplay(startDestination)
47+
}
48+
}
49+
}
50+
51+
private fun resolveStartDestination(intent: Intent): NavKey {
52+
return when (intent.getStringExtra(EXTRA_SCREEN)) {
53+
SCREEN_ERROR -> {
54+
val errorInfo = IntentCompat.getParcelableExtra(
55+
intent, EXTRA_ERROR_INFO, ErrorInfo::class.java
56+
) ?: throw IllegalArgumentException("ErrorInfo is required for error screen")
57+
Screen.Error(errorInfo)
58+
}
59+
SCREEN_SETTINGS -> Screen.Settings.Home
60+
else -> throw IllegalArgumentException(
61+
"Unknown screen: ${intent.getStringExtra(EXTRA_SCREEN)}"
62+
)
63+
}
64+
}
65+
66+
companion object {
67+
const val EXTRA_SCREEN = "extra_screen"
68+
const val EXTRA_ERROR_INFO = "extra_error_info"
69+
70+
const val SCREEN_ERROR = "error"
71+
const val SCREEN_SETTINGS = "settings"
72+
73+
/**
74+
* Creates an intent to launch the error report screen.
75+
*/
76+
fun errorIntent(context: Context, errorInfo: ErrorInfo): Intent {
77+
return Intent(context, ComposeActivity::class.java).apply {
78+
putExtra(EXTRA_SCREEN, SCREEN_ERROR)
79+
putExtra(EXTRA_ERROR_INFO, errorInfo)
80+
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
81+
}
82+
}
83+
}
84+
}

app/src/main/java/org/schabi/newpipe/error/ErrorInfo.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import com.google.android.exoplayer2.upstream.HttpDataSource
99
import com.google.android.exoplayer2.upstream.Loader
1010
import java.net.UnknownHostException
1111
import kotlinx.parcelize.Parcelize
12+
import kotlinx.serialization.Serializable
1213
import org.schabi.newpipe.R
1314
import org.schabi.newpipe.extractor.Info
1415
import org.schabi.newpipe.extractor.ServiceList
@@ -35,6 +36,7 @@ import org.schabi.newpipe.util.text.getText
3536
* An error has occurred in the app. This class contains plain old parcelable data that can be used
3637
* to report the error and to show it to the user along with correct action buttons.
3738
*/
39+
@Serializable
3840
@Parcelize
3941
class ErrorInfo private constructor(
4042
val stackTraces: Array<String>,
@@ -141,6 +143,7 @@ class ErrorInfo private constructor(
141143
}
142144

143145
companion object {
146+
@Serializable
144147
@Parcelize
145148
class ErrorMessage(
146149
@StringRes
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
/*
2+
* SPDX-FileCopyrightText: 2015-2026 NewPipe contributors <https://newpipe.net>
3+
* SPDX-License-Identifier: GPL-3.0-or-later
4+
*/
5+
6+
package org.schabi.newpipe.error
7+
8+
import android.content.Context
9+
import android.content.Intent
10+
import android.os.Build
11+
import android.util.Log
12+
import androidx.core.net.toUri
13+
import com.grack.nanojson.JsonWriter
14+
import org.schabi.newpipe.BuildConfig
15+
import org.schabi.newpipe.R
16+
import org.schabi.newpipe.util.Localization
17+
import org.schabi.newpipe.util.external_communication.ShareUtils
18+
import java.time.ZonedDateTime
19+
import java.time.format.DateTimeFormatter
20+
21+
/**
22+
* Pure utility functions for building and sending error reports.
23+
* No Activity dependency — only requires a [Context].
24+
*/
25+
object ErrorReportHelper {
26+
27+
private val TAG = ErrorReportHelper::class.java.simpleName
28+
29+
private const val ERROR_EMAIL_ADDRESS = "crashreport@newpipe.schabi.org"
30+
private const val ERROR_EMAIL_SUBJECT = "Exception in "
31+
const val ERROR_GITHUB_ISSUE_URL = "https://github.com/TeamNewPipe/NewPipe/issues"
32+
33+
fun buildJson(context: Context, errorInfo: ErrorInfo, comment: String): String {
34+
try {
35+
return JsonWriter.string()
36+
.`object`()
37+
.value("user_action", errorInfo.userAction.message)
38+
.value("request", errorInfo.request)
39+
.value("content_language", getContentLanguage(context))
40+
.value("content_country", getContentCountry(context))
41+
.value("app_language", getAppLanguage())
42+
.value("service", errorInfo.getServiceName())
43+
.value("package", context.packageName)
44+
.value("version", BuildConfig.VERSION_NAME)
45+
.value("os", getOsString())
46+
.value("time", getCurrentTimestamp())
47+
.array("exceptions", errorInfo.stackTraces.toList())
48+
.value("user_comment", comment)
49+
.end()
50+
.done()
51+
} catch (e: Exception) {
52+
Log.e(TAG, "Could not build json", e)
53+
}
54+
return ""
55+
}
56+
57+
fun buildMarkdown(context: Context, errorInfo: ErrorInfo, comment: String): String {
58+
try {
59+
return buildString(1024) {
60+
if (comment.isNotEmpty()) {
61+
appendLine(comment)
62+
}
63+
64+
appendLine("## Exception")
65+
appendLine("* __User Action:__ ${errorInfo.userAction.message}")
66+
appendLine("* __Request:__ ${errorInfo.request}")
67+
appendLine("* __Content Country:__ ${getContentCountry(context)}")
68+
appendLine("* __Content Language:__ ${getContentLanguage(context)}")
69+
appendLine("* __App Language:__ ${getAppLanguage()}")
70+
appendLine("* __Service:__ ${errorInfo.getServiceName()}")
71+
appendLine("* __Timestamp:__ ${getCurrentTimestamp()}")
72+
appendLine("* __Package:__ ${context.packageName}")
73+
appendLine("* __Version:__ ${BuildConfig.VERSION_NAME}")
74+
appendLine("* __OS:__ ${getOsString()}")
75+
76+
if (errorInfo.stackTraces.size > 1) {
77+
append("<details><summary><b>Exceptions (")
78+
append(errorInfo.stackTraces.size)
79+
append(")</b></summary><p>\n")
80+
}
81+
82+
errorInfo.stackTraces.forEachIndexed { index, stacktrace ->
83+
append("<details><summary><b>Crash log ")
84+
if (errorInfo.stackTraces.size > 1) {
85+
append(index + 1)
86+
}
87+
append("</b></summary><p>\n")
88+
append("\n```\n$stacktrace\n```\n")
89+
append("</details>\n")
90+
}
91+
92+
if (errorInfo.stackTraces.size > 1) {
93+
append("</p></details>\n")
94+
}
95+
96+
append("<hr>\n")
97+
}
98+
} catch (e: Exception) {
99+
Log.e(TAG, "Could not build markdown", e)
100+
return ""
101+
}
102+
}
103+
104+
fun sendErrorEmail(context: Context, errorInfo: ErrorInfo, comment: String) {
105+
val subject = "$ERROR_EMAIL_SUBJECT${context.getString(R.string.app_name)} ${BuildConfig.VERSION_NAME}"
106+
val intent = Intent(Intent.ACTION_SENDTO)
107+
.setData("mailto:".toUri())
108+
.putExtra(Intent.EXTRA_EMAIL, arrayOf(ERROR_EMAIL_ADDRESS))
109+
.putExtra(Intent.EXTRA_SUBJECT, subject)
110+
.putExtra(Intent.EXTRA_TEXT, buildJson(context, errorInfo, comment))
111+
ShareUtils.openIntentInApp(context, intent)
112+
}
113+
114+
fun shareError(context: Context, errorInfo: ErrorInfo, comment: String) {
115+
ShareUtils.shareText(
116+
context,
117+
context.getString(R.string.error_report_title),
118+
buildJson(context, errorInfo, comment)
119+
)
120+
}
121+
122+
fun copyForGitHub(context: Context, errorInfo: ErrorInfo, comment: String) {
123+
ShareUtils.copyToClipboard(context, buildMarkdown(context, errorInfo, comment))
124+
}
125+
126+
fun openGitHubIssues(context: Context) {
127+
ShareUtils.openUrlInApp(context, ERROR_GITHUB_ISSUE_URL)
128+
}
129+
130+
fun openPrivacyPolicy(context: Context) {
131+
ShareUtils.openUrlInApp(context, context.getString(R.string.privacy_policy_url))
132+
}
133+
134+
private fun getContentLanguage(context: Context): String =
135+
Localization.getPreferredLocalization(context).localizationCode
136+
137+
private fun getContentCountry(context: Context): String =
138+
Localization.getPreferredContentCountry(context).countryCode
139+
140+
private fun getAppLanguage(): String =
141+
Localization.getAppLocale().toString()
142+
143+
private fun getOsString(): String {
144+
val name = System.getProperty("os.name")!!
145+
val osBase = Build.VERSION.BASE_OS.ifEmpty { "Android" }
146+
return "$name $osBase ${Build.VERSION.RELEASE} - ${Build.VERSION.SDK_INT}"
147+
}
148+
149+
private fun getCurrentTimestamp(): String =
150+
ZonedDateTime.now().format(DateTimeFormatter.ISO_OFFSET_DATE_TIME)
151+
}
152+
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/*
2+
* SPDX-FileCopyrightText: 2025-2026 NewPipe e.V. <https://newpipe-ev.de>
3+
* SPDX-License-Identifier: GPL-3.0-or-later
4+
*/
5+
6+
package org.schabi.newpipe.navigation
7+
8+
import androidx.activity.compose.LocalActivity
9+
import androidx.compose.runtime.Composable
10+
import androidx.compose.ui.platform.LocalContext
11+
import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator
12+
import androidx.navigation3.runtime.NavKey
13+
import androidx.navigation3.runtime.entryProvider
14+
import androidx.navigation3.runtime.rememberNavBackStack
15+
import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator
16+
import androidx.navigation3.ui.NavDisplay
17+
import org.schabi.newpipe.error.ErrorReportHelper
18+
import org.schabi.newpipe.ui.screens.ErrorReportScreen
19+
import org.schabi.newpipe.ui.screens.settings.navigation.SettingsNavigation
20+
import org.schabi.newpipe.util.external_communication.ShareUtils
21+
22+
/**
23+
* Top-level navigation display for all Compose screens in the app.
24+
* @param startDestination the initial screen to display, resolved from the launching Intent.
25+
*/
26+
27+
@Composable
28+
fun NavDisplay(startDestination: NavKey) {
29+
val backstack = rememberNavBackStack(startDestination)
30+
val context = LocalContext.current
31+
32+
// TODO: Drop this logic once everything is in Compose
33+
val activity = LocalActivity.current
34+
35+
fun onNavigateUp() {
36+
if (backstack.size == 1) activity?.finish() else backstack.removeLastOrNull()
37+
}
38+
39+
NavDisplay(
40+
backStack = backstack,
41+
onBack = { backstack.removeLastOrNull() },
42+
entryDecorators = listOf(
43+
rememberSaveableStateHolderNavEntryDecorator(),
44+
rememberViewModelStoreNavEntryDecorator()
45+
),
46+
entryProvider = entryProvider {
47+
entry<Screen.Error> { screen ->
48+
ErrorReportScreen(
49+
errorInfo = screen.errorInfo,
50+
onBackClick = ::onNavigateUp,
51+
onReportViaEmail = { comment ->
52+
ErrorReportHelper.sendErrorEmail(context, screen.errorInfo, comment)
53+
},
54+
onCopyForGitHub = { comment ->
55+
ErrorReportHelper.copyForGitHub(context, screen.errorInfo, comment)
56+
},
57+
onReportOnGitHub = {
58+
ErrorReportHelper.openGitHubIssues(context)
59+
},
60+
onReadPrivacyPolicy = {
61+
ErrorReportHelper.openPrivacyPolicy(context)
62+
},
63+
onShareError = { comment ->
64+
ErrorReportHelper.shareError(context, screen.errorInfo, comment)
65+
}
66+
)
67+
}
68+
69+
entry<Screen.Settings.Home> {
70+
SettingsNavigation(onExitSettings = ::onNavigateUp)
71+
}
72+
}
73+
)
74+
}

app/src/main/java/org/schabi/newpipe/navigation/Screen.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,14 @@ package org.schabi.newpipe.navigation
77

88
import androidx.navigation3.runtime.NavKey
99
import kotlinx.serialization.Serializable
10+
import org.schabi.newpipe.error.ErrorInfo
1011

1112
@Serializable
1213
sealed interface Screen : NavKey {
1314

15+
@Serializable
16+
data class Error(val errorInfo: ErrorInfo) : Screen
17+
1418
sealed interface Settings : Screen {
1519
@Serializable
1620
data object Home : Settings

0 commit comments

Comments
 (0)