Files
claude-usage-widget/app/src/main/java/me/khodak/claudeusage/LoginActivity.kt
T
amir 6934017519 security: restrict network to system CAs, tighten WebView capabilities; v1.9
- AndroidManifest: add networkSecurityConfig to explicitly trust only system
  CAs, preventing user-installed CA cert MITM attacks on claude.ai sessions
- LoginActivity: set javaScriptCanOpenWindowsAutomatically=false (not needed
  for claude.ai login) and databaseEnabled=false (deprecated WebSQL)
- build.gradle.kts: enable buildConfig generation (required for
  BuildConfig.DEBUG guards already used in UsageRepository)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 20:00:01 +00:00

173 lines
7.0 KiB
Kotlin

package me.khodak.claudeusage
import android.annotation.SuppressLint
import android.appwidget.AppWidgetManager
import android.content.ComponentName
import android.os.Bundle
import android.view.View
import android.webkit.*
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import me.khodak.claudeusage.data.PreferencesManager
import me.khodak.claudeusage.databinding.ActivityLoginBinding
class LoginActivity : AppCompatActivity() {
private lateinit var binding: ActivityLoginBinding
private lateinit var prefs: PreferencesManager
private var loginHandled = false
// JS injected before page scripts run — hides WebView fingerprints
private val antiDetectionJs = """
(function() {
Object.defineProperty(navigator, 'webdriver', { get: () => undefined });
window.chrome = { runtime: {} };
Object.defineProperty(navigator, 'plugins', { get: () => [1,2,3] });
Object.defineProperty(navigator, 'languages', { get: () => ['en-US','en'] });
})();
""".trimIndent()
@SuppressLint("SetJavaScriptEnabled")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityLoginBinding.inflate(layoutInflater)
setContentView(binding.root)
prefs = PreferencesManager(this)
setupTabs()
setupWebView()
setupCookiePanel()
}
private fun setupTabs() {
binding.tabBrowser.setOnClickListener {
binding.panelBrowser.visibility = View.VISIBLE
binding.panelCookie.visibility = View.GONE
binding.tabBrowser.backgroundTintList = android.content.res.ColorStateList.valueOf(0xFFCC785C.toInt())
binding.tabBrowser.setTextColor(0xFFFFFFFF.toInt())
binding.tabCookie.backgroundTintList = android.content.res.ColorStateList.valueOf(0xFF2A2A2A.toInt())
binding.tabCookie.setTextColor(0xFF888888.toInt())
}
binding.tabCookie.setOnClickListener {
binding.panelBrowser.visibility = View.GONE
binding.panelCookie.visibility = View.VISIBLE
binding.tabCookie.backgroundTintList = android.content.res.ColorStateList.valueOf(0xFFCC785C.toInt())
binding.tabCookie.setTextColor(0xFFFFFFFF.toInt())
binding.tabBrowser.backgroundTintList = android.content.res.ColorStateList.valueOf(0xFF2A2A2A.toInt())
binding.tabBrowser.setTextColor(0xFF888888.toInt())
}
}
@SuppressLint("SetJavaScriptEnabled")
private fun setupWebView() {
CookieManager.getInstance().removeAllCookies(null)
CookieManager.getInstance().flush()
binding.btnDone.setOnClickListener { attemptCookieCapture(force = true) }
with(binding.webView) {
settings.apply {
javaScriptEnabled = true
domStorageEnabled = true
databaseEnabled = false
javaScriptCanOpenWindowsAutomatically = false
setSupportMultipleWindows(false)
// Standard Android Chrome UA — less suspicious than desktop
userAgentString = "Mozilla/5.0 (Linux; Android 13; Pixel 7) " +
"AppleWebKit/537.36 (KHTML, like Gecko) " +
"Chrome/120.0.6099.230 Mobile Safari/537.36"
}
CookieManager.getInstance().setAcceptCookie(true)
CookieManager.getInstance().setAcceptThirdPartyCookies(this, true)
webViewClient = object : WebViewClient() {
override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean {
val url = request.url.toString()
if (url.startsWith("market://") || url.startsWith("intent://")) return true
return false
}
override fun onPageStarted(view: WebView, url: String, favicon: android.graphics.Bitmap?) {
// Inject before page scripts
view.evaluateJavascript(antiDetectionJs, null)
}
override fun onPageFinished(view: WebView, url: String) {
// Re-inject after page load
view.evaluateJavascript(antiDetectionJs, null)
if (url.contains("claude.ai")) {
binding.btnDone.visibility = View.VISIBLE
}
if (loginHandled) return
val onMain = url.startsWith("https://claude.ai") &&
!url.contains("/login") && !url.contains("/auth") &&
!url.contains("/sign") && !url.contains("/verify")
if (onMain) attemptCookieCapture(force = false)
}
}
loadUrl("https://claude.ai/login")
}
}
private fun setupCookiePanel() {
binding.btnSaveCookie.setOnClickListener {
val cookie = binding.etCookie.text.toString().trim()
if (cookie.length < 10) {
Toast.makeText(this, "Cookie too short — paste the full string", Toast.LENGTH_SHORT).show()
return@setOnClickListener
}
handleLoginSuccess(cookie)
}
}
private fun attemptCookieCapture(force: Boolean) {
if (loginHandled) return
CookieManager.getInstance().flush()
val cookies = CookieManager.getInstance().getCookie("https://claude.ai") ?: ""
if (cookies.length > 10) {
loginHandled = true
handleLoginSuccess(cookies)
} else if (force) {
Toast.makeText(this, "No session found — finish logging in first, or use the Paste Cookie tab", Toast.LENGTH_LONG).show()
}
}
private fun handleLoginSuccess(cookies: String) {
prefs.saveCookies(cookies)
prefs.saveSessionStart(System.currentTimeMillis())
prefs.markTodayActive()
Toast.makeText(this, "Signed in — loading usage…", Toast.LENGTH_SHORT).show()
UsageUpdateWorker.triggerImmediateRefresh(this)
UsageUpdateWorker.schedulePeriodicRefresh(this)
lifecycleScope.launch(Dispatchers.IO) {
try {
val data = UsageRepository(prefs).fetchUsage()
prefs.saveUsageData(data)
val mgr = AppWidgetManager.getInstance(this@LoginActivity)
val ids = mgr.getAppWidgetIds(ComponentName(this@LoginActivity, ClaudeUsageWidget::class.java))
ids.forEach { id -> ClaudeUsageWidget.updateWidget(this@LoginActivity, mgr, id, data) }
} catch (_: Exception) {}
}
setResult(RESULT_OK)
finish()
}
override fun onBackPressed() {
if (binding.panelBrowser.visibility == View.VISIBLE && binding.webView.canGoBack())
binding.webView.goBack()
else super.onBackPressed()
}
override fun onDestroy() {
binding.webView.destroy()
super.onDestroy()
}
}