From 88e1d8b9c2008e2418b1bf35cfa7fbbd5453d9bd Mon Sep 17 00:00:00 2001 From: ajisth69 Date: Sun, 18 Oct 2026 12:04:49 +0530 Subject: [PATCH] fix(yt): ensure clean logout, purge WebView cookies/storage, and stop auto-reconnection - Optimistically set _connection to DISCONNECTED and synchronously await CookieManager.removeAllCookies and WebStorage.deleteAllData on Main dispatcher - Guard refreshCookiesFromCookieManager so disconnected accounts are never resurrected by background sync or retries - Clear stale cookies/storage on YouTubeLoginScreen entry and ensure auto-capture only triggers when reaching music.youtube.com - Reset channel options on disconnect and call ytAuthManager.signOut() on confirmClearAllData --- .../app/data/ytmusic/YtMusicAuthManager.kt | 27 +++++++--- .../app/ui/settings/SettingsViewModel.kt | 2 + .../app/ui/settings/YouTubeLoginScreen.kt | 53 ++++++++++++++----- 3 files changed, 62 insertions(+), 20 deletions(-) diff --git a/app/src/main/java/com/lastwave/app/data/ytmusic/YtMusicAuthManager.kt b/app/src/main/java/com/lastwave/app/data/ytmusic/YtMusicAuthManager.kt index 56b55cfdd..b76e99584 100644 --- a/app/src/main/java/com/lastwave/app/data/ytmusic/YtMusicAuthManager.kt +++ b/app/src/main/java/com/lastwave/app/data/ytmusic/YtMusicAuthManager.kt @@ -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 @@ -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 @@ -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 { cont -> + cm.removeAllCookies { result -> + if (cont.isActive) cont.resume(result) + } + } + cm.flush() + WebStorage.getInstance().deleteAllData() + } } } @@ -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 { @@ -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, diff --git a/app/src/main/java/com/lastwave/app/ui/settings/SettingsViewModel.kt b/app/src/main/java/com/lastwave/app/ui/settings/SettingsViewModel.kt index 0751679cf..671548b2a 100644 --- a/app/src/main/java/com/lastwave/app/ui/settings/SettingsViewModel.kt +++ b/app/src/main/java/com/lastwave/app/ui/settings/SettingsViewModel.kt @@ -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() @@ -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)) } } diff --git a/app/src/main/java/com/lastwave/app/ui/settings/YouTubeLoginScreen.kt b/app/src/main/java/com/lastwave/app/ui/settings/YouTubeLoginScreen.kt index dd371bb69..e9db356e1 100644 --- a/app/src/main/java/com/lastwave/app/ui/settings/YouTubeLoginScreen.kt +++ b/app/src/main/java/com/lastwave/app/ui/settings/YouTubeLoginScreen.kt @@ -64,17 +64,31 @@ fun YouTubeLoginScreen( val state by viewModel.uiState.collectAsStateWithLifecycle() var loadProgressVisible by remember { mutableStateOf(true) } var webViewError by remember { mutableStateOf(null) } + var activeWebView by remember { mutableStateOf(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) } @@ -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") @@ -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) + } } } @@ -138,9 +160,11 @@ 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) + } } } } @@ -148,6 +172,7 @@ fun YouTubeLoginScreen( } } 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)