Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,6 @@ app/release/
bin/
.vscode/
*.code-workspace

# logs
*.log
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package org.schabi.newpipe.error

import android.content.Context
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Assert
import org.junit.Test
import org.junit.runner.RunWith
import org.schabi.newpipe.R
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException
import java.io.IOException
import java.net.SocketTimeoutException

@RunWith(AndroidJUnit4::class)
class ErrorInfoCommentsTest {
private val context: Context by lazy { ApplicationProvider.getApplicationContext<Context>() }
// Test 1: Network error on initial load (Resource.Error)
@Test
fun testInitialCommentNetworkError() {
val errorInfo = ErrorInfo(
throwable = SocketTimeoutException("Connection timeout"),
userAction = UserAction.REQUESTED_COMMENTS,
request = "comments"
)
Assert.assertEquals(context.getString(R.string.network_error), errorInfo.getMessage(context))
Assert.assertTrue(errorInfo.isReportable)
Assert.assertTrue(errorInfo.isRetryable)
Assert.assertNull(errorInfo.recaptchaUrl)
}

// Test 2: Network error on paging (LoadState.Error)
@Test
fun testPagingNetworkError() {
val errorInfo = ErrorInfo(
throwable = IOException("Paging failed"),
userAction = UserAction.REQUESTED_COMMENTS,
request = "comments"
)
Assert.assertEquals(context.getString(R.string.network_error), errorInfo.getMessage(context))
Assert.assertTrue(errorInfo.isReportable)
Assert.assertTrue(errorInfo.isRetryable)
Assert.assertNull(errorInfo.recaptchaUrl)
}

// Test 3: ReCaptcha during comments load
@Test
fun testReCaptchaDuringComments() {
val url = "https://www.google.com/recaptcha/api/fallback?k=test"
val errorInfo = ErrorInfo(
throwable = ReCaptchaException("ReCaptcha needed", url),
userAction = UserAction.REQUESTED_COMMENTS,
request = "comments"
)
Assert.assertEquals(context.getString(R.string.recaptcha_request_toast), errorInfo.getMessage(context))
Assert.assertEquals(url, errorInfo.recaptchaUrl)
Assert.assertTrue(errorInfo.isReportable)
Assert.assertTrue(errorInfo.isRetryable)
}
}
13 changes: 7 additions & 6 deletions app/src/main/java/org/schabi/newpipe/error/ErrorInfo.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package org.schabi.newpipe.error
import android.content.Context
import android.os.Parcelable
import androidx.annotation.StringRes
import androidx.core.content.ContextCompat
import com.google.android.exoplayer2.ExoPlaybackException
import com.google.android.exoplayer2.upstream.HttpDataSource
import com.google.android.exoplayer2.upstream.Loader
Expand All @@ -28,6 +27,7 @@ import org.schabi.newpipe.extractor.exceptions.YoutubeMusicPremiumContentExcepti
import org.schabi.newpipe.ktx.isNetworkRelated
import org.schabi.newpipe.player.mediasource.FailedMediaSource
import org.schabi.newpipe.player.resolver.PlaybackResolver
import org.schabi.newpipe.util.Localization
import java.net.UnknownHostException

/**
Expand Down Expand Up @@ -147,13 +147,11 @@ class ErrorInfo private constructor(
private vararg val formatArgs: String,
) : Parcelable {
fun getString(context: Context): String {
// use Localization.compatGetString() just in case context is not AppCompatActivity
return if (formatArgs.isEmpty()) {
// use ContextCompat.getString() just in case context is not AppCompatActivity
ContextCompat.getString(context, stringRes)
Localization.compatGetString(context, stringRes)
} else {
// ContextCompat.getString() with formatArgs does not exist, so we just
// replicate its source code but with formatArgs
ContextCompat.getContextForLanguage(context).getString(stringRes, *formatArgs)
Localization.compatGetString(context, stringRes, *formatArgs)
}
}
}
Expand Down Expand Up @@ -276,6 +274,9 @@ class ErrorInfo private constructor(
// we don't have an exception, so this is a manually built error, which likely
// indicates that it's important and is thus reportable
null -> true
// a recaptcha was detected, and the user needs to solve it, there is no use in
// letting users report it
is ReCaptchaException -> false
// the service explicitly said that content is not available (e.g. age restrictions,
// video deleted, etc.), there is no use in letting users report it
is ContentNotAvailableException -> false
Expand Down
2 changes: 2 additions & 0 deletions app/src/main/java/org/schabi/newpipe/paging/CommentsSource.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,14 @@ class CommentsSource(private val commentInfo: CommentInfo) : PagingSource<Page,
override suspend fun load(params: LoadParams<Page>): LoadResult<Page, CommentsInfoItem> {
// params.key is null the first time the load() function is called, so we need to return the
// first batch of already-loaded comments

if (params.key == null) {
return LoadResult.Page(commentInfo.comments, null, commentInfo.nextPage)
} else {
val info = withContext(Dispatchers.IO) {
CommentsInfo.getMoreItems(service, commentInfo.url, params.key)
}

return LoadResult.Page(info.items, null, info.nextPage)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import android.os.Bundle
import android.os.ResultReceiver
import android.support.v4.media.session.PlaybackStateCompat
import android.util.Log
import androidx.core.content.ContextCompat
import androidx.core.net.toUri
import com.google.android.exoplayer2.Player
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector.PlaybackPreparer
Expand All @@ -29,6 +28,7 @@ import org.schabi.newpipe.player.playqueue.PlaylistPlayQueue
import org.schabi.newpipe.player.playqueue.SinglePlayQueue
import org.schabi.newpipe.util.ChannelTabHelper
import org.schabi.newpipe.util.ExtractorHelper
import org.schabi.newpipe.util.Localization
import org.schabi.newpipe.util.NavigationHelper
import java.util.function.BiConsumer
import java.util.function.Consumer
Expand Down Expand Up @@ -111,7 +111,7 @@ class MediaBrowserPlaybackPreparer(
//region Errors
private fun onUnsupportedError() {
setMediaSessionError.accept(
ContextCompat.getString(context, R.string.content_not_supported),
Localization.compatGetString(context, R.string.content_not_supported),
PlaybackStateCompat.ERROR_CODE_NOT_SUPPORTED
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ package org.schabi.newpipe.settings.viewmodel
import android.app.Application
import android.content.Context
import android.content.SharedPreferences
import androidx.core.content.ContextCompat
import androidx.lifecycle.AndroidViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import org.schabi.newpipe.R
import org.schabi.newpipe.util.Localization
import javax.inject.Inject

@HiltViewModel
Expand All @@ -20,11 +20,12 @@ class SettingsViewModel @Inject constructor(

private var _settingsLayoutRedesignPref: Boolean
get() = preferenceManager.getBoolean(
ContextCompat.getString(getApplication(), R.string.settings_layout_redesign_key), false
Localization.compatGetString(getApplication(), R.string.settings_layout_redesign_key),
false
)
set(value) {
preferenceManager.edit().putBoolean(
ContextCompat.getString(getApplication(), R.string.settings_layout_redesign_key),
Localization.compatGetString(getApplication(), R.string.settings_layout_redesign_key),
value
).apply()
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package org.schabi.newpipe.ui.components.common

import android.content.Intent
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import org.schabi.newpipe.R
import org.schabi.newpipe.error.ErrorInfo
import org.schabi.newpipe.error.ErrorUtil
import org.schabi.newpipe.error.ReCaptchaActivity
import org.schabi.newpipe.error.UserAction
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException
import org.schabi.newpipe.ui.theme.AppTheme
import org.schabi.newpipe.util.external_communication.ShareUtils

@Composable
fun ErrorPanel(
errorInfo: ErrorInfo,
modifier: Modifier = Modifier,
onRetry: (() -> Unit)? = null,
) {
val context = LocalContext.current
val isPreview = LocalInspectionMode.current
val messageText = if (isPreview) {
stringResource(R.string.error_snackbar_message)
} else {
errorInfo.getMessage(context)
}

Column(
verticalArrangement = Arrangement.spacedBy(12.dp),
horizontalAlignment = Alignment.CenterHorizontally,
modifier = modifier,
) {
Text(
text = messageText,
style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.Bold),
textAlign = TextAlign.Center
)

if (errorInfo.recaptchaUrl != null) {
ServiceColoredButton(onClick = {
// Starting ReCaptcha Challenge Activity
val intent = Intent(context, ReCaptchaActivity::class.java)
.putExtra(
ReCaptchaActivity.RECAPTCHA_URL_EXTRA,
errorInfo.recaptchaUrl
)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
context.startActivity(intent)
}) {
Text(stringResource(R.string.recaptcha_solve).uppercase())
}
}

if (errorInfo.isRetryable) {
onRetry?.let {
ServiceColoredButton(onClick = it) {
Text(stringResource(R.string.retry).uppercase())
}
}
}

if (errorInfo.isReportable) {
ServiceColoredButton(onClick = { ErrorUtil.openActivity(context, errorInfo) }) {
Text(stringResource(R.string.error_snackbar_action).uppercase())
}
}

errorInfo.openInBrowserUrl?.let { url ->
ServiceColoredButton(onClick = { ShareUtils.openUrlInBrowser(context, url) }) {
Text(stringResource(R.string.open_in_browser).uppercase())
}
}
}
}

@Preview(showBackground = true, widthDp = 360, heightDp = 640, backgroundColor = 0xffffffff)
@Composable
fun ErrorPanelPreview() {
AppTheme {
ErrorPanel(
errorInfo = ErrorInfo(
throwable = ReCaptchaException("An error", "https://example.com"),
userAction = UserAction.REQUESTED_STREAM,
request = "Preview request",
openInBrowserUrl = "https://example.com",
),
onRetry = {},
)
}
Comment thread
SttApollo marked this conversation as resolved.
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package org.schabi.newpipe.ui.components.common

import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import org.schabi.newpipe.ui.theme.AppTheme
import org.schabi.newpipe.ui.theme.SizeTokens.SpacingMedium
import org.schabi.newpipe.ui.theme.SizeTokens.SpacingSmall

@Composable
fun ServiceColoredButton(
onClick: () -> Unit,
modifier: Modifier = Modifier,
Comment thread
theimpulson marked this conversation as resolved.
content: @Composable() RowScope.() -> Unit,
) {
Button(
onClick = onClick,
modifier = modifier.wrapContentWidth(),
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.error,
contentColor = MaterialTheme.colorScheme.onError
),
contentPadding = PaddingValues(horizontal = SpacingMedium, vertical = SpacingSmall),
shape = RectangleShape,
elevation = ButtonDefaults.buttonElevation(
defaultElevation = 8.dp,

),
) {
content()
}
}

@Preview
@Composable
fun ServiceColoredButtonPreview() {
Comment thread
SttApollo marked this conversation as resolved.
Outdated
AppTheme {
ServiceColoredButton(
onClick = {},
content = {
Text("Button")
}
)
}
}
Loading
Loading