6934017519
- 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>
173 lines
7.0 KiB
Kotlin
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()
|
|
}
|
|
}
|