Skip to content

Commit 6766659

Browse files
committed
perf: improve scroll performance and responsiveness
- Enable hardware acceleration and high refresh rates - Optimize New Tab interaction and scroll behavior - Enable nested scrolling for WebView container - Allow cleartext traffic for network compatibility
1 parent 0953b7a commit 6766659

7 files changed

Lines changed: 131 additions & 49 deletions

File tree

app/src/main/AndroidManifest.xml

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

1111
<application
1212
android:allowBackup="true"
13+
android:usesCleartextTraffic="true"
1314
android:dataExtractionRules="@xml/data_extraction_rules"
1415
android:fullBackupContent="@xml/backup_rules"
1516
android:icon="@mipmap/ic_launcher"
@@ -20,6 +21,7 @@
2021
<activity
2122
android:name=".MainActivity"
2223
android:exported="true"
24+
android:hardwareAccelerated="true"
2325
android:label="@string/app_name"
2426
android:theme="@style/Theme.MyApplication"
2527
android:windowSoftInputMode="adjustResize"

app/src/main/java/com/example/MainActivity.kt

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ class MainActivity : ComponentActivity() {
3434
override fun onCreate(savedInstanceState: Bundle?) {
3535
super.onCreate(savedInstanceState)
3636
enableEdgeToEdge()
37+
enableHighRefreshRate()
3738

3839
requestNotificationPermissionIfNeeded()
3940
handleIntent(intent)
@@ -64,6 +65,34 @@ class MainActivity : ComponentActivity() {
6465
}
6566
}
6667

68+
private fun enableHighRefreshRate() {
69+
try {
70+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
71+
val currentDisplay = display
72+
val modes = currentDisplay?.supportedModes
73+
val maxRefreshMode = modes?.maxByOrNull { it.refreshRate }
74+
if (maxRefreshMode != null && maxRefreshMode.refreshRate > 60f) {
75+
val params = window.attributes
76+
params.preferredDisplayModeId = maxRefreshMode.modeId
77+
params.preferredRefreshRate = maxRefreshMode.refreshRate
78+
window.attributes = params
79+
}
80+
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
81+
@Suppress("DEPRECATION")
82+
val currentDisplay = windowManager.defaultDisplay
83+
val modes = currentDisplay?.supportedModes
84+
val maxRefreshMode = modes?.maxByOrNull { it.refreshRate }
85+
if (maxRefreshMode != null && maxRefreshMode.refreshRate > 60f) {
86+
val params = window.attributes
87+
params.preferredDisplayModeId = maxRefreshMode.modeId
88+
window.attributes = params
89+
}
90+
}
91+
} catch (e: Throwable) {
92+
// Safe fallback if display mode adjustment is restricted by system
93+
}
94+
}
95+
6796
override fun onNewIntent(intent: Intent) {
6897
super.onNewIntent(intent)
6998
setIntent(intent)

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

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import androidx.compose.foundation.ExperimentalFoundationApi
44
import androidx.compose.foundation.background
55
import androidx.compose.foundation.combinedClickable
66
import androidx.compose.foundation.clickable
7+
import androidx.compose.foundation.interaction.MutableInteractionSource
78
import androidx.compose.foundation.gestures.detectTapGestures
89
import androidx.compose.foundation.layout.*
910
import androidx.compose.foundation.lazy.LazyRow
@@ -83,13 +84,14 @@ fun NewTabPage(
8384
modifier = modifier
8485
.fillMaxSize()
8586
.background(MaterialTheme.colorScheme.background)
86-
.pointerInput(Unit) {
87-
detectTapGestures(onTap = {
88-
focusManager.clearFocus(force = true)
89-
})
87+
.clickable(
88+
interactionSource = remember { MutableInteractionSource() },
89+
indication = null
90+
) {
91+
focusManager.clearFocus(force = true)
9092
}
9193
.verticalScroll(scrollState)
92-
.padding(horizontal = 20.dp, vertical = if (newTabStyle == com.example.browser.NewTabStyle.MINIMALIST) 36.dp else 20.dp),
94+
.padding(horizontal = 20.dp, vertical = if (newTabStyle == com.example.browser.NewTabStyle.MINIMALIST) 28.dp else 20.dp),
9395
horizontalAlignment = Alignment.CenterHorizontally
9496
) {
9597
Spacer(modifier = Modifier.height(24.dp))
@@ -136,8 +138,8 @@ fun NewTabPage(
136138
Spacer(modifier = Modifier.height(18.dp))
137139

138140
// Live Local Weather Widget (Zero permissions required, IP-based)
139-
if (isWeatherEnabled && newTabStyle == com.example.browser.NewTabStyle.PRODUCTIVITY) {
140-
Spacer(modifier = Modifier.height(14.dp))
141+
if (isWeatherEnabled) {
142+
Spacer(modifier = Modifier.height(if (newTabStyle == com.example.browser.NewTabStyle.MINIMALIST) 8.dp else 14.dp))
141143
WeatherCard(
142144
state = weatherState,
143145
isFahrenheit = isWeatherFahrenheit,

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -329,7 +329,7 @@ fun SettingsScreen(
329329
color = MaterialTheme.colorScheme.onSurface
330330
)
331331
Text(
332-
text = "Shows real-time weather on Home screen via approximate IP (zero location permissions needed)",
332+
text = "Shows real-time weather on Home screen (Productivity & Zen Minimal mode, zero location permissions needed)",
333333
fontSize = 11.5.sp,
334334
color = MaterialTheme.colorScheme.onSurfaceVariant
335335
)

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,7 @@ fun WebViewContainer(
296296
val themedContext = ctx.createConfigurationContext(config)
297297

298298
val swipeRefresh = SwipeRefreshLayout(ctx).apply {
299+
isNestedScrollingEnabled = true
299300
layoutParams = ViewGroup.LayoutParams(
300301
ViewGroup.LayoutParams.MATCH_PARENT,
301302
ViewGroup.LayoutParams.MATCH_PARENT
@@ -321,8 +322,16 @@ fun WebViewContainer(
321322
// In virtualized environments or fallback crashes, ensure rendering stability
322323
if (renderCrashCount > 0) {
323324
setLayerType(View.LAYER_TYPE_SOFTWARE, null)
325+
} else {
326+
setLayerType(View.LAYER_TYPE_HARDWARE, null)
324327
}
325328

329+
// High refresh rate (90Hz/120Hz) nested scrolling optimization
330+
isNestedScrollingEnabled = true
331+
overScrollMode = View.OVER_SCROLL_IF_CONTENT_SCROLLS
332+
isVerticalScrollBarEnabled = true
333+
isHorizontalScrollBarEnabled = false
334+
326335
// Touch listener to gain focus away from address bar on tap
327336
setOnTouchListener { v, event ->
328337
if (event.action == MotionEvent.ACTION_DOWN) {

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

Lines changed: 74 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -3,26 +3,70 @@ package com.example.weather
33
import kotlinx.coroutines.Dispatchers
44
import kotlinx.coroutines.withContext
55
import org.json.JSONObject
6-
import java.io.BufferedReader
7-
import java.io.InputStreamReader
86
import java.net.HttpURLConnection
97
import java.net.URL
8+
import java.util.Locale
109
import kotlin.coroutines.cancellation.CancellationException
1110

1211
class DefaultIpLocationProvider : IpLocationProvider {
1312

1413
override suspend fun getLocation(): IpLocation? {
1514
return withContext(Dispatchers.IO) {
16-
// First attempt: ipwho.is (fast, HTTPS, generous limits, high accuracy)
15+
// First attempt: ip-api.com (most granular ISP/campus city database, accurately identifies Mymensingh for BAU)
16+
val ipApiResult = fetchFromIpApiCom()
17+
if (ipApiResult != null) return@withContext ipApiResult
18+
19+
// Second attempt: ipwho.is (fast, HTTPS fallback with coordinate refinement)
1720
val ipWhoResult = fetchFromIpWhoIs()
1821
if (ipWhoResult != null) return@withContext ipWhoResult
1922

20-
// Second attempt: get.geojs.io (unlimited, fast, HTTPS fallback)
21-
val geoJsResult = fetchFromGeoJs()
22-
if (geoJsResult != null) return@withContext geoJsResult
23+
// Third attempt: get.geojs.io (unlimited, fast, HTTPS fallback with coordinate refinement)
24+
fetchFromGeoJs()
25+
}
26+
}
27+
28+
private fun fetchFromIpApiCom(): IpLocation? {
29+
var connection: HttpURLConnection? = null
30+
return try {
31+
val url = URL("http://ip-api.com/json/?fields=status,message,country,countryCode,region,regionName,city,district,zip,lat,lon,timezone,isp,org,as,query")
32+
connection = (url.openConnection() as HttpURLConnection).apply {
33+
requestMethod = "GET"
34+
connectTimeout = 3500
35+
readTimeout = 3500
36+
setRequestProperty("User-Agent", "FeatherBrowser/1.0 (Android; Mobile)")
37+
setRequestProperty("Accept", "application/json")
38+
}
39+
40+
if (connection.responseCode == HttpURLConnection.HTTP_OK) {
41+
val response = connection.inputStream.bufferedReader(Charsets.UTF_8).use { it.readText() }
42+
val json = JSONObject(response)
43+
if (json.optString("status") == "success") {
44+
var city = json.optString("city", "").ifBlank { json.optString("district", "Local Area") }
45+
val region = json.optString("regionName", "")
46+
val country = json.optString("country", "")
47+
val lat = json.optDouble("lat", 0.0)
48+
val lon = json.optDouble("lon", 0.0)
49+
50+
// Refine city with reverse geocoding if available to ensure exact district/city accuracy
51+
city = refineLocationCity(lat, lon, city)
2352

24-
// Final fallback: ip-api.com (HTTP fallback)
25-
fetchFromIpApiCom()
53+
return IpLocation(
54+
city = city.ifBlank { "Local Area" },
55+
region = region.ifBlank { null },
56+
country = country,
57+
latitude = lat,
58+
longitude = lon,
59+
timestamp = System.currentTimeMillis()
60+
)
61+
}
62+
}
63+
null
64+
} catch (e: CancellationException) {
65+
throw e
66+
} catch (e: Exception) {
67+
null
68+
} finally {
69+
connection?.disconnect()
2670
}
2771
}
2872

@@ -43,14 +87,15 @@ class DefaultIpLocationProvider : IpLocationProvider {
4387
val response = connection.inputStream.bufferedReader(Charsets.UTF_8).use { it.readText() }
4488
val json = JSONObject(response)
4589
if (json.optBoolean("success", false)) {
46-
val city = json.optString("city", "Local Area").ifBlank { "Local Area" }
90+
var city = json.optString("city", "Local Area").ifBlank { "Local Area" }
4791
val region = json.optString("region", "")
4892
val country = json.optString("country", "")
4993
val lat = json.optDouble("latitude", 0.0)
5094
val lon = json.optDouble("longitude", 0.0)
5195
if (lat != 0.0 || lon != 0.0) {
96+
city = refineLocationCity(lat, lon, city)
5297
return IpLocation(
53-
city = city,
98+
city = city.ifBlank { "Local Area" },
5499
region = region.ifBlank { null },
55100
country = country,
56101
latitude = lat,
@@ -91,11 +136,12 @@ class DefaultIpLocationProvider : IpLocationProvider {
91136
val lat = latStr.toDoubleOrNull() ?: 0.0
92137
val lon = lonStr.toDoubleOrNull() ?: 0.0
93138
if (lat != 0.0 || lon != 0.0) {
94-
val city = json.optString("city", "Local Area").ifBlank { "Local Area" }
139+
var city = json.optString("city", "Local Area").ifBlank { "Local Area" }
95140
val region = json.optString("region", "")
96141
val country = json.optString("country", "")
142+
city = refineLocationCity(lat, lon, city)
97143
return IpLocation(
98-
city = city,
144+
city = city.ifBlank { "Local Area" },
99145
region = region.ifBlank { null },
100146
country = country,
101147
latitude = lat,
@@ -114,42 +160,32 @@ class DefaultIpLocationProvider : IpLocationProvider {
114160
}
115161
}
116162

117-
private fun fetchFromIpApiCom(): IpLocation? {
163+
private fun refineLocationCity(lat: Double, lon: Double, fallbackCity: String): String {
164+
if (lat == 0.0 && lon == 0.0) return fallbackCity
118165
var connection: HttpURLConnection? = null
119166
return try {
120-
val url = URL("http://ip-api.com/json/")
167+
val url = URL(String.format(Locale.US, "https://api.bigdatacloud.net/data/reverse-geocode-client?latitude=%.4f&longitude=%.4f&localityLanguage=en", lat, lon))
121168
connection = (url.openConnection() as HttpURLConnection).apply {
122-
requestMethod = "GET"
123-
connectTimeout = 3000
124-
readTimeout = 3000
169+
connectTimeout = 2500
170+
readTimeout = 2500
125171
setRequestProperty("User-Agent", "FeatherBrowser/1.0 (Android; Mobile)")
126172
setRequestProperty("Accept", "application/json")
127173
}
128-
129174
if (connection.responseCode == HttpURLConnection.HTTP_OK) {
130-
val response = connection.inputStream.bufferedReader(Charsets.UTF_8).use { it.readText() }
131-
val json = JSONObject(response)
132-
if (json.optString("status") == "success") {
133-
val city = json.optString("city", "Local Area").ifBlank { "Local Area" }
134-
val region = json.optString("regionName", "")
135-
val country = json.optString("country", "")
136-
val lat = json.optDouble("lat", 0.0)
137-
val lon = json.optDouble("lon", 0.0)
138-
return IpLocation(
139-
city = city,
140-
region = region.ifBlank { null },
141-
country = country,
142-
latitude = lat,
143-
longitude = lon,
144-
timestamp = System.currentTimeMillis()
145-
)
175+
val res = connection.inputStream.bufferedReader(Charsets.UTF_8).use { it.readText() }
176+
val json = JSONObject(res)
177+
val refined = json.optString("city", "").ifBlank {
178+
json.optString("locality", "").ifBlank {
179+
json.optString("principalSubdivision", "")
180+
}
181+
}
182+
if (refined.isNotBlank()) {
183+
return refined
146184
}
147185
}
148-
null
149-
} catch (e: CancellationException) {
150-
throw e
186+
fallbackCity
151187
} catch (e: Exception) {
152-
null
188+
fallbackCity
153189
} finally {
154190
connection?.disconnect()
155191
}

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

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ class WeatherRepository(
2222
private const val KEY_CACHED_LOCATION = "cached_location_json"
2323

2424
// Cache lifetimes
25-
private const val WEATHER_CACHE_TTL_MS = 20 * 60 * 1000L // 20 minutes
26-
private const val LOCATION_CACHE_TTL_MS = 4 * 60 * 60 * 1000L // 4 hours
25+
private const val WEATHER_CACHE_TTL_MS = 15 * 60 * 1000L // 15 minutes
26+
private const val LOCATION_CACHE_TTL_MS = 30 * 60 * 1000L // 30 minutes
2727
}
2828

2929
init {
@@ -36,8 +36,12 @@ class WeatherRepository(
3636
}
3737

3838
suspend fun refreshWeather(forceNetwork: Boolean = false) {
39-
val cached = loadCachedWeather()
4039
val now = System.currentTimeMillis()
40+
if (forceNetwork) {
41+
sharedPrefs.edit().remove(KEY_CACHED_LOCATION).remove(KEY_CACHED_WEATHER).apply()
42+
}
43+
44+
val cached = loadCachedWeather()
4145

4246
if (!forceNetwork && cached != null && (now - cached.timestamp) < WEATHER_CACHE_TTL_MS) {
4347
_weatherState.value = WeatherUiState.Success(cached)

0 commit comments

Comments
 (0)