Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package com.lastwave.app.data.ytmusic

import android.util.Log
import android.webkit.CookieManager
import android.webkit.WebStorage
import java.security.MessageDigest
import kotlin.coroutines.resume
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
Expand All @@ -10,6 +13,8 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext
import javax.inject.Inject
import javax.inject.Singleton

Expand Down Expand Up @@ -106,11 +111,19 @@ class YtMusicAuthManager @Inject constructor(
* wipes the WebView's own cookie jar so a later sign-in can never
* auto-capture the previous account's stale session. */
suspend fun signOut() {
_connection.value = YtConnection.DISCONNECTED
preferences.clearConnection()
runCatching {
val cm = android.webkit.CookieManager.getInstance()
cm.removeAllCookies(null)
cm.flush()
withContext(Dispatchers.Main) {
val cm = CookieManager.getInstance()
suspendCancellableCoroutine<Boolean> { cont ->
cm.removeAllCookies { result ->
if (cont.isActive) cont.resume(result)
}
}
cm.flush()
WebStorage.getInstance().deleteAllData()
}
}
}

Expand All @@ -121,8 +134,11 @@ class YtMusicAuthManager @Inject constructor(
* Returns true if fresh cookies were found and saved.
*/
suspend fun refreshCookiesFromCookieManager(): Boolean {
val current = connection.value
if (!current.isConnected) return false

return try {
val cm = android.webkit.CookieManager.getInstance()
val cm = CookieManager.getInstance()
val musicCookies = cm.getCookie("https://music.youtube.com").orEmpty()
val ytCookies = cm.getCookie("https://www.youtube.com").orEmpty()
val combined = when {
Expand All @@ -136,8 +152,7 @@ class YtMusicAuthManager @Inject constructor(
val hasLogin = cookies.containsKey("LOGIN_INFO")
if (!hasSapisid || !hasLogin) return false

val current = connection.value
if (current.isConnected && current.cookies == cookies) return false
if (current.cookies == cookies) return false

preferences.saveConnection(
cookies = cookies,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -685,6 +685,7 @@ class SettingsViewModel @Inject constructor(
downloadManager.clearAllDownloads()
discoverRepository.clearRecommendationExclusions()
playlistRepository.clearAll()
runCatching { ytAuthManager.signOut() }
// Session last: clearing it navigates to login and can cancel this
// scope, which used to interrupt the reset halfway through.
sessionPreferences.clearAll()
Expand Down Expand Up @@ -1002,6 +1003,7 @@ class SettingsViewModel @Inject constructor(
fun disconnectYouTube() {
launchSettingsAction("disconnect YouTube Music") {
ytMusicPreferences.setSyncEnabled(false)
_ytChannels.value = emptyList()
ytAuthManager.signOut()
_uiState.update { it.copy(toastMessage = appContext.getString(com.lastwave.app.R.string.toast_yt_off)) }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,17 +64,31 @@ fun YouTubeLoginScreen(
val state by viewModel.uiState.collectAsStateWithLifecycle()
var loadProgressVisible by remember { mutableStateOf(true) }
var webViewError by remember { mutableStateOf<String?>(null) }
var activeWebView by remember { mutableStateOf<WebView?>(null) }

LaunchedEffect(state.connectedName, state.verifying) {
LaunchedEffect(Unit) {
runCatching {
val cm = CookieManager.getInstance()
cm.removeAllCookies(null)
cm.flush()
android.webkit.WebStorage.getInstance().deleteAllData()
}
}

LaunchedEffect(state.connectedName, state.verifying, activeWebView) {
if (state.connectedName != null || state.verifying) return@LaunchedEffect
while (true) {
val cookies = readYouTubeCookies()
if (hasSessionTokens(cookies)) {
val current = viewModel.uiState.value
if (!current.verifying && current.connectedName == null) {
viewModel.attemptConnect(cookies)
val currentUrl = activeWebView?.url
val isAtTarget = currentUrl?.contains("music.youtube.com") == true
if (isAtTarget) {
val cookies = readYouTubeCookies()
if (hasSessionTokens(cookies)) {
val current = viewModel.uiState.value
if (!current.verifying && current.connectedName == null) {
viewModel.attemptConnect(cookies)
}
break
}
break
}
kotlinx.coroutines.delay(1_200L)
}
Expand All @@ -101,11 +115,17 @@ fun YouTubeLoginScreen(
factory = { ctx ->
var createdWebView: WebView? = null
try {
WebView(ctx).also { createdWebView = it }.apply {
WebView(ctx).also {
createdWebView = it
activeWebView = it
}.apply {
@SuppressLint("SetJavaScriptEnabled")
settings.javaScriptEnabled = true
settings.domStorageEnabled = true
settings.databaseEnabled = true
clearCache(true)
clearHistory()
clearFormData()
// Google blocks the default WebView user agent on
// accounts.google.com ("may not be secure"). Stripping
// embedded WebView markers ("; wv" and "Version/X.X")
Expand All @@ -126,9 +146,11 @@ fun YouTubeLoginScreen(
super.doUpdateVisitedHistory(view, url, isReload)
val current = viewModel.uiState.value
if (current.verifying || current.connectedName != null) return
val cookies = readYouTubeCookies()
if (hasSessionTokens(cookies)) {
viewModel.attemptConnect(cookies)
if (url?.contains("music.youtube.com") == true) {
val cookies = readYouTubeCookies()
if (hasSessionTokens(cookies)) {
viewModel.attemptConnect(cookies)
}
}
}

Expand All @@ -138,16 +160,19 @@ fun YouTubeLoginScreen(
// Read state live — a factory-captured copy would be stale.
val current = viewModel.uiState.value
if (current.verifying || current.connectedName != null) return
val cookies = readYouTubeCookies()
if (hasSessionTokens(cookies)) {
viewModel.attemptConnect(cookies)
if (url?.contains("music.youtube.com") == true) {
val cookies = readYouTubeCookies()
if (hasSessionTokens(cookies)) {
viewModel.attemptConnect(cookies)
}
}
}
}
loadUrl(LOGIN_URL)
}
} catch (error: Exception) {
runCatching { createdWebView?.destroy() }
activeWebView = null
loadProgressVisible = false
webViewError = "Android System WebView is unavailable. Install or enable a WebView provider to connect YouTube Music."
android.widget.FrameLayout(ctx)
Expand Down
Loading