Skip to content

Commit ee18c93

Browse files
committed
feat: enhance background tab and webview persistence
- Improve WebView lifecycle management for background playback - Add `getTabById` to `BrowserRepository` - Optimize tab rendering in `BrowserScreen` - Update app version to 1.110
1 parent 2089cc2 commit ee18c93

8 files changed

Lines changed: 99 additions & 13 deletions

File tree

.github/workflows/release.yml

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,19 @@ jobs:
9292
VER="${{ steps.version_step.outputs.version }}"
9393
mkdir -p release-assets
9494
APK_NAME="Feather-Browser-${TAG}.apk"
95-
cp app/build/outputs/apk/release/app-release.apk "release-assets/${APK_NAME}"
95+
FOUND_APK=$(find app/build/outputs/apk/release -name "*.apk" 2>/dev/null | head -n 1)
96+
if [ -z "$FOUND_APK" ]; then
97+
FOUND_APK=$(find . -name "*release*.apk" 2>/dev/null | head -n 1)
98+
fi
99+
if [ -n "$FOUND_APK" ] && [ -f "$FOUND_APK" ]; then
100+
cp "$FOUND_APK" "release-assets/${APK_NAME}"
101+
elif [ -f app/build/outputs/apk/release/app-release.apk ]; then
102+
cp app/build/outputs/apk/release/app-release.apk "release-assets/${APK_NAME}"
103+
else
104+
echo "Error: Release APK not found!"
105+
find . -name "*.apk"
106+
exit 1
107+
fi
96108
cd release-assets
97109
sha256sum "${APK_NAME}" > "${APK_NAME}.sha256"
98110
echo "apk_name=${APK_NAME}" >> $GITHUB_OUTPUT

app/build.gradle.kts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ android {
99
namespace = "com.example"
1010
compileSdk { version = release(36) { minorApiLevel = 1 } }
1111

12-
val envVersionCode = System.getenv("APP_VERSION_CODE")?.toIntOrNull() ?: 109
13-
val envVersionName = System.getenv("APP_VERSION_NAME") ?: "1.0.109"
12+
val envVersionCode = System.getenv("APP_VERSION_CODE")?.toIntOrNull() ?: 110
13+
val envVersionName = System.getenv("APP_VERSION_NAME") ?: "1.0.110"
1414

1515
defaultConfig {
1616
applicationId = "apps.feather.browser"

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

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,12 @@ import com.example.privacy.ContentBlocker
2020
import com.example.privacy.PrivacyManager
2121
import com.example.weather.WeatherRepository
2222
import com.example.weather.WeatherUiState
23+
import kotlinx.coroutines.ExperimentalCoroutinesApi
2324
import kotlinx.coroutines.flow.*
2425
import kotlinx.coroutines.launch
2526
import java.util.UUID
2627

28+
@OptIn(ExperimentalCoroutinesApi::class)
2729
class BrowserViewModel(application: Application) : AndroidViewModel(application) {
2830

2931
private val context: Context get() = getApplication()
@@ -602,7 +604,20 @@ class BrowserViewModel(application: Application) : AndroidViewModel(application)
602604
isPrivate = isPrivate
603605
)
604606
repository.saveTab(newTab)
605-
selectTab(tabId, autoDismiss = autoDismiss)
607+
setBarsVisible(true)
608+
_activeTabId.value = tabId
609+
_activeTabState.value = ActiveTabState(
610+
id = tabId,
611+
profileId = profileId,
612+
url = url,
613+
title = if (url.isBlank()) "New Tab" else url,
614+
isPrivate = isPrivate,
615+
isDesktopMode = false,
616+
blockedCount = 0
617+
)
618+
if (autoDismiss) {
619+
dismissSheet()
620+
}
606621
}
607622
}
608623

@@ -873,7 +888,7 @@ class BrowserViewModel(application: Application) : AndroidViewModel(application)
873888
}
874889
}
875890
viewModelScope.launch {
876-
val cur = currentTabs.value.find { it.id == tabId }
891+
val cur = currentTabs.value.find { it.id == tabId } ?: repository.getTab(tabId)
877892
if (cur != null && cur.url != url) {
878893
repository.saveTab(cur.copy(url = url, lastAccessedAt = System.currentTimeMillis()))
879894
}
@@ -889,7 +904,7 @@ class BrowserViewModel(application: Application) : AndroidViewModel(application)
889904
}
890905
}
891906
viewModelScope.launch {
892-
val cur = currentTabs.value.find { it.id == tabId }
907+
val cur = currentTabs.value.find { it.id == tabId } ?: repository.getTab(tabId)
893908
if (cur != null) {
894909
if (cur.title != title) {
895910
repository.saveTab(cur.copy(title = title))

app/src/main/java/com/example/data/BrowserRepository.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,10 @@ class BrowserRepository(private val database: AppDatabase) {
152152
}
153153

154154
// Tabs
155+
suspend fun getTab(tabId: String): BrowserTab? = withContext(Dispatchers.IO) {
156+
database.tabDao().getTabById(tabId)
157+
}
158+
155159
suspend fun saveTab(tab: BrowserTab) = withContext(Dispatchers.IO) {
156160
database.tabDao().insertTab(tab)
157161
}

app/src/main/java/com/example/data/dao/Daos.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ interface ProfileDao {
2424

2525
@Dao
2626
interface TabDao {
27+
@Query("SELECT * FROM browser_tabs WHERE id = :tabId LIMIT 1")
28+
suspend fun getTabById(tabId: String): BrowserTab?
29+
2730
@Query("SELECT * FROM browser_tabs WHERE profileId = :profileId AND isPrivate = 0 ORDER BY lastAccessedAt DESC")
2831
fun getTabsForProfile(profileId: String): Flow<List<BrowserTab>>
2932

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,13 @@ object MediaSessionManager {
151151

152152
fun onMediaEnded(context: Context, tabId: String) {
153153
if (_activeMediaTabId.value == tabId || _activeMediaTabId.value == null) {
154-
stopPlayback(context)
154+
scope.launch {
155+
// Short grace period to allow YouTube playlist or next track autoplay to start seamlessly
156+
kotlinx.coroutines.delay(1000)
157+
if (!_isPlaying.value && (_activeMediaTabId.value == tabId || _activeMediaTabId.value == null)) {
158+
stopPlayback(context)
159+
}
160+
}
155161
}
156162
}
157163

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

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import androidx.compose.foundation.layout.*
1212
import androidx.compose.material3.*
1313
import androidx.compose.runtime.*
1414
import androidx.compose.ui.Modifier
15+
import androidx.compose.ui.graphics.graphicsLayer
1516
import androidx.compose.ui.input.pointer.pointerInput
1617
import androidx.compose.ui.platform.LocalContext
1718
import androidx.compose.ui.platform.LocalFocusManager
@@ -294,16 +295,30 @@ fun BrowserScreen(
294295
.padding(top = effectiveTopPadding, bottom = effectiveBottomPadding)
295296
.background(MaterialTheme.colorScheme.background)
296297
) {
298+
val loadedTabIds = remember { mutableStateMapOf<String, Boolean>() }
299+
val openTabIdSet = remember(openTabs) { openTabs.map { it.id }.toSet() }
300+
LaunchedEffect(openTabIdSet) {
301+
val toRemove = loadedTabIds.keys.filter { it !in openTabIdSet }
302+
toRemove.forEach { loadedTabIds.remove(it) }
303+
}
304+
297305
// Persistent WebViews for open tabs to keep background playback and prevent reloads
298306
for (tab in openTabs) {
299-
val hasLoadedUrl = tab.url.isNotBlank() && tab.url != "about:blank"
300307
val isActive = (tab.id == activeTabId && !isHome)
301-
if (hasLoadedUrl || isActive) {
308+
val hasLoadedUrl = tab.url.isNotBlank() && tab.url != "about:blank"
309+
if (isActive || hasLoadedUrl) {
310+
loadedTabIds[tab.id] = true
311+
}
312+
val shouldRender = loadedTabIds[tab.id] == true || isActive
313+
if (shouldRender) {
302314
key(tab.id) {
303315
Box(
304316
modifier = Modifier
305317
.fillMaxSize()
306318
.zIndex(if (isActive) 1f else 0f)
319+
.graphicsLayer {
320+
alpha = if (isActive) 1f else 0f
321+
}
307322
.then(
308323
if (!isActive) Modifier.pointerInput(Unit) {} else Modifier
309324
)

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

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,36 @@ class PersistentWebView(context: Context) : WebView(context) {
109109
} catch (e: Throwable) { }
110110
}
111111

112+
override fun isShown(): Boolean {
113+
return if (allowBackgroundPlayback) true else super.isShown()
114+
}
115+
116+
override fun getVisibility(): Int {
117+
return if (allowBackgroundPlayback) View.VISIBLE else super.getVisibility()
118+
}
119+
120+
override fun getWindowVisibility(): Int {
121+
return if (allowBackgroundPlayback) View.VISIBLE else super.getWindowVisibility()
122+
}
123+
124+
override fun hasWindowFocus(): Boolean {
125+
return if (allowBackgroundPlayback) true else super.hasWindowFocus()
126+
}
127+
128+
override fun onWindowFocusChanged(hasWindowFocus: Boolean) {
129+
try {
130+
val effectiveFocus = if (allowBackgroundPlayback) true else hasWindowFocus
131+
super.onWindowFocusChanged(effectiveFocus)
132+
} catch (e: Throwable) { }
133+
}
134+
135+
override fun dispatchWindowFocusChanged(hasFocus: Boolean) {
136+
try {
137+
val effectiveFocus = if (allowBackgroundPlayback) true else hasFocus
138+
super.dispatchWindowFocusChanged(effectiveFocus)
139+
} catch (e: Throwable) { }
140+
}
141+
112142
override fun onPause() {
113143
if (!allowBackgroundPlayback) {
114144
try {
@@ -399,7 +429,8 @@ fun WebViewContainer(
399429
factory = { ctx ->
400430
val swipeRefresh = SwipeRefreshLayout(ctx).apply {
401431
isNestedScrollingEnabled = true
402-
visibility = if (isActive) View.VISIBLE else View.GONE
432+
visibility = View.VISIBLE
433+
isEnabled = (customVideoView == null) && isActive
403434
layoutParams = ViewGroup.LayoutParams(
404435
ViewGroup.LayoutParams.MATCH_PARENT,
405436
ViewGroup.LayoutParams.MATCH_PARENT
@@ -934,7 +965,7 @@ fun WebViewContainer(
934965
},
935966
update = { swipeRefresh ->
936967
swipeRefreshRef = swipeRefresh
937-
swipeRefresh.visibility = if (isActive) View.VISIBLE else View.GONE
968+
swipeRefresh.visibility = View.VISIBLE
938969
val webView = (0 until swipeRefresh.childCount)
939970
.map { swipeRefresh.getChildAt(it) }
940971
.filterIsInstance<PersistentWebView>()
@@ -946,8 +977,8 @@ fun WebViewContainer(
946977
webView.allowBackgroundPlayback = enableBackgroundPlay
947978
}
948979

949-
// Pull-to-refresh enabled unless custom video view is active
950-
swipeRefresh.isEnabled = (customVideoView == null)
980+
// Pull-to-refresh enabled unless custom video view is active, and only for active tab
981+
swipeRefresh.isEnabled = (customVideoView == null) && isActive
951982

952983
val primaryColor = if (effectiveDark) android.graphics.Color.parseColor("#80D8FF") else android.graphics.Color.parseColor("#00668B")
953984
val progressBgColor = if (effectiveDark) android.graphics.Color.parseColor("#2C2C2C") else android.graphics.Color.WHITE

0 commit comments

Comments
 (0)