Skip to content

Commit 4a84002

Browse files
committed
refactor: simplify background playback handling
Remove redundant WebView focus overrides and aggressive event listener blocking. Improve media control reliability by switching to explicit play/pause intent actions.
1 parent ee18c93 commit 4a84002

5 files changed

Lines changed: 87 additions & 303 deletions

File tree

app/src/main/java/com/example/media/MediaPlaybackService.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -228,10 +228,11 @@ class MediaPlaybackService : Service() {
228228
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
229229
)
230230

231+
val playPauseAction = if (isPlaying) ACTION_PAUSE else ACTION_PLAY
231232
val playPauseIntent = PendingIntent.getService(
232233
this,
233234
2,
234-
Intent(this, MediaPlaybackService::class.java).apply { action = ACTION_TOGGLE },
235+
Intent(this, MediaPlaybackService::class.java).apply { action = playPauseAction },
235236
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
236237
)
237238

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

Lines changed: 85 additions & 157 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ object FingerprintScriptGenerator {
7878
if (window.__feather_bg_play_active) return;
7979
window.__feather_bg_play_active = true;
8080
81-
// 1. Override Page Visibility API on both document instance and Document.prototype
81+
// 1. Spoof Page Visibility API so websites (like YouTube) report visible and focused
8282
try {
8383
const defineProp = function(obj, prop, value) {
8484
try {
@@ -102,87 +102,31 @@ object FingerprintScriptGenerator {
102102
103103
document.hasFocus = function() { return true; };
104104
Document.prototype.hasFocus = function() { return true; };
105-
106-
// Override property event handlers
107-
defineProp(document, 'onvisibilitychange', null);
108-
defineProp(window, 'onpagehide', null);
109-
defineProp(window, 'onblur', null);
110105
} catch(e) {}
111106
112-
// 2. Intercept registration of visibility pause event listeners
113-
const origAddEventListener = EventTarget.prototype.addEventListener;
114-
EventTarget.prototype.addEventListener = function(type, listener, options) {
115-
if (type === 'visibilitychange' || type === 'webkitvisibilitychange' || type === 'pagehide' || type === 'blur') {
116-
return;
117-
}
118-
return origAddEventListener.apply(this, arguments);
119-
};
120-
121-
// 3. Suppress any bubbling visibilitychange & blur events
122-
const suppressEvents = ['visibilitychange', 'webkitvisibilitychange', 'pagehide', 'blur', 'freeze'];
123-
suppressEvents.forEach(function(evt) {
124-
origAddEventListener.call(window, evt, function(e) {
125-
if (e) {
126-
if (e.stopImmediatePropagation) e.stopImmediatePropagation();
127-
if (e.stopPropagation) e.stopPropagation();
128-
}
129-
}, true);
130-
origAddEventListener.call(document, evt, function(e) {
131-
if (e) {
132-
if (e.stopImmediatePropagation) e.stopImmediatePropagation();
133-
if (e.stopPropagation) e.stopPropagation();
107+
// 2. Prevent YouTube from auto-pausing on tab change: intercept visibilitychange event listeners
108+
try {
109+
const origAddEventListener = EventTarget.prototype.addEventListener;
110+
EventTarget.prototype.addEventListener = function(type, listener, options) {
111+
if (type === 'visibilitychange' || type === 'webkitvisibilitychange') {
112+
return;
134113
}
135-
}, true);
136-
});
137-
138-
// 4. Wrap IntersectionObserver so YouTube / video players never think they are offscreen
139-
if (window.IntersectionObserver) {
140-
const OrigIO = window.IntersectionObserver;
141-
window.IntersectionObserver = function(callback, options) {
142-
const wrappedCb = function(entries, observer) {
143-
const modEntries = entries.map(function(entry) {
144-
return new Proxy(entry, {
145-
get: function(target, prop) {
146-
if (prop === 'isIntersecting') return true;
147-
if (prop === 'intersectionRatio') return 1.0;
148-
return target[prop];
149-
}
150-
});
151-
});
152-
return callback(modEntries, observer);
153-
};
154-
return new OrigIO(wrappedCb, options);
114+
return origAddEventListener.apply(this, arguments);
155115
};
156-
window.IntersectionObserver.prototype = OrigIO.prototype;
157-
}
158116
159-
// 5. Track user interaction with player controls vs backgrounding
160-
window.__feather_explicit_pause = false;
161-
let userInteractedWithPlayer = false;
162-
let userPlayerTimer = null;
163-
164-
function markPlayerTap() {
165-
userInteractedWithPlayer = true;
166-
if (userPlayerTimer) clearTimeout(userPlayerTimer);
167-
userPlayerTimer = setTimeout(function() {
168-
userInteractedWithPlayer = false;
169-
}, 1500);
170-
}
171-
172-
['click', 'touchend', 'pointerup'].forEach(function(evt) {
173-
document.addEventListener(evt, function(e) {
174-
try {
175-
const target = e.target;
176-
if (!target) return;
177-
if (target.tagName === 'VIDEO' ||
178-
(target.closest && target.closest('video, #movie_player, .html5-video-player, .ytp-play-button, [aria-label*="Pause"], [aria-label*="Play"], .player-controls, ytm-custom-control-button'))) {
179-
markPlayerTap();
180-
}
181-
} catch(err) {}
182-
}, true);
183-
});
117+
['visibilitychange', 'webkitvisibilitychange'].forEach(function(evt) {
118+
origAddEventListener.call(window, evt, function(e) {
119+
if (e && e.stopImmediatePropagation) e.stopImmediatePropagation();
120+
if (e && e.stopPropagation) e.stopPropagation();
121+
}, true);
122+
origAddEventListener.call(document, evt, function(e) {
123+
if (e && e.stopImmediatePropagation) e.stopImmediatePropagation();
124+
if (e && e.stopPropagation) e.stopPropagation();
125+
}, true);
126+
});
127+
} catch(e) {}
184128
185-
// 6. Media Session API hooking for notification media player
129+
// 3. Media Session Hooking for Android notification metadata & controls
186130
window.__feather_actions = window.__feather_actions || {};
187131
188132
function getMediaThumbnail() {
@@ -283,66 +227,75 @@ object FingerprintScriptGenerator {
283227
}
284228
} catch(e) {}
285229
286-
// 7. Direct media element tracking & action triggers
230+
// 4. Media control methods callable from notification bar
287231
window.__feather_media_play = function() {
288-
window.__feather_explicit_pause = false;
289-
userInteractedWithPlayer = false;
232+
try {
233+
if (window.__feather_actions && typeof window.__feather_actions['play'] === 'function') {
234+
window.__feather_actions['play']();
235+
return;
236+
}
237+
} catch(e) {}
290238
try {
291239
const moviePlayer = document.getElementById('movie_player');
292240
if (moviePlayer && typeof moviePlayer.playVideo === 'function') {
293241
moviePlayer.playVideo();
242+
return;
243+
}
244+
} catch(e) {}
245+
try {
246+
const playBtn = document.querySelector('.ytp-play-button, .player-control-play-pause-icon, ytm-custom-control-button, [aria-label*="Play"]');
247+
if (playBtn) {
248+
playBtn.click();
249+
return;
294250
}
295251
} catch(e) {}
296252
try {
297253
const mediaEls = document.querySelectorAll('video, audio');
298254
mediaEls.forEach(function(m) {
299-
m.play().catch(function() {});
255+
if (m.paused) m.play().catch(function() {});
300256
});
301257
} catch(e) {}
302-
try {
303-
if (window.__feather_actions && typeof window.__feather_actions['play'] === 'function') {
304-
window.__feather_actions['play']();
305-
}
306-
} catch(e) {}
307-
notifyMediaBridge(true);
308258
};
309259
310260
window.__feather_media_pause = function() {
311-
window.__feather_explicit_pause = true;
261+
try {
262+
if (window.__feather_actions && typeof window.__feather_actions['pause'] === 'function') {
263+
window.__feather_actions['pause']();
264+
return;
265+
}
266+
} catch(e) {}
312267
try {
313268
const moviePlayer = document.getElementById('movie_player');
314269
if (moviePlayer && typeof moviePlayer.pauseVideo === 'function') {
315270
moviePlayer.pauseVideo();
271+
return;
272+
}
273+
} catch(e) {}
274+
try {
275+
const pauseBtn = document.querySelector('.ytp-play-button, .player-control-play-pause-icon, ytm-custom-control-button, [aria-label*="Pause"]');
276+
if (pauseBtn) {
277+
pauseBtn.click();
278+
return;
316279
}
317280
} catch(e) {}
318281
try {
319282
const mediaEls = document.querySelectorAll('video, audio');
320283
mediaEls.forEach(function(m) {
321-
if (typeof origPause === 'function') {
322-
origPause.call(m);
323-
} else {
324-
m.pause();
325-
}
284+
if (!m.paused) m.pause();
326285
});
327286
} catch(e) {}
328-
try {
329-
if (window.__feather_actions && typeof window.__feather_actions['pause'] === 'function') {
330-
window.__feather_actions['pause']();
331-
}
332-
} catch(e) {}
333-
notifyMediaBridge(false);
334287
};
335288
336289
window.__feather_media_toggle = function() {
337-
const video = document.querySelector('video');
290+
const video = document.querySelector('video, audio');
338291
if (video) {
339292
if (video.paused) {
340293
window.__feather_media_play();
341294
} else {
342295
window.__feather_media_pause();
343296
}
344297
} else {
345-
const btn = document.querySelector('.ytp-play-button') || document.querySelector('[aria-label*="Play"], [aria-label*="Pause"]');
298+
const btn = document.querySelector('.ytp-play-button, .player-control-play-pause-icon, ytm-custom-control-button, [aria-label*="Play"], [aria-label*="Pause"]');
346299
if (btn) btn.click();
347300
}
348301
};
@@ -354,7 +307,7 @@ object FingerprintScriptGenerator {
354307
return;
355308
}
356309
} catch(e) {}
357-
const nextBtn = document.querySelector('.ytp-next-button') || document.querySelector('[aria-label*="Next"]');
310+
const nextBtn = document.querySelector('.ytp-next-button, [aria-label*="Next"]');
358311
if (nextBtn) {
359312
nextBtn.click();
360313
} else {
@@ -370,84 +323,59 @@ object FingerprintScriptGenerator {
370323
return;
371324
}
372325
} catch(e) {}
373-
const prevBtn = document.querySelector('.ytp-prev-button') || document.querySelector('[aria-label*="Previous"]');
326+
const prevBtn = document.querySelector('.ytp-prev-button, [aria-label*="Previous"]');
374327
if (prevBtn) {
375328
prevBtn.click();
376329
} else {
377330
window.history.back();
378331
}
379332
};
380333
381-
// 8. Hook HTMLMediaElement prototype play and pause with bulletproof background defense
382-
let origPlay = null;
383-
let origPause = null;
384-
try {
385-
origPlay = HTMLMediaElement.prototype.play;
386-
HTMLMediaElement.prototype.play = function() {
387-
window.__feather_explicit_pause = false;
388-
notifyMediaBridge(true);
389-
return origPlay.apply(this, arguments);
390-
};
334+
// 5. Periodic monitor & watchdog for HTML media elements
335+
let lastReportedState = null;
336+
let lastReportedTitle = '';
391337
392-
origPause = HTMLMediaElement.prototype.pause;
393-
HTMLMediaElement.prototype.pause = function() {
394-
const isAllowedPause = window.__feather_explicit_pause || userInteractedWithPlayer;
395-
if (!isAllowedPause) {
396-
// Suppress unwanted auto-pause triggered by backgrounding or visibility change!
397-
return;
338+
function hookMediaElement(el) {
339+
if (!el || el.__feather_monitored) return;
340+
el.__feather_monitored = true;
341+
342+
['play', 'playing'].forEach(function(evt) {
343+
el.addEventListener(evt, function() {
344+
wasActivePlayback = true;
345+
notifyMediaBridge(true);
346+
});
347+
});
348+
349+
el.addEventListener('pause', function() {
350+
const anyStillPlaying = Array.from(document.querySelectorAll('video, audio')).some(function(m) {
351+
return !m.paused && !m.ended;
352+
});
353+
if (!anyStillPlaying) {
354+
notifyMediaBridge(false);
398355
}
399-
window.__feather_explicit_pause = false;
400-
userInteractedWithPlayer = false;
401-
notifyMediaBridge(false);
402-
return origPause.apply(this, arguments);
403-
};
404-
} catch(e) {}
356+
});
405357
406-
// 9. Periodic monitor & watchdog for HTML media elements
407-
let lastReportedState = null;
408-
let lastReportedTitle = '';
358+
el.addEventListener('ended', function() {
359+
wasActivePlayback = false;
360+
if (window.FeatherMediaBridge) {
361+
window.FeatherMediaBridge.onMediaEnded();
362+
}
363+
});
364+
}
409365
410366
const monitorMedia = function() {
411367
try {
412368
const els = document.querySelectorAll('video, audio');
413369
let anyPlaying = false;
414370
els.forEach(function(el) {
371+
hookMediaElement(el);
415372
if (!el.paused && !el.ended) {
416373
anyPlaying = true;
417374
}
418-
if (el.__feather_monitored) return;
419-
el.__feather_monitored = true;
420-
421-
el.addEventListener('play', function() {
422-
wasActivePlayback = true;
423-
notifyMediaBridge(true);
424-
});
425-
426-
el.addEventListener('pause', function() {
427-
if (window.__feather_explicit_pause || userInteractedWithPlayer) {
428-
notifyMediaBridge(false);
429-
} else if (wasActivePlayback) {
430-
// Auto-resume if backgrounding caused an unexpected pause
431-
el.play().catch(function() {});
432-
}
433-
});
434-
435-
el.addEventListener('ended', function() {
436-
wasActivePlayback = false;
437-
if (window.FeatherMediaBridge) {
438-
window.FeatherMediaBridge.onMediaEnded();
439-
}
440-
});
441375
});
442376
443377
if (anyPlaying) {
444378
wasActivePlayback = true;
445-
} else if (wasActivePlayback && !window.__feather_explicit_pause && !userInteractedWithPlayer) {
446-
// Automatically resume background playback if video paused without user interaction
447-
const video = document.querySelector('video');
448-
if (video && video.paused && !video.ended) {
449-
video.play().catch(function() {});
450-
}
451379
}
452380
453381
const currentTitle = getMediaTitle();
@@ -460,7 +388,7 @@ object FingerprintScriptGenerator {
460388
}
461389
} catch(e) {}
462390
};
463-
setInterval(monitorMedia, 800);
391+
setInterval(monitorMedia, 1000);
464392
monitorMedia();
465393
} catch(e) {}
466394
})();

app/src/main/java/com/example/ui/BrowserScreen.kt

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -316,9 +316,6 @@ fun BrowserScreen(
316316
modifier = Modifier
317317
.fillMaxSize()
318318
.zIndex(if (isActive) 1f else 0f)
319-
.graphicsLayer {
320-
alpha = if (isActive) 1f else 0f
321-
}
322319
.then(
323320
if (!isActive) Modifier.pointerInput(Unit) {} else Modifier
324321
)

0 commit comments

Comments
 (0)