MainActivity.kt
package com.yojnaportal.app
import android.annotation.SuppressLint
import android.app.DownloadManager
import android.content.ActivityNotFoundException
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.Color
import android.graphics.drawable.Drawable
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Environment
import android.os.Handler
import android.provider.Settings
import android.speech.tts.TextToSpeech
import android.view.GestureDetector
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.webkit.CookieManager
import android.webkit.JavascriptInterface
import android.provider.OpenableColumns
import android.webkit.WebResourceRequest
import android.webkit.WebSettings
import android.webkit.WebView
import android.webkit.WebViewClient
import android.widget.Button
import android.widget.ImageButton
import android.widget.ProgressBar
import android.widget.TextView
import android.widget.Toast
import androidx.activity.OnBackPressedCallback
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.content.res.AppCompatResources
import androidx.appcompat.widget.PopupMenu
import androidx.core.content.FileProvider
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.navigation.NavigationBarView
import com.google.android.material.progressindicator.CircularProgressIndicator
import com.yojnaportal.app.databinding.ActivityMainBinding
import org.json.JSONArray
import org.json.JSONObject
import java.io.File
import java.util.Locale
import androidx.webkit.WebSettingsCompat
import androidx.webkit.WebViewFeature
/** Custom history entry used by history.html */
data class HistoryEntry(val title: String, val url: String, val time: Long)
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private var currentItem = R.id.nav_home
private lateinit var wv: WebView
private lateinit var progressBar: ProgressBar
private lateinit var loadingSpinner: ProgressBar
private lateinit var swipeRefresh: SwipeRefreshLayout
private var sidebarSheet: BottomSheetDialog? = null
private var onlineMenuSheet: BottomSheetDialog? = null
private var tts: TextToSpeech? = null
// Desktop mode state (runtime; persisted in settings)
private var isDesktopMode = false
// History cap
private val HISTORY_CAP = 400
// Share content
private val APP_SHARE_TEXT = "Yojna Portal App – नवीनतम योजनाएँ और जॉब अपडेट्स पाएँ।"
private val APP_SHARE_URL by lazy {
"https://play.google.com/store/apps/details?id=" + applicationContext.packageName
}
// ----- Download chip + manager -----
private var currentDownloadId: Long = -1L
private val dm by lazy { getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager }
private val pollHandler by lazy { Handler(mainLooper) }
private val dlChip by lazy { findViewById(R.id.dlChip) }
private val dlProgressCircle by lazy { findViewById(R.id.dlProgressCircle) }
private val dlPercent by lazy { findViewById(R.id.dlPercent) }
private var lastDownloadedFileUri: Uri? = null
companion object {
private const val SETTINGS_PREFS = "AppSettings"
private const val SETTINGS_KEY_JSON = "settings_json"
private const val DESKTOP_UA =
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
private const val MOBILE_UA =
"Mozilla/5.0 (Linux; Android 13; Device) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36"
private val DEFAULT_SETTINGS = JSONObject(
"""{"darkMode":false,"desktopMode":false,"zoomControl":true,"themeColor":"blue"}"""
)
}
private fun readSettings(): JSONObject {
val prefs = getSharedPreferences(SETTINGS_PREFS, MODE_PRIVATE)
val raw = prefs.getString(SETTINGS_KEY_JSON, null)
return try {
if (raw.isNullOrEmpty()) DEFAULT_SETTINGS
else JSONObject(raw)
} catch (_: Exception) {
DEFAULT_SETTINGS
}
}
private fun writeSettings(newJson: JSONObject) {
getSharedPreferences(SETTINGS_PREFS, MODE_PRIVATE)
.edit().putString(SETTINGS_KEY_JSON, newJson.toString()).apply()
}
// ---------- Dark-mode fallback CSS injection ----------
private fun postInjectDarkCss() {
val js = """
(function(){
try{
if (document.getElementById('__dark_inject__')) return;
var css = `
html, body { background:#0b1220 !important; color:#e5e7eb !important; }
a { color:#93c5fd !important; }
a:hover { color:#33FF33 !important; }
html { filter: invert(1) hue-rotate(180deg) !important; }
img, picture, video, canvas, svg { filter: invert(1) hue-rotate(180deg) !important; }
input, textarea, select, button { background:#111827 !important; color:#e5e7eb !important; border-color:#374151 !important; }
`;
var s = document.createElement('style');
s.id='__dark_inject__';
s.type='text/css';
s.appendChild(document.createTextNode(css));
document.documentElement.appendChild(s);
}catch(e){}
})();
""".trimIndent()
wv.evaluateJavascript(js, null)
}
/** Apply user settings on WebView + app chrome */
private fun applyUserSettings() {
val s = readSettings()
val enabled = s.optBoolean("enabled", true)
val darkMode = s.optBoolean("darkMode", false)
val desktop = s.optBoolean("desktopMode", false)
val zoom = s.optBoolean("zoomControl", true)
val themeColor = s.optString("themeColor", "blue")
val applyLocal = s.optBoolean("applyLocal", true)
isDesktopMode = desktop
wv.settings.apply {
builtInZoomControls = zoom
displayZoomControls = false
setSupportZoom(zoom)
useWideViewPort = desktop
loadWithOverviewMode = desktop
userAgentString = if (desktop) DESKTOP_UA else MOBILE_UA
}
if (WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK)) {
WebSettingsCompat.setForceDark(
wv.settings,
if (enabled && darkMode) WebSettingsCompat.FORCE_DARK_ON
else WebSettingsCompat.FORCE_DARK_OFF
)
}
if (WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK_STRATEGY)) {
WebSettingsCompat.setForceDarkStrategy(
wv.settings,
WebSettingsCompat.DARK_STRATEGY_PREFER_WEB_THEME_OVER_USER_AGENT_DARKENING
)
}
if (enabled && darkMode) {
val url = currentUrl()
if (isOnlineUrl(url) || applyLocal) postInjectDarkCss()
}
window.statusBarColor = when (themeColor) {
"green" -> Color.parseColor("#065f46")
"purple" -> Color.parseColor("#5b21b6")
else -> Color.parseColor("#1e40af")
}
}
@SuppressLint("SetJavaScriptEnabled")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
// Views
wv = binding.webView
progressBar = findViewById(R.id.progressBar)
loadingSpinner = findViewById(R.id.loadingSpinner)
swipeRefresh = findViewById(R.id.swipeRefresh)
// WebView settings
wv.settings.apply {
javaScriptEnabled = true
domStorageEnabled = true
databaseEnabled = true
loadsImagesAutomatically = true
setSupportZoom(false)
builtInZoomControls = false
displayZoomControls = false
useWideViewPort = true
loadWithOverviewMode = true
mediaPlaybackRequiresUserGesture = true
allowFileAccess = true
try { safeBrowsingEnabled = true } catch (_: Exception) {}
try { mixedContentMode = WebSettings.MIXED_CONTENT_NEVER_ALLOW } catch (_: Exception) {}
}
try { WebView.setWebContentsDebuggingEnabled(true) } catch (_: Exception) {}
// JS bridge
wv.addJavascriptInterface(AndroidBridge(), "Android")
// URL strip views (header_online_layout.xml)
val onlineHeader = findViewById(R.id.onlineHeader)
val onlineUrlTxt = findViewById(R.id.onlineUrl)
val onlineTitleTxt = findViewById(R.id.onlineTitle)
// Pull to refresh
swipeRefresh.setOnRefreshListener {
val url = currentUrl()
if (isOnlineUrl(url) && !isConnected()) {
showOfflinePage()
swipeRefresh.isRefreshing = false
} else {
wv.reload()
}
}
wv.setOnScrollChangeListener { _, _, scrollY, _, _ ->
swipeRefresh.isEnabled = (scrollY == 0)
}
// WebViewClient: offline + loaders + SOCIAL + PDF
wv.webViewClient = object : WebViewClient() {
// Modern callback (API 21+)
override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean {
val targetUri = request.url
val target = targetUri.toString()
if (isOnlineUrl(target) && !isConnected()) {
showOfflinePage()
return true
}
// PDFs: DM + progress chip + open existing (canonical URL)
if (isPdfUrl(target)) {
if (openMappedFileIfExists(target)) return true
val safeName = ((view.title ?: "Document").ifBlank { "Document" })
.replace("\\s+".toRegex(), "_") + ".pdf"
startPdfDownloadWithProgress(target, safeName)
return true
}
// Social / External intents
if (tryHandleExternalIntents(targetUri)) return true
return false
}
// Deprecated callback (still triggered by some sites)
@Suppress("DEPRECATION")
override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean {
if (isOnlineUrl(url) && !isConnected()) { showOfflinePage(); return true }
if (isPdfUrl(url)) {
if (openMappedFileIfExists(url)) return true
val safeName = ((view.title ?: "Document").ifBlank { "Document" })
.replace("\\s+".toRegex(), "_") + ".pdf"
startPdfDownloadWithProgress(url, safeName)
return true
}
if (tryHandleExternalIntents(Uri.parse(url))) return true
return false
}
override fun onPageStarted(view: WebView, url: String?, favicon: Bitmap?) {
loadingSpinner.visibility = View.VISIBLE
progressBar.visibility = View.VISIBLE
progressBar.progress = 0
val isOnline = isOnlineUrl(url)
if (isOnline && !isConnected()) {
showOfflinePage(); return
}
toggleHeaders(isOnline)
updateOnlineHeader("Loading…", url)
super.onPageStarted(view, url, favicon)
}
override fun onPageFinished(view: WebView, url: String) {
loadingSpinner.visibility = View.GONE
swipeRefresh.isRefreshing = false
if (!isOnlineUrl(url)) {
super.onPageFinished(view, url)
progressBar.visibility = View.GONE
return
}
toggleHeaders(true)
updateOnlineHeader(view.title, url)
val t = view.title ?: "Untitled"
saveHistory(t, url, System.currentTimeMillis())
val s = readSettings()
val enabled = s.optBoolean("enabled", true)
val darkMode = s.optBoolean("darkMode", false)
val applyLocal = s.optBoolean("applyLocal", true)
if (enabled && darkMode && (isOnlineUrl(url) || applyLocal)) postInjectDarkCss()
progressBar.visibility = View.GONE
super.onPageFinished(view, url)
}
}
// WebChromeClient: title updates
wv.webChromeClient = object : android.webkit.WebChromeClient() {
override fun onProgressChanged(view: WebView?, newProgress: Int) {
progressBar.visibility = View.VISIBLE
progressBar.progress = newProgress.coerceIn(0, 100)
if (newProgress >= 100) progressBar.visibility = View.GONE
}
override fun onReceivedTitle(view: WebView?, title: String?) {
updateOnlineHeader(if (title.isNullOrBlank()) "Loading…" else title, view?.url ?: currentUrl())
}
}
// Downloads (covers direct download flows too)
wv.setDownloadListener { url, userAgent, contentDisposition, mimeType, _ ->
val isPdf = (mimeType?.contains("pdf", ignoreCase = true) == true) || isPdfUrl(url)
if (isPdf) {
// ✅ first, canonical mapping + existing file
if (openMappedFileIfExists(url)) return@setDownloadListener
val name = guessFileName(url, contentDisposition)
// same-name exists? open + map
if (openExistingFileIfAny(name)) {
mapUrlToFile(url, name)
return@setDownloadListener
}
startPdfDownloadWithProgress(url, name)
return@setDownloadListener
}
// non-PDF fallback
try {
val request = DownloadManager.Request(Uri.parse(url)).apply {
setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
setMimeType(mimeType)
addRequestHeader("User-Agent", userAgent ?: "")
val filename = contentDisposition
?.substringAfter("filename=", "")
?.trim('"', ' ', ';')
?: Uri.parse(url).lastPathSegment
?: "download"
setTitle(filename)
setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, filename)
}
dm.enqueue(request)
Toast.makeText(this, "Download started…", Toast.LENGTH_SHORT).show()
} catch (_: Exception) {
if (isOnlineUrl(url) && !isConnected()) {
showOfflinePage()
} else {
openInChrome(url)
}
}
}
// Header gestures
val sharedDetector = GestureDetector(this, object : GestureDetector.SimpleOnGestureListener() {
override fun onDoubleTap(e: MotionEvent): Boolean {
val link = currentUrl()
copyToClipboard("URL", link)
Toast.makeText(this@MainActivity, "Full link copied", Toast.LENGTH_SHORT).show()
return true
}
override fun onLongPress(e: MotionEvent) {
showUrlDetailsBottomSheet(wv)
}
})
onlineHeader?.setOnTouchListener { _, ev -> sharedDetector.onTouchEvent(ev) }
onlineUrlTxt?.setOnTouchListener { _, ev -> sharedDetector.onTouchEvent(ev) }
onlineTitleTxt?.setOnTouchListener { _, ev -> sharedDetector.onTouchEvent(ev) }
// Apply settings then initial page
applyUserSettings()
loadPage("home.html")
// Bottom Navigation
binding.bottomNav.setOnItemSelectedListener(
NavigationBarView.OnItemSelectedListener { item ->
currentItem = item.itemId
when (item.itemId) {
R.id.nav_home -> loadPage("home.html")
R.id.nav_schemes -> loadPage("schemes.html")
R.id.nav_jobs -> loadPage("jobs.html")
R.id.nav_updates -> loadPage("updates.html")
R.id.nav_about -> loadPage("about.html")
}
true
}
)
// Local header buttons
findViewById(R.id.menuButton)?.setOnClickListener { showSidebar() }
findViewById(R.id.shareButton)?.setOnClickListener { shareApp() }
// Online header buttons
findViewById(R.id.onlineBack)?.setOnClickListener {
if (wv.canGoBack()) wv.goBack() else onBackPressedDispatcher.onBackPressed()
}
findViewById(R.id.onlineBookmarkSave)?.setOnClickListener {
val url = currentUrl()
val title = wv.title ?: "Bookmark"
saveBookmark(title, url)
}
findViewById(R.id.onlineMenu)?.setOnClickListener { showOnlineMenu() }
// Back handling
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
if (wv.canGoBack()) {
wv.goBack()
} else if (currentItem != R.id.nav_home) {
binding.bottomNav.selectedItemId = R.id.nav_home
loadPage("home.html")
} else {
finish()
}
}
})
}
override fun onResume() {
super.onResume()
applyUserSettings()
}
// ---------- Connectivity helpers ----------
private fun isConnected(): Boolean {
val cm = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val nw = cm.activeNetwork ?: return false
val caps = cm.getNetworkCapabilities(nw) ?: return false
return caps.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) ||
caps.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) ||
caps.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) ||
caps.hasTransport(NetworkCapabilities.TRANSPORT_BLUETOOTH)
}
private fun showOfflinePage() {
wv.loadUrl("file:///android_asset/offline.html")
toggleHeaders(false)
loadingSpinner.visibility = View.GONE
progressBar.visibility = View.GONE
swipeRefresh.isRefreshing = false
Toast.makeText(this, "You’re offline. Showing saved page.", Toast.LENGTH_SHORT).show()
}
// ---------- URL helpers & header toggle ----------
private fun isOnlineUrl(url: String?): Boolean {
if (url.isNullOrBlank()) return false
return url.startsWith("http://") || url.startsWith("https://")
}
private fun toggleHeaders(online: Boolean) {
val headerLocal = findViewById(R.id.headerLocal)
val headerOnline = findViewById(R.id.headerOnline)
if (online) {
headerLocal?.visibility = View.GONE
headerOnline?.visibility = View.VISIBLE
} else {
headerOnline?.visibility = View.GONE
headerLocal?.visibility = View.VISIBLE
}
}
private fun loadPage(fileName: String) {
wv.loadUrl("file:///android_asset/$fileName")
toggleHeaders(false)
}
private fun currentUrl(): String = wv.url ?: "https://yojnaportal.com"
// ---------- Header helpers ----------
private fun updateOnlineHeader(title: String?, url: String?) {
val onlineTitle = findViewById(R.id.onlineTitle)
val onlineUrl = findViewById(R.id.onlineUrl)
val finalTitle = when {
title.isNullOrBlank() -> "Loading…"
title.length > 50 -> title.take(47) + "…"
else -> title
}
onlineTitle?.text = finalTitle
updateUrlStrip(onlineUrl, url ?: currentUrl())
}
// ---------- Sidebar bottom-sheet ----------
@SuppressLint("SetJavaScriptEnabled")
private fun showSidebar() {
val view = layoutInflater.inflate(R.layout.bottom_sheet_sidebar, null, false)
val sideWV = view.findViewById(R.id.sidebarWebView)
sideWV.settings.javaScriptEnabled = true
sideWV.webViewClient = WebViewClient()
sideWV.addJavascriptInterface(AndroidBridge(), "Android")
sideWV.loadUrl("file:///android_asset/sidebar.html")
sidebarSheet?.dismiss()
sidebarSheet = BottomSheetDialog(this)
sidebarSheet?.setContentView(view)
sidebarSheet?.show()
}
// ---------- Online header menu (⋮) ----------
private fun showOnlineMenu() {
try {
val view = LayoutInflater.from(this).inflate(R.layout.bottom_sheet_online_menu, null, false)
view.findViewById(R.id.miShare).setOnClickListener {
shareText(currentUrl()); onlineMenuSheet?.dismiss()
}
view.findViewById(R.id.miDetails).setOnClickListener {
showPageDetails(); onlineMenuSheet?.dismiss()
}
view.findViewById(R.id.miClearHistory).setOnClickListener {
clearWebHistory(); clearCustomHistory()
onlineMenuSheet?.dismiss()
}
view.findViewById(R.id.miBookmark).setOnClickListener {
val url = currentUrl(); val title = wv.title ?: "Bookmark"
saveBookmark(title, url); onlineMenuSheet?.dismiss()
}
view.findViewById(R.id.miOpenChrome).setOnClickListener {
openInChrome(currentUrl()); onlineMenuSheet?.dismiss()
}
view.findViewById(R.id.miTranslate).setOnClickListener {
openTranslate(currentUrl()); onlineMenuSheet?.dismiss()
}
view.findViewById(R.id.miDownloads).setOnClickListener {
openDownloads(); onlineMenuSheet?.dismiss()
}
view.findViewById(R.id.miListen).setOnClickListener {
listenToPage(); onlineMenuSheet?.dismiss()
}
view.findViewById(R.id.miHistory).setOnClickListener {
wv.loadUrl("file:///android_asset/history.html"); onlineMenuSheet?.dismiss()
}
view.findViewById(R.id.miSettings)?.setOnClickListener {
wv.loadUrl("file:///android_asset/settings.html"); onlineMenuSheet?.dismiss()
}
view.findViewById(R.id.miDesktop)?.setOnClickListener {
toggleDesktopMode(); onlineMenuSheet?.dismiss()
}
onlineMenuSheet?.dismiss()
onlineMenuSheet = BottomSheetDialog(this)
onlineMenuSheet?.setContentView(view)
onlineMenuSheet?.show()
return
} catch (_: Throwable) { }
val anchor = findViewById(R.id.onlineMenu) ?: return
val popup = PopupMenu(this, anchor)
popup.menu.add(0, 1, 0, "Share link")
popup.menu.add(0, 2, 1, "Open in browser")
popup.menu.add(0, 3, 2, "Copy link")
popup.menu.add(0, 4, 3, "Refresh")
popup.menu.add(0, 5, 4, "Desktop mode")
popup.menu.add(0, 6, 5, "History")
popup.menu.add(0, 7, 6, "Clear cache & history")
popup.menu.add(0, 8, 7, "Settings")
popup.setOnMenuItemClickListener { item ->
when (item.itemId) {
1 -> { shareText(currentUrl()); true }
2 -> { openInChrome(currentUrl()); true }
3 -> { copyToClipboard("URL", currentUrl()); Toast.makeText(this, "Link copied", Toast.LENGTH_SHORT).show(); true }
4 -> { if (isOnlineUrl(currentUrl()) && !isConnected()) showOfflinePage() else wv.reload(); true }
5 -> { toggleDesktopMode(); true }
6 -> { wv.loadUrl("file:///android_asset/history.html"); true }
7 -> { clearWebHistory(); clearCustomHistory(); true }
8 -> { wv.loadUrl("file:///android_asset/settings.html"); true }
else -> false
}
}
popup.show()
}
// ---------- Desktop Mode Toggle ----------
private fun toggleDesktopMode() {
try {
isDesktopMode = !isDesktopMode
val s = readSettings()
s.put("desktopMode", isDesktopMode)
writeSettings(s)
wv.settings.apply {
userAgentString = if (isDesktopMode) DESKTOP_UA else MOBILE_UA
useWideViewPort = isDesktopMode
loadWithOverviewMode = isDesktopMode
}
Toast.makeText(this, if (isDesktopMode) "Desktop mode on" else "Desktop mode off", Toast.LENGTH_SHORT).show()
if (isOnlineUrl(currentUrl()) && !isConnected()) showOfflinePage() else wv.reload()
} catch (_: Exception) { }
}
// ---------- JS Bridge ----------
inner class AndroidBridge {
@JavascriptInterface fun shareApp() { runOnUiThread { shareApp() } }
@JavascriptInterface fun shareCurrent() { runOnUiThread { shareText(currentUrl()) } }
@JavascriptInterface fun refreshPage() {
runOnUiThread {
if (isOnlineUrl(currentUrl()) && !isConnected()) showOfflinePage() else wv.reload()
}
}
@JavascriptInterface
fun getDownloadHistoryJson(): String {
val arr = JSONArray()
getDownloadHistory().forEach {
arr.put(JSONObject().apply {
put("fileName", it.fileName)
put("uri", it.localUri)
put("time", it.finishedAt)
put("status", it.status)
// ✅ ये दो लाइनें ज़रूर जोड़ें
put("pageUrl", it.pageUrl)
put("pageTitle", it.pageTitle)
})
}
return arr.toString()
}
@JavascriptInterface
fun openWebPage(url: String) {
runOnUiThread {
if (url.isBlank()) {
Toast.makeText(this@MainActivity, "Invalid post link", Toast.LENGTH_SHORT).show()
return@runOnUiThread
}
wv.loadUrl(url)
}
}
@JavascriptInterface
fun openDownloadedFile(uri: String) {
runOnUiThread {
try {
val u = Uri.parse(uri)
openInGoogleDrive(u)
} catch (_: Exception) {
Toast.makeText(this@MainActivity, "Unable to open file", Toast.LENGTH_SHORT).show()
}
}
}
@JavascriptInterface
fun removeDownloadHistoryByUri(uri: String) {
runOnUiThread {
val set = dlPrefsSet()
set.removeIf {
val ex = decodeDownloadEntry(it)
ex != null && ex.localUri == uri
}
getSharedPreferences("download_history_store", MODE_PRIVATE)
.edit().putStringSet("list", set).apply()
Toast.makeText(this@MainActivity, "Deleted", Toast.LENGTH_SHORT).show()
}
}
@JavascriptInterface
fun clearDownloadHistory() {
runOnUiThread {
getSharedPreferences("download_history_store", MODE_PRIVATE)
.edit().putStringSet("list", mutableSetOf()).apply()
Toast.makeText(this@MainActivity, "All history cleared", Toast.LENGTH_SHORT).show()
}
}
@JavascriptInterface fun openAppSettings() {
runOnUiThread {
startActivity(Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
data = Uri.parse("package:$packageName")
})
}
}
@JavascriptInterface fun getSettings(): String = readSettings().toString()
@JavascriptInterface fun saveSettings(json: String) {
runCatching {
val obj = JSONObject(json)
writeSettings(obj)
runOnUiThread { applyUserSettings() }
Toast.makeText(this@MainActivity, "Settings saved", Toast.LENGTH_SHORT).show()
}
}
@JavascriptInterface fun addBookmark() {
runOnUiThread {
val url = currentUrl(); val title = wv.title ?: "Bookmark"
saveBookmark(title, url)
Toast.makeText(this@MainActivity, "Bookmarked ✔", Toast.LENGTH_SHORT).show()
}
}
@JavascriptInterface fun openBookmarksPage() { runOnUiThread { wv.loadUrl("file:///android_asset/bookmarks.html") } }
@JavascriptInterface fun getBookmarksJson(): String {
val list = getBookmarks()
val arr = JSONArray()
list.forEach { (title, url) ->
arr.put(JSONObject().apply { put("title", title); put("url", url) })
}
return arr.toString()
}
@JavascriptInterface fun removeBookmark(url: String) {
runOnUiThread {
removeBookmarkInternal(url)
Toast.makeText(this@MainActivity, "Removed", Toast.LENGTH_SHORT).show()
}
}
@JavascriptInterface fun getHistoryJson(): String {
val arr = JSONArray()
getHistory().forEach {
arr.put(JSONObject().apply {
put("title", it.title)
put("url", it.url)
put("time", it.time)
})
}
return arr.toString()
}
@JavascriptInterface fun removeHistory(url: String) { runOnUiThread { removeHistoryInternal(url) } }
@JavascriptInterface fun openHistoryPage() { runOnUiThread { wv.loadUrl("file:///android_asset/history.html") } }
@JavascriptInterface fun openUrl(url: String) {
runOnUiThread {
if (isOnlineUrl(url) && !isConnected()) { showOfflinePage() } else { wv.loadUrl(url) }
}
}
@JavascriptInterface fun openSettingsPage() { runOnUiThread { wv.loadUrl("file:///android_asset/settings.html") } }
@JavascriptInterface fun closeSheet() { runOnUiThread { sidebarSheet?.dismiss() } }
@JavascriptInterface fun goBackFromBookmarks() { runOnUiThread { onBackPressedDispatcher.onBackPressed() } }
}
// ---------- Share helpers ----------
private fun shareApp() = shareText("$APP_SHARE_TEXT\n$APP_SHARE_URL")
private fun shareText(text: String) {
try {
val i = Intent(Intent.ACTION_SEND).apply {
type = "text/plain"
putExtra(Intent.EXTRA_SUBJECT, "Yojna Portal")
putExtra(Intent.EXTRA_TEXT, text)
}
startActivity(Intent.createChooser(i, "Share via"))
} catch (_: ActivityNotFoundException) { }
}
private fun openInChrome(url: String) {
if (isOnlineUrl(url) && !isConnected()) { showOfflinePage(); return }
try {
val i = Intent(Intent.ACTION_VIEW, Uri.parse(url)).apply { setPackage("com.android.chrome") }
startActivity(i)
} catch (_: Exception) {
try { startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url))) } catch (_: Exception) { }
}
}
private fun openTranslate(url: String) {
if (isOnlineUrl(url) && !isConnected()) { showOfflinePage(); return }
val tUrl = "https://translate.google.com/translate?sl=auto&tl=hi&u=" + Uri.encode(url)
try { startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(tUrl))) } catch (_: Exception) { }
}
private fun openDownloads() {
try { startActivity(Intent(DownloadManager.ACTION_VIEW_DOWNLOADS)) }
catch (_: Exception) { Toast.makeText(this, "Downloads not available", Toast.LENGTH_SHORT).show() }
}
private fun showPageDetails() {
val title = wv.title ?: "Untitled"
val url = currentUrl()
val msg = "Title:\n$title\n\nURL:\n$url"
AlertDialog.Builder(this).setTitle("Page detail").setMessage(msg).setPositiveButton("OK", null).show()
}
private fun clearWebHistory() {
try {
wv.clearHistory()
wv.clearCache(true)
Toast.makeText(this, "Browsing cache cleared", Toast.LENGTH_SHORT).show()
} catch (_: Exception) { }
}
private fun listenToPage() {
try {
wv.evaluateJavascript("(function(){return document.body && document.body.innerText ? document.body.innerText : ''})()") { text ->
val spoken = text?.trim('"')?.replace("\\n", "\n")?.replace("\\t", " ") ?: ""
if (spoken.isBlank()) {
Toast.makeText(this, "Nothing to read on this page", Toast.LENGTH_SHORT).show()
return@evaluateJavascript
}
if (tts == null) {
tts = TextToSpeech(this) { status ->
if (status == TextToSpeech.SUCCESS) {
tts?.language = Locale.getDefault()
tts?.speak(spoken, TextToSpeech.QUEUE_FLUSH, null, "page-tts")
} else Toast.makeText(this, "TTS not available", Toast.LENGTH_SHORT).show()
}
} else {
tts?.speak(spoken, TextToSpeech.QUEUE_FLUSH, null, "page-tts")
}
}
} catch (_: Exception) {
Toast.makeText(this, "Unable to read this page", Toast.LENGTH_SHORT).show()
}
}
// ---------- URL strip + details ----------
private fun updateUrlStrip(urlTxt: TextView?, url: String?) {
if (urlTxt == null || url.isNullOrBlank()) return
val hostShown = domainFrom(url) ?: "—"
urlTxt.text = hostShown
val isHttps = url.startsWith("https://", true)
val iconRes = if (isHttps) R.drawable.ic_lock_24 else R.drawable.ic_warning_24
val d: Drawable? = AppCompatResources.getDrawable(this, iconRes)
?: AppCompatResources.getDrawable(this, android.R.drawable.stat_sys_warning)
urlTxt.setCompoundDrawablesRelativeWithIntrinsicBounds(d, null, null, null)
urlTxt.alpha = if (isHttps) 0.95f else 0.85f
}
/** host -> registrable main domain */
private fun domainFrom(url: String): String? {
val uri = runCatching { Uri.parse(url) }.getOrNull() ?: return null
val rawHost = uri.host ?: return null
val host = rawHost.removePrefix("www.")
val parts = host.split('.')
if (parts.size <= 2) return host
val multiTlds = setOf(
"co.in","gov.in","nic.in","ac.in","edu.in","res.in","mil.in",
"co.uk","gov.uk","ac.uk",
"com.au","net.au","org.au"
)
val last2 = parts.takeLast(2).joinToString(".")
val last3 = parts.takeLast(3).joinToString(".")
return when {
multiTlds.contains(last2) && parts.size >= 3 -> parts.takeLast(3).joinToString(".")
multiTlds.contains(last3) && parts.size >= 4 -> parts.takeLast(4).joinToString(".")
else -> last2
}
}
private fun copyToClipboard(label: String, text: String) {
val cm = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
cm.setPrimaryClip(ClipData.newPlainText(label, text))
}
private fun showUrlDetailsBottomSheet(webView: WebView) {
val dialog = BottomSheetDialog(this)
val view = LayoutInflater.from(this).inflate(R.layout.bottomsheet_url_details, null)
dialog.setContentView(view)
val tvHost = view.findViewById(R.id.tvHost)
val tvFullUrl = view.findViewById(R.id.tvFullUrl)
val tvScheme = view.findViewById(R.id.tvScheme)
val tvSecurity = view.findViewById(R.id.tvSecurity)
val tvCert = view.findViewById(R.id.tvCert)
val btnHistory = view.findViewById