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(R.id.miHistory) val btnCopy = view.findViewById(R.id.btnCopyUrl) val btnOpen = view.findViewById(R.id.btnOpenExternal) val btnClear = view.findViewById(R.id.btnClearCache) val url = webView.url ?: "" val uri = runCatching { Uri.parse(url) }.getOrNull() val scheme = uri?.scheme ?: "-" val host = uri?.host ?: "-" tvHost.text = host tvFullUrl.text = url tvScheme.text = "Scheme: $scheme" val isHttps = scheme.equals("https", true) tvSecurity.text = if (isHttps) "Security: Secure (HTTPS)" else "Security: Not secure (HTTP)" tvCert.text = "Certificate: ${getLastKnownCertificateInfo(webView) ?: "—"}" btnHistory?.setOnClickListener { dialog.dismiss() wv.loadUrl("file:///android_asset/history.html") } btnCopy.setOnClickListener { copyToClipboard("URL", url) Toast.makeText(this, "Link copied", Toast.LENGTH_SHORT).show() } btnOpen.setOnClickListener { runCatching { startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url))) } } btnClear.setOnClickListener { clearWebViewCacheForHost(webView, host) Toast.makeText(this, "Cache & cookies cleared", Toast.LENGTH_SHORT).show() } dialog.show() } private fun clearWebViewCacheForHost(webView: WebView, host: String?) { webView.clearCache(true) val cm = CookieManager.getInstance() cm.setAcceptCookie(true) if (!host.isNullOrBlank()) { val cookieStr = cm.getCookie("https://$host") ?: cm.getCookie("http://$host") if (!cookieStr.isNullOrBlank()) { val parts = cookieStr.split(";").map { it.trim() } for (part in parts) { val name = part.substringBefore("=").trim() if (name.isNotEmpty()) { cm.setCookie("https://$host", "$name=; Expires=Thu, 01 Jan 1970 00:00:00 GMT") cm.setCookie("http://$host", "$name=; Expires=Thu, 01 Jan 1970 00:00:00 GMT") } } cm.flush() } else { cm.removeAllCookies(null); cm.flush() } } else { cm.removeAllCookies(null); cm.flush() } } private fun getLastKnownCertificateInfo(webView: WebView): String? { return if ((webView.url ?: "").startsWith("https://", true)) { "Available on secure sites (HTTPS)" } else null } // ---------- Bookmarks store ---------- private fun prefsSet(): MutableSet { val prefs = getSharedPreferences("bookmarks", MODE_PRIVATE) return (prefs.getStringSet("list", mutableSetOf()) ?: mutableSetOf()).toMutableSet() } private fun saveBookmark(title: String, url: String) { val set = prefsSet() set.removeIf { it.endsWith("|||$url") } set.add("${title.trim()}|||${url.trim()}") getSharedPreferences("bookmarks", MODE_PRIVATE).edit().putStringSet("list", set).apply() Toast.makeText(this, "Bookmarked ✔", Toast.LENGTH_SHORT).show() } private fun getBookmarks(): List> { val set = getSharedPreferences("bookmarks", MODE_PRIVATE) .getStringSet("list", emptySet()) ?: emptySet() return set.mapNotNull { val p = it.split("|||") if (p.size == 2) p[0] to p[1] else null }.sortedBy { it.first.lowercase() } } private fun removeBookmarkInternal(url: String) { val set = prefsSet() set.removeIf { it.endsWith("|||$url") } getSharedPreferences("bookmarks", MODE_PRIVATE).edit().putStringSet("list", set).apply() } // ---------- Custom History store ---------- private fun historyPrefsSet(): MutableSet { val prefs = getSharedPreferences("history_store", MODE_PRIVATE) return (prefs.getStringSet("list", mutableSetOf()) ?: mutableSetOf()).toMutableSet() } private fun saveHistory(title: String, url: String, time: Long) { if (!isOnlineUrl(url)) return val set = historyPrefsSet() set.removeIf { it.endsWith("|||$url") || it.contains("|||$url|||") } set.add("${title.trim()}|||${url.trim()}|||$time") if (set.size > HISTORY_CAP) { val sorted = set.mapNotNull { val p = it.split("|||") if (p.size == 3) p[2].toLongOrNull()?.let { t -> t to it } else null }.sortedBy { it.first } val overflow = set.size - HISTORY_CAP for (i in 0 until overflow) set.remove(sorted[i].second) } getSharedPreferences("history_store", MODE_PRIVATE).edit().putStringSet("list", set).apply() } private fun getHistory(): List { val set = getSharedPreferences("history_store", MODE_PRIVATE) .getStringSet("list", emptySet()) ?: emptySet() return set.mapNotNull { val p = it.split("|||") if (p.size == 3) { val t = p[0] val u = p[1] val ms = p[2].toLongOrNull() ?: 0L HistoryEntry(t, u, ms) } else null }.sortedByDescending { it.time } } private fun removeHistoryInternal(url: String) { val set = historyPrefsSet() set.removeIf { it.endsWith("|||$url") || it.contains("|||$url|||") } getSharedPreferences("history_store", MODE_PRIVATE).edit().putStringSet("list", set).apply() } private fun clearCustomHistory() { getSharedPreferences("history_store", MODE_PRIVATE).edit().putStringSet("list", mutableSetOf()).apply() } // ---------- Lifecycle ---------- override fun onPause() { super.onPause() try { tts?.stop() } catch (_: Exception) {} } override fun onDestroy() { try { tts?.stop(); tts?.shutdown() } catch (_: Exception) { } tts = null sidebarSheet?.dismiss(); sidebarSheet = null onlineMenuSheet?.dismiss(); onlineMenuSheet = null try { (wv.parent as? ViewGroup)?.removeView(wv) wv.stopLoading() wv.clearHistory() wv.destroy() } catch (_: Exception) {} super.onDestroy() } // ========================= // SOCIAL / EXTERNAL INTENTS // ========================= private data class AppRule( val packageName: String, val buildUri: (String) -> Uri, val playStorePkg: String = "" ) private fun isPackageInstalled(pkg: String): Boolean = runCatching { packageManager.getPackageInfo(pkg, 0); true }.getOrElse { false } private val socialRules: Map = mapOf( "whatsapp.com" to AppRule("com.whatsapp", { Uri.parse(it) }, "com.whatsapp"), "wa.me" to AppRule("com.whatsapp", { Uri.parse(it) }, "com.whatsapp"), "t.me" to AppRule("org.telegram.messenger", { url -> val seg = Uri.parse(url).lastPathSegment ?: "" if (seg.isNotEmpty()) Uri.parse("tg://resolve?domain=$seg") else Uri.parse(url) }, "org.telegram.messenger"), "telegram.me" to AppRule("org.telegram.messenger", { url -> val seg = Uri.parse(url).lastPathSegment ?: "" if (seg.isNotEmpty()) Uri.parse("tg://resolve?domain=$seg") else Uri.parse(url) }, "org.telegram.messenger"), "youtube.com" to AppRule("com.google.android.youtube", { url -> val u = Uri.parse(url); val v = u.getQueryParameter("v") if (v != null) Uri.parse("vnd.youtube:$v") else Uri.parse(url) }, "com.google.android.youtube"), "youtu.be" to AppRule("com.google.android.youtube", { url -> val id = Uri.parse(url).lastPathSegment ?: "" if (id.isNotEmpty()) Uri.parse("vnd.youtube:$id") else Uri.parse(url) }, "com.google.android.youtube"), "instagram.com" to AppRule("com.instagram.android", { url -> val segs = Uri.parse(url).pathSegments if (segs.isNotEmpty()) Uri.parse("http://instagram.com/_u/${segs[0]}") else Uri.parse(url) }, "com.instagram.android"), "facebook.com" to AppRule("com.facebook.katana", { url -> Uri.parse("fb://facewebmodal/f?href=$url") }, "com.facebook.katana"), "m.facebook.com" to AppRule("com.facebook.katana", { url -> Uri.parse("fb://facewebmodal/f?href=$url") }, "com.facebook.katana"), "twitter.com" to AppRule("com.twitter.android", { url -> val segs = Uri.parse(url).pathSegments if (segs.isNotEmpty()) Uri.parse("twitter://user?screen_name=${segs[0]}") else Uri.parse(url) }, "com.twitter.android"), "x.com" to AppRule("com.twitter.android", { url -> val segs = Uri.parse(url).pathSegments if (segs.isNotEmpty()) Uri.parse("twitter://user?screen_name=${segs[0]}") else Uri.parse(url) }, "com.twitter.android"), "linkedin.com" to AppRule("com.linkedin.android", { Uri.parse(it) }, "com.linkedin.android"), "arattai" to AppRule("com.zoho.arattai", { Uri.parse(it) }, "com.zoho.arattai") ) private fun tryHandleExternalIntents(url: Uri): Boolean { val scheme = url.scheme?.lowercase() ?: "" when (scheme) { "tel", "sms", "smsto", "mailto", "geo", "whatsapp", "tg", "twitter", "fb", "instagram", "vnd.youtube" -> { return runCatching { startActivity(Intent(Intent.ACTION_VIEW, url)) true }.getOrElse { false } } "intent" -> { return runCatching { val intent = Intent.parseUri(url.toString(), Intent.URI_INTENT_SCHEME) try { startActivity(intent); true } catch (_: ActivityNotFoundException) { val pkg = intent.`package` if (!pkg.isNullOrBlank()) { val ps = Intent(Intent.ACTION_VIEW, Uri.parse("https://play.google.com/store/apps/details?id=$pkg")) startActivity(ps); true } else false } }.getOrElse { false } } } val host = url.host?.lowercase() ?: "" val matched = socialRules.entries.firstOrNull { (h, _) -> host.contains(h) || (h == "arattai" && host.contains("arattai")) } if (matched != null) { val rule = matched.value val appUri = rule.buildUri(url.toString()) val appIntent = Intent(Intent.ACTION_VIEW, appUri).apply { setPackage(rule.packageName) } return if (isPackageInstalled(rule.packageName)) { runCatching { startActivity(appIntent); true }.getOrElse { false } } else { runCatching { startActivity(Intent(Intent.ACTION_VIEW, url)); true }.getOrElse { false } } } return false } // ========================= // PDF: DownloadManager + progress chip + open existing if present // ========================= private fun isPdfUrl(url: String?): Boolean { if (url.isNullOrBlank()) return false val u = url.lowercase() return u.endsWith(".pdf") || u.contains("application/pdf") } private fun guessFileName(url: String, contentDisposition: String? = null): String { contentDisposition?.let { val fn = Regex("filename\\*=UTF-8''([^;]+)|filename=\"?([^\";]+)\"?", RegexOption.IGNORE_CASE) .find(it)?.groups?.filterNotNull()?.lastOrNull()?.value if (!fn.isNullOrBlank()) return fn } val last = Uri.parse(url).lastPathSegment ?: "document.pdf" return if (last.endsWith(".pdf", true)) last else "$last.pdf" } private fun fileInDownloads(name: String): File { val base = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) return File(base, name) } private fun openExistingFileIfAny(name: String): Boolean { val f = fileInDownloads(name) if (!f.exists() || f.length() == 0L) return false val uri = buildFileUriSafe(f) ?: return false openInGoogleDrive(uri) return true } /** Try FileProvider first; fall back to Uri.fromFile if needed */ private fun buildFileUriSafe(file: File): Uri? { return try { FileProvider.getUriForFile( this, "$packageName.fileprovider", file ) } catch (_: Exception) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { Uri.fromFile(file) } else null } } @SuppressLint("QueryPermissionsNeeded") private fun openInGoogleDrive(uri: Uri) { try { val intent = Intent(Intent.ACTION_VIEW).apply { setDataAndType(uri, "application/pdf") addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) } startActivity(intent) } catch (e: Exception) { Toast.makeText(this, "No PDF viewer found", Toast.LENGTH_SHORT).show() // ✅ ‘view’ नहीं ‘this’ } // ✅ Save real name from Uri val fileName = getFileNameFromUri(uri) ?: "Unknown.pdf" val entry = DownloadEntry( id = System.currentTimeMillis(), fileName = fileName, mime = "application/pdf", bytes = 0L, finishedAt = System.currentTimeMillis(), localUri = uri.toString(), status = "opened", pageUrl = currentUrl(), pageTitle = wv.title ?: "Untitled" ) saveDownloadHistoryEntry(entry) } private fun getFileNameFromUri(uri: Uri): String? { return try { var name: String? = null if (uri.scheme == "content") { contentResolver.query(uri, null, null, null, null)?.use { cursor -> val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME) if (nameIndex >= 0 && cursor.moveToFirst()) { name = cursor.getString(nameIndex) } } } if (name == null) { val path = uri.path ?: return null name = path.substringAfterLast('/') } name } catch (e: Exception) { null } } private fun showDlChip() { dlChip.visibility = View.VISIBLE dlChip.isClickable = false dlChip.setOnClickListener(null) } private fun hideDlChip() { dlChip.visibility = View.GONE dlChip.isClickable = false dlChip.setOnClickListener(null) dlProgressCircle.setProgressCompat(0, false) dlPercent.text = "0%" lastDownloadedFileUri = null } // ---------- Canonical key (strip query/fragment) ---------- /** Remove volatile query/fragment so same file maps to one stable key */ private fun canonicalKey(rawUrl: String): String { return try { val u = Uri.parse(rawUrl) val scheme = (u.scheme ?: "https").lowercase() val host = (u.host ?: "").lowercase() val path = (u.path ?: "").removeSuffix("/") "$scheme://$host$path" } catch (_: Exception) { rawUrl } } // ---------- URL <-> FileName mapping for PDFs (by canonical key) ---------- private fun urlFilePrefsSet(): MutableSet { val prefs = getSharedPreferences("url_file_map_store", MODE_PRIVATE) return (prefs.getStringSet("list", mutableSetOf()) ?: mutableSetOf()).toMutableSet() } /** Save/replace mapping: canonicalKey(url) -> fileName */ private fun mapUrlToFile(url: String, fileName: String) { val key = canonicalKey(url) val set = urlFilePrefsSet() set.removeIf { it.startsWith("$key||||") } set.add("$key||||$fileName") getSharedPreferences("url_file_map_store", MODE_PRIVATE) .edit().putStringSet("list", set).apply() } /** Get mapped fileName (if any) for canonicalKey(url) */ private fun getFileNameForUrl(url: String): String? { val key = canonicalKey(url) val set = getSharedPreferences("url_file_map_store", MODE_PRIVATE) .getStringSet("list", emptySet()) ?: emptySet() val hit = set.firstOrNull { it.startsWith("$key||||") } ?: return null return hit.split("||||").getOrNull(1) } /** Try open by canonical mapping (if file exists in Downloads) */ private fun openMappedFileIfExists(url: String): Boolean { val name = getFileNameForUrl(url) ?: return false return openExistingFileIfAny(name) } private fun startPollingDownload(id: Long, srcUrl: String, fileName: String) { showDlChip() dlProgressCircle.setProgressCompat(0, false) dlPercent.text = "0%" fun poll() { val q = DownloadManager.Query().setFilterById(id) dm.query(q)?.use { c -> if (c.moveToFirst()) { val bytes = c.getLong(c.getColumnIndexOrThrow(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR)) val total = c.getLong(c.getColumnIndexOrThrow(DownloadManager.COLUMN_TOTAL_SIZE_BYTES)) val status = c.getInt(c.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS)) if (status == DownloadManager.STATUS_RUNNING && total > 0) { val p = ((bytes * 100f) / total).toInt().coerceIn(0, 100) dlProgressCircle.setProgressCompat(p, true) dlPercent.text = "$p%" } else if (status == DownloadManager.STATUS_PENDING) { dlPercent.text = "…" } when (status) { DownloadManager.STATUS_SUCCESSFUL -> { val fileUri = dm.getUriForDownloadedFile(id) val realName = try { c.getString(c.getColumnIndexOrThrow(DownloadManager.COLUMN_TITLE)) ?: fileName } catch (_: Exception) { fileName } if (fileUri != null) { lastDownloadedFileUri = fileUri val entry = DownloadEntry( id = System.currentTimeMillis(), fileName = realName, // ✅ असली नाम mime = "application/pdf", bytes = total, finishedAt = System.currentTimeMillis(), localUri = fileUri.toString(), status = "completed", pageUrl = currentUrl(), pageTitle = wv.title ?: "Untitled" ) saveDownloadHistoryEntry(entry) runCatching { mapUrlToFile(srcUrl, realName) } dlProgressCircle.setProgressCompat(100, true) dlPercent.text = "Open" dlChip.isClickable = true dlChip.setOnClickListener { openInGoogleDrive(fileUri) hideDlChip() } } else { hideDlChip() Toast.makeText(this@MainActivity, "File not found", Toast.LENGTH_SHORT).show() } return } DownloadManager.STATUS_FAILED -> { hideDlChip() Toast.makeText(this@MainActivity, "Download failed", Toast.LENGTH_SHORT).show() return } } } else { hideDlChip() return } } pollHandler.postDelayed({ poll() }, 500) } poll() } /** Start DM download; but अगर वही (canonical) फ़ाइल पहले से हो तो सीधे open कर दें */ private fun startPdfDownloadWithProgress(url: String, fileName: String? = null) { try { val mapped = getFileNameForUrl(url) // ✅ canonical-मैप्ड फ़ाइल पहले से मौजूद है? तो सीधे ओपन if (!mapped.isNullOrBlank() && openExistingFileIfAny(mapped)) { return } val name = fileName ?: guessFileName(url, null) // ✅ नाम से भी मौजूद है? तो ओपन + canonical mapping सुनिश्चित कर दें if (openExistingFileIfAny(name)) { mapUrlToFile(url, name) // अगली बार सीधे खुले return } // नहीं है तो download hideDlChip() lastDownloadedFileUri = null val req = DownloadManager.Request(Uri.parse(url)).apply { setMimeType("application/pdf") setTitle(name) setDescription("Downloading PDF") setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED) setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, name) addRequestHeader("User-Agent", wv.settings.userAgentString ?: "") CookieManager.getInstance().getCookie(url)?.let { addRequestHeader("Cookie", it) } setAllowedOverMetered(true) setAllowedOverRoaming(true) } currentDownloadId = dm.enqueue(req) startPollingDownload(currentDownloadId, url, name) } catch (_: Exception) { hideDlChip() Toast.makeText(this, "Download start failed", Toast.LENGTH_SHORT).show() } } /** Download history entry (optional store) */ /** Download history entry (optional store) */ data class DownloadEntry( val id: Long, val fileName: String, val mime: String, val bytes: Long, val finishedAt: Long, val localUri: String?, val status: String, val pageUrl: String? = null, val pageTitle: String? = null // ✅ यह ज़रूर होना चाहिए ) private fun dlPrefsSet(): MutableSet { val prefs = getSharedPreferences("download_history_store", MODE_PRIVATE) return (prefs.getStringSet("list", mutableSetOf()) ?: mutableSetOf()).toMutableSet() } private fun encodeDownloadEntry(d: DownloadEntry): String { return listOf( d.id.toString(), d.fileName.replace("|||", " ").replace("||||", " "), d.mime, d.bytes.toString(), d.finishedAt.toString(), d.localUri ?: "", d.status, d.pageUrl ?: "" // ✅ नया field encode करें ).joinToString("||||") } private fun decodeDownloadEntry(s: String): DownloadEntry? { val p = s.split("||||") if (p.size < 7) return null return DownloadEntry( id = p[0].toLongOrNull() ?: -1L, fileName = p[1], mime = p[2], bytes = p[3].toLongOrNull() ?: 0L, finishedAt = p[4].toLongOrNull() ?: 0L, localUri = p[5].ifBlank { null }, status = p[6], pageUrl = p.getOrNull(7) // ✅ decode pageUrl safely ) } private fun saveDownloadHistoryEntry(entry: DownloadEntry) { val set = dlPrefsSet() set.removeIf { val ex = decodeDownloadEntry(it) ex != null && ( (entry.id > 0 && ex.id == entry.id) || (entry.id <= 0 && ex.fileName == entry.fileName && ex.bytes == entry.bytes) ) } set.add(encodeDownloadEntry(entry)) getSharedPreferences("download_history_store", MODE_PRIVATE) .edit().putStringSet("list", set).apply() } private fun getDownloadHistory(): List { val set = getSharedPreferences("download_history_store", MODE_PRIVATE) .getStringSet("list", emptySet()) ?: emptySet() return set.mapNotNull { decodeDownloadEntry(it) } .sortedByDescending { it.finishedAt } } private fun removeDownloadHistoryById(id: Long) { val set = dlPrefsSet() set.removeIf { val ex = decodeDownloadEntry(it) ex != null && ex.id == id } getSharedPreferences("download_history_store", MODE_PRIVATE) .edit().putStringSet("list", set).apply() } } --------------------------------------download.html-------------------------- 📂 Download History
📂 Download History
Loading…
Next Post Previous Post