Skip to content

Commit 1bce9ec

Browse files
committed
refactor: implement per-tab event handling
Improve multi-tab architecture by routing WebView actions to specific tab IDs, refining background media playback injection, and updating CI release configuration.
1 parent 3d1332f commit 1bce9ec

5 files changed

Lines changed: 190 additions & 122 deletions

File tree

.github/workflows/release.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ on:
1515
permissions:
1616
contents: write
1717

18+
env:
19+
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
20+
1821
jobs:
1922
release:
2023
name: Build & Publish Release

app/src/main/java/com/example/browser/BrowserViewModel.kt

Lines changed: 81 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -424,7 +424,7 @@ class BrowserViewModel(application: Application) : AndroidViewModel(application)
424424
_activeTabId.value = tabId
425425
}
426426
val curState = _activeTabState.value
427-
if (curState == null) {
427+
if (curState == null || curState.id != tabId) {
428428
_activeTabState.value = ActiveTabState(
429429
id = tabId,
430430
profileId = if (_isPrivateMode.value) "private_session" else _currentProfileId.value,
@@ -438,10 +438,10 @@ class BrowserViewModel(application: Application) : AndroidViewModel(application)
438438
_activeTabState.update { it?.copy(url = parsedUrl, progress = 10, isLoading = true) }
439439
}
440440
viewModelScope.launch {
441-
_webViewActionEvent.emit(WebViewAction.LoadUrl(parsedUrl))
441+
_webViewActionEvent.emit(WebViewAction.LoadUrl(parsedUrl, targetTabId = tabId))
442442
val cur = currentTabs.value.find { it.id == tabId }
443443
if (cur != null) {
444-
repository.saveTab(cur.copy(url = parsedUrl))
444+
repository.saveTab(cur.copy(url = parsedUrl, lastAccessedAt = System.currentTimeMillis()))
445445
} else {
446446
repository.saveTab(
447447
BrowserTab(
@@ -458,39 +458,44 @@ class BrowserViewModel(application: Application) : AndroidViewModel(application)
458458
}
459459

460460
fun reload() {
461-
viewModelScope.launch { _webViewActionEvent.emit(WebViewAction.Reload) }
461+
val tabId = _activeTabId.value
462+
viewModelScope.launch { _webViewActionEvent.emit(WebViewAction.Reload(targetTabId = tabId)) }
462463
}
463464

464465
fun stopLoading() {
465-
viewModelScope.launch { _webViewActionEvent.emit(WebViewAction.StopLoading) }
466+
val tabId = _activeTabId.value
467+
viewModelScope.launch { _webViewActionEvent.emit(WebViewAction.StopLoading(targetTabId = tabId)) }
466468
}
467469

468470
fun goBack() {
469-
viewModelScope.launch { _webViewActionEvent.emit(WebViewAction.GoBack) }
471+
val tabId = _activeTabId.value
472+
viewModelScope.launch { _webViewActionEvent.emit(WebViewAction.GoBack(targetTabId = tabId)) }
470473
}
471474

472475
fun goForward() {
473-
viewModelScope.launch { _webViewActionEvent.emit(WebViewAction.GoForward) }
476+
val tabId = _activeTabId.value
477+
viewModelScope.launch { _webViewActionEvent.emit(WebViewAction.GoForward(targetTabId = tabId)) }
474478
}
475479

476480
fun goHome() {
477481
val tabId = _activeTabId.value
478482
_activeTabState.update { it?.copy(url = "", title = "New Tab", progress = 0, isLoading = false) }
479483
viewModelScope.launch {
480-
_webViewActionEvent.emit(WebViewAction.LoadUrl("about:blank"))
484+
_webViewActionEvent.emit(WebViewAction.LoadUrl("about:blank", targetTabId = tabId))
481485
val cur = currentTabs.value.find { it.id == tabId }
482486
if (cur != null) {
483-
repository.saveTab(cur.copy(url = "", title = "New Tab"))
487+
repository.saveTab(cur.copy(url = "", title = "New Tab", lastAccessedAt = System.currentTimeMillis()))
484488
}
485489
}
486490
}
487491

488492
fun toggleDesktopMode() {
493+
val tabId = _activeTabId.value
489494
val newDesktop = !(_activeTabState.value?.isDesktopMode ?: false)
490495
_activeTabState.update { it?.copy(isDesktopMode = newDesktop) }
491496
viewModelScope.launch {
492-
_webViewActionEvent.emit(WebViewAction.SetDesktopMode(newDesktop))
493-
val cur = currentTabs.value.find { it.id == _activeTabId.value }
497+
_webViewActionEvent.emit(WebViewAction.SetDesktopMode(newDesktop, targetTabId = tabId))
498+
val cur = currentTabs.value.find { it.id == tabId }
494499
if (cur != null) {
495500
repository.saveTab(cur.copy(isDesktopMode = newDesktop))
496501
}
@@ -503,18 +508,21 @@ class BrowserViewModel(application: Application) : AndroidViewModel(application)
503508
}
504509

505510
fun closeFindInPage() {
511+
val tabId = _activeTabId.value
506512
_isFindInPageActive.value = false
507513
_findQuery.value = ""
508-
viewModelScope.launch { _webViewActionEvent.emit(WebViewAction.ClearFindMatches) }
514+
viewModelScope.launch { _webViewActionEvent.emit(WebViewAction.ClearFindMatches(targetTabId = tabId)) }
509515
}
510516

511517
fun setFindQuery(query: String) {
518+
val tabId = _activeTabId.value
512519
_findQuery.value = query
513-
viewModelScope.launch { _webViewActionEvent.emit(WebViewAction.FindAllAsync(query)) }
520+
viewModelScope.launch { _webViewActionEvent.emit(WebViewAction.FindAllAsync(query, targetTabId = tabId)) }
514521
}
515522

516523
fun findNext(forward: Boolean) {
517-
viewModelScope.launch { _webViewActionEvent.emit(WebViewAction.FindNext(forward)) }
524+
val tabId = _activeTabId.value
525+
viewModelScope.launch { _webViewActionEvent.emit(WebViewAction.FindNext(forward, targetTabId = tabId)) }
518526
}
519527

520528
fun onFindMatchResult(activeMatchOrdinal: Int, numberOfMatches: Int) {
@@ -527,51 +535,58 @@ class BrowserViewModel(application: Application) : AndroidViewModel(application)
527535
}
528536

529537
// Callbacks from WebView
530-
fun onPageStarted(url: String) {
538+
fun onPageStarted(tabId: String, url: String) {
531539
if (url.isBlank() || url == "about:blank") {
532-
_activeTabState.update { it?.copy(isLoading = false, progress = 0) }
540+
if (tabId == _activeTabId.value) {
541+
_activeTabState.update { it?.copy(isLoading = false, progress = 0) }
542+
}
533543
return
534544
}
535-
onUrlChanged(url)
536-
_activeTabState.update {
537-
it?.copy(
538-
isLoading = true,
539-
progress = it.progress.coerceIn(15, 90)
540-
)
545+
onUrlChanged(tabId, url)
546+
if (tabId == _activeTabId.value) {
547+
_activeTabState.update {
548+
it?.copy(
549+
isLoading = true,
550+
progress = it.progress.coerceIn(15, 90)
551+
)
552+
}
541553
}
542554
}
543555

544-
fun onPageFinished(url: String) {
545-
_activeTabState.update {
546-
it?.copy(
547-
isLoading = false,
548-
progress = 100
549-
)
556+
fun onPageFinished(tabId: String, url: String) {
557+
if (tabId == _activeTabId.value) {
558+
_activeTabState.update {
559+
it?.copy(
560+
isLoading = false,
561+
progress = 100
562+
)
563+
}
550564
}
551565
}
552566

553-
fun onPageLoadError() {
554-
_activeTabState.update {
555-
it?.copy(
556-
isLoading = false,
557-
progress = 100
558-
)
567+
fun onPageLoadError(tabId: String) {
568+
if (tabId == _activeTabId.value) {
569+
_activeTabState.update {
570+
it?.copy(
571+
isLoading = false,
572+
progress = 100
573+
)
574+
}
559575
}
560576
}
561577

562-
fun onUrlChanged(url: String) {
578+
fun onUrlChanged(tabId: String, url: String) {
563579
if (url.isBlank() || url == "about:blank") return
564580
val isSec = UrlUtils.isHttps(url)
565-
val tabId = _activeTabId.value
566581
val blocked = ContentBlocker.getBlockCountForTab(tabId)
567582

568-
val curState = _activeTabState.value
569-
if (curState?.url == url && curState.isSecure == isSec && curState.blockedCount == blocked) {
570-
return
571-
}
572-
573-
_activeTabState.update {
574-
it?.copy(url = url, isSecure = isSec, blockedCount = blocked)
583+
if (tabId == _activeTabId.value) {
584+
val curState = _activeTabState.value
585+
if (curState?.url != url || curState.isSecure != isSec || curState.blockedCount != blocked) {
586+
_activeTabState.update {
587+
it?.copy(url = url, isSecure = isSec, blockedCount = blocked)
588+
}
589+
}
575590
}
576591
viewModelScope.launch {
577592
val cur = currentTabs.value.find { it.id == tabId }
@@ -581,21 +596,22 @@ class BrowserViewModel(application: Application) : AndroidViewModel(application)
581596
}
582597
}
583598

584-
fun onTitleChanged(title: String) {
599+
fun onTitleChanged(tabId: String, title: String) {
585600
if (title.isBlank() || title == "about:blank") return
586-
val tabId = _activeTabId.value
587-
val curState = _activeTabState.value
588-
if (curState?.title == title) return
589-
590-
_activeTabState.update { it?.copy(title = title) }
601+
if (tabId == _activeTabId.value) {
602+
val curState = _activeTabState.value
603+
if (curState?.title != title) {
604+
_activeTabState.update { it?.copy(title = title) }
605+
}
606+
}
591607
viewModelScope.launch {
592608
val cur = currentTabs.value.find { it.id == tabId }
593609
if (cur != null) {
594610
if (cur.title != title) {
595611
repository.saveTab(cur.copy(title = title))
596612
}
597613
// Record history if not private
598-
val url = _activeTabState.value?.url ?: ""
614+
val url = if (tabId == _activeTabId.value) _activeTabState.value?.url ?: cur.url else cur.url
599615
if (url.isNotBlank() && url != "about:blank") {
600616
repository.recordHistory(
601617
profileId = cur.profileId,
@@ -608,7 +624,8 @@ class BrowserViewModel(application: Application) : AndroidViewModel(application)
608624
}
609625
}
610626

611-
fun onProgressChanged(progress: Int) {
627+
fun onProgressChanged(tabId: String, progress: Int) {
628+
if (tabId != _activeTabId.value) return
612629
val curState = _activeTabState.value ?: return
613630
// If on Home or about:blank, keep loading disabled
614631
if (curState.url.isBlank() || curState.url == "about:blank") {
@@ -630,7 +647,8 @@ class BrowserViewModel(application: Application) : AndroidViewModel(application)
630647
}
631648
}
632649

633-
fun onNavigationStateChanged(canGoBack: Boolean, canGoForward: Boolean) {
650+
fun onNavigationStateChanged(tabId: String, canGoBack: Boolean, canGoForward: Boolean) {
651+
if (tabId != _activeTabId.value) return
634652
val curState = _activeTabState.value ?: return
635653
if (curState.canGoBack == canGoBack && curState.canGoForward == canGoForward) {
636654
return
@@ -804,13 +822,15 @@ class BrowserViewModel(application: Application) : AndroidViewModel(application)
804822
}
805823

806824
sealed class WebViewAction {
807-
data class LoadUrl(val url: String) : WebViewAction()
808-
data object Reload : WebViewAction()
809-
data object StopLoading : WebViewAction()
810-
data object GoBack : WebViewAction()
811-
data object GoForward : WebViewAction()
812-
data class SetDesktopMode(val enabled: Boolean) : WebViewAction()
813-
data class FindAllAsync(val query: String) : WebViewAction()
814-
data class FindNext(val forward: Boolean) : WebViewAction()
815-
data object ClearFindMatches : WebViewAction()
825+
abstract val targetTabId: String?
826+
827+
data class LoadUrl(val url: String, override val targetTabId: String? = null) : WebViewAction()
828+
data class Reload(override val targetTabId: String? = null) : WebViewAction()
829+
data class StopLoading(override val targetTabId: String? = null) : WebViewAction()
830+
data class GoBack(override val targetTabId: String? = null) : WebViewAction()
831+
data class GoForward(override val targetTabId: String? = null) : WebViewAction()
832+
data class SetDesktopMode(val enabled: Boolean, override val targetTabId: String? = null) : WebViewAction()
833+
data class FindAllAsync(val query: String, override val targetTabId: String? = null) : WebViewAction()
834+
data class FindNext(val forward: Boolean, override val targetTabId: String? = null) : WebViewAction()
835+
data class ClearFindMatches(override val targetTabId: String? = null) : WebViewAction()
816836
}

app/src/main/java/com/example/privacy/FingerprintScriptGenerator.kt

Lines changed: 29 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -68,53 +68,44 @@ object FingerprintScriptGenerator {
6868
}
6969

7070
/**
71-
* Injects a background audio/video playback shim that prevents YouTube and HTML5 video players
72-
* from pausing when the app is placed into the background or switched away.
71+
* Injects a background audio/video playback shim that prevents YouTube, SoundCloud, Spotify,
72+
* and HTML5 video players from pausing when switching tabs or backgrounding the app.
7373
*/
7474
fun generateBackgroundPlayScript(): String {
7575
return """
7676
(function() {
7777
try {
78-
if (window.__bg_play_injected) return;
79-
window.__bg_play_injected = true;
78+
if (window.__feather_bg_play_active) return;
79+
window.__feather_bg_play_active = true;
8080
81-
// Override Page Visibility API so media sites (YouTube, SoundCloud, Spotify) think the tab is always active
82-
Object.defineProperty(document, 'hidden', {
83-
get: function() { return false; },
84-
configurable: true
85-
});
86-
Object.defineProperty(document, 'visibilityState', {
87-
get: function() { return 'visible'; },
88-
configurable: true
89-
});
90-
Object.defineProperty(document, 'webkitHidden', {
91-
get: function() { return false; },
92-
configurable: true
93-
});
94-
Object.defineProperty(document, 'webkitVisibilityState', {
95-
get: function() { return 'visible'; },
96-
configurable: true
97-
});
98-
99-
// Prevent visibilitychange events from pausing playback
100-
window.addEventListener('visibilitychange', function(e) {
101-
e.stopImmediatePropagation();
102-
}, true);
103-
document.addEventListener('visibilitychange', function(e) {
104-
e.stopImmediatePropagation();
105-
}, true);
81+
// 1. Override Page Visibility API so media sites always consider the tab active
82+
try {
83+
Object.defineProperty(document, 'hidden', { get: function() { return false; }, configurable: true });
84+
Object.defineProperty(document, 'visibilityState', { get: function() { return 'visible'; }, configurable: true });
85+
Object.defineProperty(document, 'webkitHidden', { get: function() { return false; }, configurable: true });
86+
Object.defineProperty(document, 'webkitVisibilityState', { get: function() { return 'visible'; }, configurable: true });
87+
document.hasFocus = function() { return true; };
88+
} catch(e) {}
10689
107-
// Auto unpause when video/audio receives an automated pause while playing in background
108-
const originalPlay = HTMLMediaElement.prototype.play;
109-
const originalPause = HTMLMediaElement.prototype.pause;
110-
111-
HTMLMediaElement.prototype.pause = function() {
112-
// If user paused manually, allow it; if triggered by page hide, continue playing
113-
if (document.visibilityState === 'visible' && !document.hidden) {
114-
return originalPause.apply(this, arguments);
90+
// 2. Intercept registration of visibility pause event listeners
91+
const origAddEventListener = EventTarget.prototype.addEventListener;
92+
EventTarget.prototype.addEventListener = function(type, listener, options) {
93+
if (type === 'visibilitychange' || type === 'webkitvisibilitychange' || type === 'pagehide') {
94+
return;
11595
}
116-
return Promise.resolve();
96+
return origAddEventListener.apply(this, arguments);
11797
};
98+
99+
// 3. Suppress any existing or bubbling visibilitychange & blur events
100+
const suppressEvents = ['visibilitychange', 'webkitvisibilitychange', 'pagehide', 'blur'];
101+
suppressEvents.forEach(function(evt) {
102+
window.addEventListener(evt, function(e) {
103+
if (e && e.stopImmediatePropagation) e.stopImmediatePropagation();
104+
}, true);
105+
document.addEventListener(evt, function(e) {
106+
if (e && e.stopImmediatePropagation) e.stopImmediatePropagation();
107+
}, true);
108+
});
118109
} catch(e) {}
119110
})();
120111
""".trimIndent()

0 commit comments

Comments
 (0)