summaryrefslogtreecommitdiff
path: root/app/src/main/java/org/terst/feedreader/ui/BrowserScreen.kt
blob: f26a35c96d805f6b8b8bd19577d61bed42f1eae5 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
package org.terst.feedreader.ui

import android.annotation.SuppressLint
import android.content.Intent
import android.net.Uri
import android.webkit.CookieManager
import android.webkit.WebChromeClient
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.OpenInBrowser
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.viewinterop.AndroidView

/**
 * A real, interactive mini-browser -- JS and DOM storage on, ordinary navigation, no
 * Readability interception. Unlike LinkReaderScreen (always tries to extract a clean
 * article view), this is for the two cases where the user needs to actually USE a
 * page rather than just read it: browsing search results to find an alternate,
 * non-paywalled source for a story, or logging into a site they already subscribe to.
 *
 * Login needs no extra plumbing beyond turning cookies on: Android's CookieManager is
 * a single process-wide store shared by every WebView in the app, so once a session
 * cookie is set here, the reader's (JS-off) extraction WebView picks it up automatically
 * on the next visit to that domain -- same as a normal browser remembering you're signed in.
 */
@SuppressLint("SetJavaScriptEnabled")
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun BrowserScreen(startUrl: String, onBack: () -> Unit) {
    val context = LocalContext.current
    var title by remember(startUrl) { mutableStateOf<String?>(null) }
    var webViewRef by remember(startUrl) { mutableStateOf<WebView?>(null) }

    // Real browser-style back: step back through THIS WebView's own page history first
    // (search results -> article -> another link -> ...) and only close the screen once
    // there's nowhere left to go back to. An earlier version made back always close the
    // screen outright, on the theory that stepping through a deep session one page at a
    // time was tedious -- but that meant a single back press (system gesture or the in-app
    // arrow) could silently throw away an entire multi-page browsing/login session, which
    // is worse: there's no way to recover it, and it doesn't match how every other browser
    // (or in-app browser view) on the platform behaves. Both the system back gesture and
    // the in-app arrow go through the same path below, so they behave identically.
    fun handleBack() {
        val webView = webViewRef
        if (webView != null && webView.canGoBack()) {
            webView.goBack()
        } else {
            onBack()
        }
    }
    BackHandler(onBack = ::handleBack)

    Scaffold(
        topBar = {
            TopAppBar(
                title = { Text(title ?: "Browser", maxLines = 1, overflow = TextOverflow.Ellipsis) },
                navigationIcon = {
                    IconButton(onClick = ::handleBack) {
                        Icon(Icons.Default.ArrowBack, contentDescription = "Back")
                    }
                },
                actions = {
                    IconButton(onClick = {
                        val url = webViewRef?.url ?: startUrl
                        context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)))
                    }) {
                        Icon(Icons.Default.OpenInBrowser, contentDescription = "Open in browser")
                    }
                }
            )
        }
    ) { padding ->
        Box(modifier = Modifier.fillMaxSize().padding(padding)) {
            AndroidView(
                modifier = Modifier.fillMaxSize(),
                factory = { ctx ->
                    WebView(ctx).apply {
                        settings.javaScriptEnabled = true
                        settings.domStorageEnabled = true
                        CookieManager.getInstance().setAcceptCookie(true)
                        // Deliberately NOT setAcceptThirdPartyCookies: the login-persistence
                        // feature this screen exists for (see the class doc comment) only
                        // needs a site's own FIRST-party session cookie to reach the
                        // extraction WebView -- third-party cookies are for cross-site
                        // trackers embedded within whatever page is loaded here, which this
                        // feature has no use for and which only adds tracking surface shared
                        // with every other WebView in the app.
                        webViewClient = object : WebViewClient() {
                            override fun onPageFinished(view: WebView, url: String) {
                                title = view.title
                            }
                        }
                        webChromeClient = WebChromeClient()
                        loadUrl(startUrl)
                        webViewRef = this
                    }
                },
                onRelease = { webView ->
                    // Flush explicitly so a just-completed login survives even if the
                    // process is killed shortly after (modern WebView auto-persists, but
                    // this makes it immediate rather than relying on that timing).
                    CookieManager.getInstance().flush()
                    webView.destroy()
                }
            )
        }
    }
}