Skip to content

Commit 0953b7a

Browse files
committed
refactor: optimize UI stability and event handling
- Stabilize progress bar layout to prevent jittering - Improve tab state management in BrowserScreen - Enhance visibility override handling for background playback - Refine event suppression scripts for privacy protection - Remove redundant profile badge UI from NewTabPage
1 parent cd0464a commit 0953b7a

8 files changed

Lines changed: 122 additions & 126 deletions

File tree

app/src/main/AndroidManifest.xml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010

1111
<application
1212
android:allowBackup="true"
13-
android:hardwareAccelerated="true"
1413
android:dataExtractionRules="@xml/data_extraction_rules"
1514
android:fullBackupContent="@xml/backup_rules"
1615
android:icon="@mipmap/ic_launcher"

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ class MediaPlaybackService : Service() {
7676
}
7777

7878
private fun setupMediaSession() {
79-
val session = MediaSessionCompat(this, "NeonMediaSession")
79+
val session = MediaSessionCompat(this, "FeatherMediaSession")
8080
session.setFlags(
8181
MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS or
8282
MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS
@@ -155,8 +155,8 @@ class MediaPlaybackService : Service() {
155155
}
156156

157157
val title = metadata?.title?.ifBlank { "Media Playing" } ?: "Media Playing"
158-
val artist = metadata?.artist?.ifBlank { "Neon Browser" } ?: "YouTube"
159-
val album = metadata?.album?.ifBlank { "Neon Browser" } ?: "Neon Browser"
158+
val artist = metadata?.artist?.ifBlank { "Feather Browser" } ?: "YouTube"
159+
val album = metadata?.album?.ifBlank { "Feather Browser" } ?: "Feather Browser"
160160
val artworkUrl = metadata?.artworkUrl ?: ""
161161

162162
// Update MediaSession state
@@ -173,6 +173,7 @@ class MediaPlaybackService : Service() {
173173
.setState(state, PlaybackStateCompat.PLAYBACK_POSITION_UNKNOWN, 1.0f)
174174
.build()
175175
mediaSession?.setPlaybackState(playbackState)
176+
mediaSession?.isActive = true
176177

177178
val metaBuilder = MediaMetadataCompat.Builder()
178179
.putString(MediaMetadataCompat.METADATA_KEY_TITLE, title)

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

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -121,13 +121,17 @@ object FingerprintScriptGenerator {
121121
// 3. Suppress any bubbling visibilitychange & blur events
122122
const suppressEvents = ['visibilitychange', 'webkitvisibilitychange', 'pagehide', 'blur', 'freeze'];
123123
suppressEvents.forEach(function(evt) {
124-
window.addEventListener(evt, function(e) {
125-
if (e && e.stopImmediatePropagation) e.stopImmediatePropagation();
126-
if (e && e.stopPropagation) e.stopPropagation();
124+
origAddEventListener.call(window, evt, function(e) {
125+
if (e) {
126+
if (e.stopImmediatePropagation) e.stopImmediatePropagation();
127+
if (e.stopPropagation) e.stopPropagation();
128+
}
127129
}, true);
128-
document.addEventListener(evt, function(e) {
129-
if (e && e.stopImmediatePropagation) e.stopImmediatePropagation();
130-
if (e && e.stopPropagation) e.stopPropagation();
130+
origAddEventListener.call(document, evt, function(e) {
131+
if (e) {
132+
if (e.stopImmediatePropagation) e.stopImmediatePropagation();
133+
if (e.stopPropagation) e.stopPropagation();
134+
}
131135
}, true);
132136
});
133137
@@ -222,7 +226,7 @@ object FingerprintScriptGenerator {
222226
const title = getMediaTitle();
223227
const artist = getMediaArtist();
224228
const art = getMediaThumbnail();
225-
window.FeatherMediaBridge.updateMetadata(title, artist, 'Neon Browser', art);
229+
window.FeatherMediaBridge.updateMetadata(title, artist, 'Feather Browser', art);
226230
}
227231
window.FeatherMediaBridge.updatePlaybackState(isPlaying);
228232
} catch(e) {}
@@ -237,15 +241,15 @@ object FingerprintScriptGenerator {
237241
Object.defineProperty(origMS, 'metadata', {
238242
set: function(val) {
239243
try {
240-
if (window.FeatherMediaBridge && val && wasActivePlayback) {
244+
if (window.FeatherMediaBridge && val) {
241245
let artUrl = '';
242246
if (val.artwork && val.artwork.length > 0) {
243247
artUrl = val.artwork[val.artwork.length - 1].src || '';
244248
}
245249
window.FeatherMediaBridge.updateMetadata(
246250
val.title || getMediaTitle(),
247251
val.artist || getMediaArtist(),
248-
val.album || 'Neon Browser',
252+
val.album || 'Feather Browser',
249253
artUrl || getMediaThumbnail()
250254
);
251255
}
@@ -375,7 +379,7 @@ object FingerprintScriptGenerator {
375379
const origPause = HTMLMediaElement.prototype.pause;
376380
HTMLMediaElement.prototype.pause = function() {
377381
// Check if the pause call was triggered by genuine user action or explicitly by media session bridge
378-
const isExplicitUserPause = window.__feather_explicit_pause || (Date.now() - lastUserTouchTime < 800);
382+
const isExplicitUserPause = window.__feather_explicit_pause || (Date.now() - lastUserTouchTime < 2500);
379383
if (!isExplicitUserPause) {
380384
// Suppress unwanted auto-pause triggered by YouTube visibility listeners or background state!
381385
return;
@@ -407,7 +411,7 @@ object FingerprintScriptGenerator {
407411
});
408412
409413
el.addEventListener('pause', function() {
410-
if (window.__feather_explicit_pause || (Date.now() - lastUserTouchTime < 800)) {
414+
if (window.__feather_explicit_pause || (Date.now() - lastUserTouchTime < 2500)) {
411415
notifyMediaBridge(false);
412416
}
413417
});
@@ -422,7 +426,7 @@ object FingerprintScriptGenerator {
422426
423427
if (anyPlaying) {
424428
wasActivePlayback = true;
425-
} else if (wasActivePlayback && !window.__feather_explicit_pause && (Date.now() - lastUserTouchTime > 1500)) {
429+
} else if (wasActivePlayback && !window.__feather_explicit_pause && (Date.now() - lastUserTouchTime > 2500)) {
426430
// Video unexpectedly paused while in the background without user touch!
427431
// Auto-resume background playback
428432
const video = document.querySelector('video');

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

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -155,20 +155,24 @@ fun BrowserScreen(
155155
modifier = Modifier.fillMaxSize()
156156
) { innerPadding ->
157157
val activeTabId = activeTabState?.id ?: ""
158-
val openTabs = remember(currentTabs, activeTabState) {
158+
val activeTabProfileId = activeTabState?.profileId
159+
val activeTabUrl = activeTabState?.url
160+
val activeTabTitle = activeTabState?.title
161+
val activeTabIsDesktop = activeTabState?.isDesktopMode
162+
val activeTabIsPrivate = activeTabState?.isPrivate
163+
164+
val openTabs = remember(currentTabs, activeTabId) {
159165
val map = linkedMapOf<String, BrowserTab>()
160166
currentTabs.forEach { map[it.id] = it }
161-
activeTabState?.let { active ->
162-
if (active.id.isNotBlank() && !map.containsKey(active.id)) {
163-
map[active.id] = BrowserTab(
164-
id = active.id,
165-
profileId = active.profileId,
166-
url = active.url,
167-
title = active.title,
168-
isDesktopMode = active.isDesktopMode,
169-
isPrivate = active.isPrivate
170-
)
171-
}
167+
if (activeTabId.isNotBlank() && !map.containsKey(activeTabId)) {
168+
map[activeTabId] = BrowserTab(
169+
id = activeTabId,
170+
profileId = activeTabProfileId ?: "default",
171+
url = activeTabUrl ?: "",
172+
title = activeTabTitle ?: "New Tab",
173+
isDesktopMode = activeTabIsDesktop ?: false,
174+
isPrivate = activeTabIsPrivate ?: false
175+
)
172176
}
173177
map.values.toList()
174178
}

app/src/main/java/com/example/ui/components/AddressBar.kt

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -405,19 +405,20 @@ fun AddressBar(
405405
label = "urlLoadingProgress"
406406
)
407407

408-
AnimatedVisibility(
409-
visible = isPageLoading,
410-
enter = fadeIn(animationSpec = androidx.compose.animation.core.tween(100)),
411-
exit = fadeOut(animationSpec = androidx.compose.animation.core.tween(220))
408+
// Fixed-height container (3.dp) prevents vertical layout jumping/jittering during page loads
409+
Box(
410+
modifier = Modifier
411+
.fillMaxWidth()
412+
.height(3.dp)
412413
) {
413-
LinearProgressIndicator(
414-
progress = { animatedProgress },
415-
modifier = Modifier
416-
.fillMaxWidth()
417-
.height(3.dp),
418-
color = profileColor,
419-
trackColor = profileColor.copy(alpha = 0.12f)
420-
)
414+
if (isPageLoading) {
415+
LinearProgressIndicator(
416+
progress = { animatedProgress },
417+
modifier = Modifier.fillMaxSize(),
418+
color = profileColor,
419+
trackColor = profileColor.copy(alpha = 0.12f)
420+
)
421+
}
421422
}
422423

423424
HorizontalDivider(

app/src/main/java/com/example/ui/components/NewTabPage.kt

Lines changed: 1 addition & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -92,49 +92,7 @@ fun NewTabPage(
9292
.padding(horizontal = 20.dp, vertical = if (newTabStyle == com.example.browser.NewTabStyle.MINIMALIST) 36.dp else 20.dp),
9393
horizontalAlignment = Alignment.CenterHorizontally
9494
) {
95-
if (newTabStyle == com.example.browser.NewTabStyle.PRODUCTIVITY) {
96-
Spacer(modifier = Modifier.height(12.dp))
97-
98-
// Profile Badge Pill
99-
Surface(
100-
shape = RoundedCornerShape(20.dp),
101-
color = profileColor.copy(alpha = 0.12f),
102-
border = androidx.compose.foundation.BorderStroke(1.dp, profileColor.copy(alpha = 0.3f)),
103-
modifier = Modifier
104-
.clickable { onOpenProfiles() }
105-
.testTag("new_tab_profile_pill")
106-
) {
107-
Row(
108-
modifier = Modifier.padding(horizontal = 14.dp, vertical = 7.dp),
109-
verticalAlignment = Alignment.CenterVertically
110-
) {
111-
Icon(
112-
imageVector = if (isPrivateMode) Icons.Default.VpnKey else getProfileIcon(currentProfile?.iconName),
113-
contentDescription = null,
114-
tint = profileColor,
115-
modifier = Modifier.size(16.dp)
116-
)
117-
Spacer(modifier = Modifier.width(8.dp))
118-
Text(
119-
text = if (isPrivateMode) "Private Session" else "${currentProfile?.displayName ?: "Personal"} Profile",
120-
fontSize = 12.5.sp,
121-
fontWeight = FontWeight.SemiBold,
122-
color = profileColor
123-
)
124-
Spacer(modifier = Modifier.width(4.dp))
125-
Icon(
126-
imageVector = Icons.Default.ArrowDropDown,
127-
contentDescription = "Switch profile",
128-
tint = profileColor,
129-
modifier = Modifier.size(16.dp)
130-
)
131-
}
132-
}
133-
134-
Spacer(modifier = Modifier.height(20.dp))
135-
} else {
136-
Spacer(modifier = Modifier.height(30.dp))
137-
}
95+
Spacer(modifier = Modifier.height(24.dp))
13896

13997
// Feather App Icon Hero Logo
14098
Box(
@@ -177,39 +135,6 @@ fun NewTabPage(
177135

178136
Spacer(modifier = Modifier.height(18.dp))
179137

180-
// Center Quick Search / URL Bar Pill
181-
Surface(
182-
shape = RoundedCornerShape(26.dp),
183-
color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.65f),
184-
border = androidx.compose.foundation.BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.4f)),
185-
modifier = Modifier
186-
.fillMaxWidth()
187-
.height(48.dp)
188-
.clickable { onNavigate("") }
189-
.testTag("new_tab_center_search")
190-
) {
191-
Row(
192-
modifier = Modifier
193-
.fillMaxSize()
194-
.padding(horizontal = 14.dp),
195-
verticalAlignment = Alignment.CenterVertically
196-
) {
197-
Icon(
198-
imageVector = Icons.Default.Search,
199-
contentDescription = "Search",
200-
tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f),
201-
modifier = Modifier.size(19.dp)
202-
)
203-
Spacer(modifier = Modifier.width(10.dp))
204-
Text(
205-
text = "Search or type URL",
206-
fontSize = 14.5.sp,
207-
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.65f),
208-
modifier = Modifier.weight(1f)
209-
)
210-
}
211-
}
212-
213138
// Live Local Weather Widget (Zero permissions required, IP-based)
214139
if (isWeatherEnabled && newTabStyle == com.example.browser.NewTabStyle.PRODUCTIVITY) {
215140
Spacer(modifier = Modifier.height(14.dp))

app/src/main/java/com/example/ui/components/WebViewContainer.kt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,20 @@ class PersistentWebView(context: Context) : WebView(context) {
4949
}
5050
}
5151

52+
override fun dispatchVisibilityChanged(changedView: View, visibility: Int) {
53+
try {
54+
val effectiveVisibility = if (allowBackgroundPlayback && isAttachedToWindow) View.VISIBLE else visibility
55+
super.dispatchVisibilityChanged(changedView, effectiveVisibility)
56+
} catch (e: Throwable) { }
57+
}
58+
59+
override fun onVisibilityChanged(changedView: View, visibility: Int) {
60+
try {
61+
val effectiveVisibility = if (allowBackgroundPlayback && isAttachedToWindow) View.VISIBLE else visibility
62+
super.onVisibilityChanged(changedView, effectiveVisibility)
63+
} catch (e: Throwable) { }
64+
}
65+
5266
override fun onPause() {
5367
if (!allowBackgroundPlayback) {
5468
try {

app/src/main/java/com/example/weather/DefaultIpLocationProvider.kt

Lines changed: 58 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,23 +13,27 @@ class DefaultIpLocationProvider : IpLocationProvider {
1313

1414
override suspend fun getLocation(): IpLocation? {
1515
return withContext(Dispatchers.IO) {
16-
// First attempt: ipapi.co (high accuracy, clean JSON)
17-
val ipApiResult = fetchFromIpApiCo()
18-
if (ipApiResult != null) return@withContext ipApiResult
16+
// First attempt: ipwho.is (fast, HTTPS, generous limits, high accuracy)
17+
val ipWhoResult = fetchFromIpWhoIs()
18+
if (ipWhoResult != null) return@withContext ipWhoResult
1919

20-
// Fallback: ip-api.com
20+
// Second attempt: get.geojs.io (unlimited, fast, HTTPS fallback)
21+
val geoJsResult = fetchFromGeoJs()
22+
if (geoJsResult != null) return@withContext geoJsResult
23+
24+
// Final fallback: ip-api.com (HTTP fallback)
2125
fetchFromIpApiCom()
2226
}
2327
}
2428

25-
private fun fetchFromIpApiCo(): IpLocation? {
29+
private fun fetchFromIpWhoIs(): IpLocation? {
2630
var connection: HttpURLConnection? = null
2731
return try {
28-
val url = URL("https://ipapi.co/json/")
32+
val url = URL("https://ipwho.is/")
2933
connection = (url.openConnection() as HttpURLConnection).apply {
3034
requestMethod = "GET"
31-
connectTimeout = 3000
32-
readTimeout = 3000
35+
connectTimeout = 3500
36+
readTimeout = 3500
3337
setRequestProperty("User-Agent", "FeatherBrowser/1.0 (Android; Mobile)")
3438
setRequestProperty("Accept", "application/json")
3539
instanceFollowRedirects = true
@@ -38,10 +42,10 @@ class DefaultIpLocationProvider : IpLocationProvider {
3842
if (connection.responseCode == HttpURLConnection.HTTP_OK) {
3943
val response = connection.inputStream.bufferedReader(Charsets.UTF_8).use { it.readText() }
4044
val json = JSONObject(response)
41-
if (json.has("latitude") && json.has("longitude")) {
45+
if (json.optBoolean("success", false)) {
4246
val city = json.optString("city", "Local Area").ifBlank { "Local Area" }
4347
val region = json.optString("region", "")
44-
val country = json.optString("country_name", json.optString("country", ""))
48+
val country = json.optString("country", "")
4549
val lat = json.optDouble("latitude", 0.0)
4650
val lon = json.optDouble("longitude", 0.0)
4751
if (lat != 0.0 || lon != 0.0) {
@@ -66,6 +70,50 @@ class DefaultIpLocationProvider : IpLocationProvider {
6670
}
6771
}
6872

73+
private fun fetchFromGeoJs(): IpLocation? {
74+
var connection: HttpURLConnection? = null
75+
return try {
76+
val url = URL("https://get.geojs.io/v1/ip/geo.json")
77+
connection = (url.openConnection() as HttpURLConnection).apply {
78+
requestMethod = "GET"
79+
connectTimeout = 3500
80+
readTimeout = 3500
81+
setRequestProperty("User-Agent", "FeatherBrowser/1.0 (Android; Mobile)")
82+
setRequestProperty("Accept", "application/json")
83+
instanceFollowRedirects = true
84+
}
85+
86+
if (connection.responseCode == HttpURLConnection.HTTP_OK) {
87+
val response = connection.inputStream.bufferedReader(Charsets.UTF_8).use { it.readText() }
88+
val json = JSONObject(response)
89+
val latStr = json.optString("latitude", "")
90+
val lonStr = json.optString("longitude", "")
91+
val lat = latStr.toDoubleOrNull() ?: 0.0
92+
val lon = lonStr.toDoubleOrNull() ?: 0.0
93+
if (lat != 0.0 || lon != 0.0) {
94+
val city = json.optString("city", "Local Area").ifBlank { "Local Area" }
95+
val region = json.optString("region", "")
96+
val country = json.optString("country", "")
97+
return IpLocation(
98+
city = city,
99+
region = region.ifBlank { null },
100+
country = country,
101+
latitude = lat,
102+
longitude = lon,
103+
timestamp = System.currentTimeMillis()
104+
)
105+
}
106+
}
107+
null
108+
} catch (e: CancellationException) {
109+
throw e
110+
} catch (e: Exception) {
111+
null
112+
} finally {
113+
connection?.disconnect()
114+
}
115+
}
116+
69117
private fun fetchFromIpApiCom(): IpLocation? {
70118
var connection: HttpURLConnection? = null
71119
return try {

0 commit comments

Comments
 (0)