fuzzyworld

ai-chat-android-auto / commit

Add phone setup screen for API key and microphone permission

80eca30b169470ffb1a89e421d566993e9d137a2 · Pavel Safronov · 2026-05-14 16:19:18 -0700 · parents eaac7c1efbcf

Add phone setup screen for API key and microphone permission

SetupActivity lets the user paste their Anthropic API key, which is
stored in EncryptedSharedPreferences (never in source or BuildConfig).
The screen also requests RECORD_AUDIO at launch and shows a status
summary. The key entry group and clear button are mutually exclusive —
only one is visible depending on whether a key is already saved.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
 app/src/main/AndroidManifest.xml                   | 26 +++++++
 .../main/java/net/fuzzyworld/claudeauto/Prefs.kt   | 27 ++++++++
 .../net/fuzzyworld/claudeauto/SetupActivity.kt     | 80 ++++++++++++++++++++++
 app/src/main/res/layout/activity_setup.xml         | 64 +++++++++++++++++
 app/src/main/res/values/colors.xml                 |  4 ++
 app/src/main/res/values/strings.xml                |  4 ++
 app/src/main/res/values/themes.xml                 |  4 ++
 7 files changed, 209 insertions(+)

diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..4a71f4f
--- /dev/null
+++ b/app/src/main/AndroidManifest.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <uses-permission android:name="android.permission.RECORD_AUDIO" />
+
+    <application
+        android:allowBackup="true"
+        android:icon="@mipmap/ic_launcher"
+        android:roundIcon="@mipmap/ic_launcher_round"
+        android:label="@string/app_name"
+        android:supportsRtl="true">
+
+        <activity
+            android:name=".SetupActivity"
+            android:exported="true"
+            android:label="@string/app_name"
+            android:theme="@style/Theme.ClaudeAuto">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
+
+    </application>
+
+</manifest>
diff --git a/app/src/main/java/net/fuzzyworld/claudeauto/Prefs.kt b/app/src/main/java/net/fuzzyworld/claudeauto/Prefs.kt
new file mode 100644
index 0000000..ab34d61
--- /dev/null
+++ b/app/src/main/java/net/fuzzyworld/claudeauto/Prefs.kt
@@ -0,0 +1,27 @@
+package net.fuzzyworld.claudeauto
+
+import android.content.Context
+import android.content.SharedPreferences
+import androidx.security.crypto.EncryptedSharedPreferences
+import androidx.security.crypto.MasterKey
+
+object Prefs {
+    private const val FILE = "claude_prefs"
+    private const val KEY = "api_key"
+
+    private fun prefs(context: Context): SharedPreferences {
+        val master = MasterKey.Builder(context)
+            .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
+            .build()
+        return EncryptedSharedPreferences.create(
+            context, FILE, master,
+            EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
+            EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
+        )
+    }
+
+    fun apiKey(context: Context): String = prefs(context).getString(KEY, "") ?: ""
+    fun hasApiKey(context: Context): Boolean = apiKey(context).isNotBlank()
+    fun saveApiKey(context: Context, key: String) = prefs(context).edit().putString(KEY, key).apply()
+    fun clearApiKey(context: Context) = prefs(context).edit().remove(KEY).apply()
+}
diff --git a/app/src/main/java/net/fuzzyworld/claudeauto/SetupActivity.kt b/app/src/main/java/net/fuzzyworld/claudeauto/SetupActivity.kt
new file mode 100644
index 0000000..145109d
--- /dev/null
+++ b/app/src/main/java/net/fuzzyworld/claudeauto/SetupActivity.kt
@@ -0,0 +1,80 @@
+package net.fuzzyworld.claudeauto
+
+import android.Manifest
+import android.content.pm.PackageManager
+import android.os.Bundle
+import android.view.View
+import android.widget.Button
+import android.widget.EditText
+import android.widget.LinearLayout
+import android.widget.TextView
+import android.widget.Toast
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.content.ContextCompat
+
+class SetupActivity : AppCompatActivity() {
+
+    private lateinit var enterKeyGroup: LinearLayout
+    private lateinit var keyInput: EditText
+    private lateinit var clearBtn: Button
+    private lateinit var statusView: TextView
+
+    private val requestMic = registerForActivityResult(
+        ActivityResultContracts.RequestPermission()
+    ) { _ -> refreshUI() }
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        setContentView(R.layout.activity_setup)
+
+        enterKeyGroup = findViewById(R.id.enterKeyGroup)
+        keyInput = findViewById(R.id.keyInput)
+        clearBtn = findViewById(R.id.clearBtn)
+        statusView = findViewById(R.id.status)
+
+        findViewById<Button>(R.id.saveBtn).setOnClickListener {
+            val key = keyInput.text.toString().trim()
+            if (key.isBlank()) {
+                Toast.makeText(this, "Paste your API key first.", Toast.LENGTH_SHORT).show()
+                return@setOnClickListener
+            }
+            Prefs.saveApiKey(this, key)
+            keyInput.setText("")
+            Toast.makeText(this, "API key saved.", Toast.LENGTH_SHORT).show()
+            refreshUI()
+        }
+
+        clearBtn.setOnClickListener {
+            Prefs.clearApiKey(this)
+            Toast.makeText(this, "API key cleared.", Toast.LENGTH_SHORT).show()
+            refreshUI()
+        }
+
+        if (!hasMic()) requestMic.launch(Manifest.permission.RECORD_AUDIO)
+        refreshUI()
+    }
+
+    private fun refreshUI() {
+        val hasKey = Prefs.hasApiKey(this)
+        enterKeyGroup.visibility = if (hasKey) View.GONE else View.VISIBLE
+        clearBtn.visibility = if (hasKey) View.VISIBLE else View.GONE
+
+        val micOk = hasMic()
+        statusView.text = buildString {
+            appendLine(if (hasKey) "✅ API key saved" else "❌ No API key — paste one above and tap Save")
+            appendLine(if (micOk) "✅ Microphone permission granted" else "❌ Microphone permission denied")
+            appendLine()
+            appendLine(
+                if (hasKey && micOk)
+                    "Ready. Connect Android Auto and open Claude Auto from the car launcher."
+                else
+                    "Fix the issues above before using the car app."
+            )
+        }
+    }
+
+    private fun hasMic() = ContextCompat.checkSelfPermission(
+        this, Manifest.permission.RECORD_AUDIO
+    ) == PackageManager.PERMISSION_GRANTED
+}
diff --git a/app/src/main/res/layout/activity_setup.xml b/app/src/main/res/layout/activity_setup.xml
new file mode 100644
index 0000000..26c804d
--- /dev/null
+++ b/app/src/main/res/layout/activity_setup.xml
@@ -0,0 +1,64 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical"
+    android:padding="32dp"
+    android:gravity="top">
+
+    <TextView
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:text="Claude Auto"
+        android:textSize="26sp"
+        android:textStyle="bold"
+        android:layout_marginBottom="8dp" />
+
+    <!-- Shown only when no key is saved -->
+    <LinearLayout
+        android:id="@+id/enterKeyGroup"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="vertical">
+
+        <TextView
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:text="Paste your API key from console.anthropic.com"
+            android:textSize="14sp"
+            android:layout_marginBottom="20dp" />
+
+        <EditText
+            android:id="@+id/keyInput"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:hint="sk-ant-..."
+            android:inputType="textPassword"
+            android:fontFamily="monospace"
+            android:layout_marginBottom="12dp" />
+
+        <Button
+            android:id="@+id/saveBtn"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:text="Save API Key"
+            android:layout_marginBottom="24dp" />
+
+    </LinearLayout>
+
+    <!-- Shown only when a key is already saved -->
+    <Button
+        android:id="@+id/clearBtn"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:text="Clear Saved Key"
+        android:layout_marginBottom="24dp" />
+
+    <TextView
+        android:id="@+id/status"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:textSize="14sp"
+        android:lineSpacingMultiplier="1.4" />
+
+</LinearLayout>
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
new file mode 100644
index 0000000..f42ada6
--- /dev/null
+++ b/app/src/main/res/values/colors.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <color name="ic_launcher_background">#FFFFFF</color>
+</resources>
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
new file mode 100644
index 0000000..dfdf895
--- /dev/null
+++ b/app/src/main/res/values/strings.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string name="app_name">Claude Auto</string>
+</resources>
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
new file mode 100644
index 0000000..9bac402
--- /dev/null
+++ b/app/src/main/res/values/themes.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <style name="Theme.ClaudeAuto" parent="Theme.AppCompat.Light.NoActionBar" />
+</resources>