Initial release: Claude Pro usage widget for Android
This commit is contained in:
@@ -0,0 +1,172 @@
|
||||
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 kotlinx.coroutines.CoroutineScope
|
||||
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 = true
|
||||
javaScriptCanOpenWindowsAutomatically = true
|
||||
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)
|
||||
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user