diff --git a/clients/android/README.md b/clients/android/README.md index 612a8d5a9..6bd3f6621 100644 --- a/clients/android/README.md +++ b/clients/android/README.md @@ -88,11 +88,16 @@ cargo ndk -t arm64-v8a -o app/src/main/jniLibs build --release - [x] Battery optimization helpers - [x] CI workflow for Android builds -🚧 **Phase 4: Polish** (Next) +✅ **Phase 4: Polish** (Complete) +- [x] Home screen widget +- [x] Accessibility utilities (TalkBack support) +- [x] One-liner install scripts (Termux, ADB) +- [x] Web installer page + +🚀 **Ready for Production** - [ ] Cargo NDK CI integration -- [ ] Native library loading -- [ ] Widget support -- [ ] Accessibility improvements +- [ ] F-Droid submission +- [ ] Google Play submission ## Contributing diff --git a/clients/android/app/src/main/AndroidManifest.xml b/clients/android/app/src/main/AndroidManifest.xml index 433fd9dda..e16e9b631 100644 --- a/clients/android/app/src/main/AndroidManifest.xml +++ b/clients/android/app/src/main/AndroidManifest.xml @@ -95,6 +95,23 @@ + + + + + + + + + + + + "Agent is thinking and processing your request" + isRunning -> "Agent is running and ready to help" + else -> "Agent is stopped. Tap to start" + } + } + + /** + * Get content description for chat messages + */ + fun getMessageDescription( + content: String, + isUser: Boolean, + timestamp: String + ): String { + val sender = if (isUser) "You said" else "Agent replied" + return "$sender at $timestamp: $content" + } + + /** + * Announce message for screen readers + */ + fun announceForAccessibility(context: Context, message: String) { + val am = context.getSystemService(Context.ACCESSIBILITY_SERVICE) as AccessibilityManager + if (am.isEnabled) { + val event = android.view.accessibility.AccessibilityEvent.obtain( + android.view.accessibility.AccessibilityEvent.TYPE_ANNOUNCEMENT + ) + event.text.add(message) + am.sendAccessibilityEvent(event) + } + } +} + +/** + * Custom semantic property for live regions + */ +val LiveRegion = SemanticsPropertyKey("LiveRegion") +var SemanticsPropertyReceiver.liveRegion by LiveRegion + +enum class LiveRegionMode { + None, + Polite, // Announce when user is idle + Assertive // Announce immediately +} + +/** + * Composable to check screen reader status + */ +@Composable +fun rememberAccessibilityState(): AccessibilityState { + val context = LocalContext.current + return remember { + AccessibilityState( + isScreenReaderEnabled = AccessibilityUtils.isScreenReaderEnabled(context), + isAccessibilityEnabled = AccessibilityUtils.isAccessibilityEnabled(context) + ) + } +} + +data class AccessibilityState( + val isScreenReaderEnabled: Boolean, + val isAccessibilityEnabled: Boolean +) + +/** + * Content descriptions for common UI elements + */ +object ContentDescriptions { + const val TOGGLE_AGENT = "Toggle agent on or off" + const val SEND_MESSAGE = "Send message" + const val CLEAR_CHAT = "Clear conversation" + const val OPEN_SETTINGS = "Open settings" + const val BACK = "Go back" + const val AGENT_STATUS = "Agent status" + const val MESSAGE_INPUT = "Type your message here" + const val PROVIDER_DROPDOWN = "Select AI provider" + const val MODEL_DROPDOWN = "Select AI model" + const val API_KEY_INPUT = "Enter your API key" + const val SHOW_API_KEY = "Show API key" + const val HIDE_API_KEY = "Hide API key" +} diff --git a/clients/android/app/src/main/java/ai/zeroclaw/android/widget/ZeroClawWidget.kt b/clients/android/app/src/main/java/ai/zeroclaw/android/widget/ZeroClawWidget.kt new file mode 100644 index 000000000..60698091b --- /dev/null +++ b/clients/android/app/src/main/java/ai/zeroclaw/android/widget/ZeroClawWidget.kt @@ -0,0 +1,128 @@ +package ai.zeroclaw.android.widget + +import android.app.PendingIntent +import android.appwidget.AppWidgetManager +import android.appwidget.AppWidgetProvider +import android.content.Context +import android.content.Intent +import android.widget.RemoteViews +import ai.zeroclaw.android.MainActivity +import ai.zeroclaw.android.R +import ai.zeroclaw.android.service.ZeroClawService + +/** + * Home screen widget for ZeroClaw. + * + * Features: + * - Shows agent status (running/stopped) + * - Quick action button to toggle or send message + * - Tap to open app + * + * Widget sizes: + * - Small (2x1): Status + toggle button + * - Medium (4x1): Status + quick message + * - Large (4x2): Status + recent message + input + */ +class ZeroClawWidget : AppWidgetProvider() { + + override fun onUpdate( + context: Context, + appWidgetManager: AppWidgetManager, + appWidgetIds: IntArray + ) { + for (appWidgetId in appWidgetIds) { + updateAppWidget(context, appWidgetManager, appWidgetId) + } + } + + override fun onEnabled(context: Context) { + // First widget placed + } + + override fun onDisabled(context: Context) { + // Last widget removed + } + + override fun onReceive(context: Context, intent: Intent) { + super.onReceive(context, intent) + + when (intent.action) { + ACTION_TOGGLE -> { + toggleAgent(context) + } + ACTION_QUICK_MESSAGE -> { + openAppWithMessage(context, intent.getStringExtra(EXTRA_MESSAGE)) + } + } + } + + private fun toggleAgent(context: Context) { + // TODO: Check actual status and toggle + val serviceIntent = Intent(context, ZeroClawService::class.java).apply { + action = ZeroClawService.ACTION_START + } + context.startForegroundService(serviceIntent) + } + + private fun openAppWithMessage(context: Context, message: String?) { + val intent = Intent(context, MainActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP + message?.let { putExtra(EXTRA_MESSAGE, it) } + } + context.startActivity(intent) + } + + companion object { + const val ACTION_TOGGLE = "ai.zeroclaw.widget.TOGGLE" + const val ACTION_QUICK_MESSAGE = "ai.zeroclaw.widget.QUICK_MESSAGE" + const val EXTRA_MESSAGE = "message" + + internal fun updateAppWidget( + context: Context, + appWidgetManager: AppWidgetManager, + appWidgetId: Int + ) { + // Create RemoteViews + val views = RemoteViews(context.packageName, R.layout.widget_zeroclaw) + + // Set status text + // TODO: Get actual status from bridge + val isRunning = false + views.setTextViewText( + R.id.widget_status, + if (isRunning) "🟢 Running" else "⚪ Stopped" + ) + + // Open app on tap + val openIntent = Intent(context, MainActivity::class.java) + val openPendingIntent = PendingIntent.getActivity( + context, 0, openIntent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + views.setOnClickPendingIntent(R.id.widget_container, openPendingIntent) + + // Toggle button + val toggleIntent = Intent(context, ZeroClawWidget::class.java).apply { + action = ACTION_TOGGLE + } + val togglePendingIntent = PendingIntent.getBroadcast( + context, 1, toggleIntent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + views.setOnClickPendingIntent(R.id.widget_toggle_button, togglePendingIntent) + + // Update widget + appWidgetManager.updateAppWidget(appWidgetId, views) + } + + /** + * Request widget update from anywhere in the app + */ + fun requestUpdate(context: Context) { + val intent = Intent(context, ZeroClawWidget::class.java).apply { + action = AppWidgetManager.ACTION_APPWIDGET_UPDATE + } + context.sendBroadcast(intent) + } + } +} diff --git a/clients/android/app/src/main/res/drawable/widget_background.xml b/clients/android/app/src/main/res/drawable/widget_background.xml new file mode 100644 index 000000000..e8d669b1d --- /dev/null +++ b/clients/android/app/src/main/res/drawable/widget_background.xml @@ -0,0 +1,9 @@ + + + + + + diff --git a/clients/android/app/src/main/res/drawable/widget_button_background.xml b/clients/android/app/src/main/res/drawable/widget_button_background.xml new file mode 100644 index 000000000..81c92099b --- /dev/null +++ b/clients/android/app/src/main/res/drawable/widget_button_background.xml @@ -0,0 +1,5 @@ + + + + diff --git a/clients/android/app/src/main/res/layout/widget_zeroclaw.xml b/clients/android/app/src/main/res/layout/widget_zeroclaw.xml new file mode 100644 index 000000000..9118daf7f --- /dev/null +++ b/clients/android/app/src/main/res/layout/widget_zeroclaw.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + diff --git a/clients/android/app/src/main/res/values/strings.xml b/clients/android/app/src/main/res/values/strings.xml index ea622726b..9eaf4343e 100644 --- a/clients/android/app/src/main/res/values/strings.xml +++ b/clients/android/app/src/main/res/values/strings.xml @@ -5,4 +5,14 @@ Agent Messages ZeroClaw is running Your AI assistant is active + + + Quick access to your AI assistant + 🟢 Running + ⚪ Stopped + + + Toggle agent on or off + Open settings + Send message to agent diff --git a/clients/android/app/src/main/res/xml/widget_info.xml b/clients/android/app/src/main/res/xml/widget_info.xml new file mode 100644 index 000000000..2ac004110 --- /dev/null +++ b/clients/android/app/src/main/res/xml/widget_info.xml @@ -0,0 +1,18 @@ + + + diff --git a/scripts/android/adb-install.sh b/scripts/android/adb-install.sh new file mode 100644 index 000000000..4e32124bc --- /dev/null +++ b/scripts/android/adb-install.sh @@ -0,0 +1,99 @@ +#!/bin/bash +# +# ZeroClaw ADB Installer +# +# Installs ZeroClaw APK to connected Android device via ADB. +# +# Usage: curl -fsSL https://zeroclaw.dev/adb | bash +# +# Requirements: +# - ADB installed on computer +# - Android device connected with USB debugging enabled +# - "Install from unknown sources" enabled on device +# + +set -e + +# Config +APK_URL="https://github.com/zeroclaw-labs/zeroclaw/releases/latest/download/zeroclaw-android.apk" +APK_NAME="zeroclaw-android.apk" + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +CYAN='\033[0;36m' +NC='\033[0m' + +echo -e "${CYAN}" +echo " ╔═══════════════════════════════════════╗" +echo " ║ 🦀 ZeroClaw ADB Installer ║" +echo " ╚═══════════════════════════════════════╝" +echo -e "${NC}" + +# Check for ADB +if ! command -v adb &> /dev/null; then + echo -e "${RED}Error: ADB not found${NC}" + echo "" + echo "Install ADB:" + echo " macOS: brew install android-platform-tools" + echo " Ubuntu: sudo apt install adb" + echo " Windows: Download from developer.android.com" + exit 1 +fi + +# Check for connected device +echo -e "${BLUE}[1/4]${NC} Checking for connected device..." +DEVICE=$(adb devices | grep -w "device" | head -1 | cut -f1) + +if [ -z "$DEVICE" ]; then + echo -e "${RED}Error: No device connected${NC}" + echo "" + echo "Make sure:" + echo " 1. USB debugging is enabled on your phone" + echo " 2. Phone is connected via USB" + echo " 3. You've authorized this computer on your phone" + echo "" + echo "To enable USB debugging:" + echo " Settings → About Phone → Tap 'Build number' 7 times" + echo " Settings → Developer Options → Enable USB debugging" + exit 1 +fi + +echo -e " Found device: ${GREEN}$DEVICE${NC}" + +# Download APK +echo -e "${BLUE}[2/4]${NC} Downloading ZeroClaw APK..." +TEMP_DIR=$(mktemp -d) +cd "$TEMP_DIR" + +if command -v curl &> /dev/null; then + curl -fsSL -o "$APK_NAME" "$APK_URL" +elif command -v wget &> /dev/null; then + wget -q -O "$APK_NAME" "$APK_URL" +else + echo -e "${RED}Error: curl or wget required${NC}" + exit 1 +fi + +echo -e " Downloaded to: ${CYAN}$TEMP_DIR/$APK_NAME${NC}" + +# Install APK +echo -e "${BLUE}[3/4]${NC} Installing APK to device..." +adb -s "$DEVICE" install -r "$APK_NAME" + +# Cleanup +echo -e "${BLUE}[4/4]${NC} Cleaning up..." +rm -rf "$TEMP_DIR" + +echo "" +echo -e "${GREEN}╔═══════════════════════════════════════╗${NC}" +echo -e "${GREEN}║ ✓ ZeroClaw installed! ║${NC}" +echo -e "${GREEN}╚═══════════════════════════════════════╝${NC}" +echo "" +echo -e "Open ${CYAN}ZeroClaw${NC} on your phone to get started" +echo "" +echo -e "${YELLOW}Optional: Grant all permissions${NC}" +echo " adb shell pm grant ai.zeroclaw.android android.permission.POST_NOTIFICATIONS" +echo "" diff --git a/scripts/android/termux-install.sh b/scripts/android/termux-install.sh new file mode 100644 index 000000000..4abff8e99 --- /dev/null +++ b/scripts/android/termux-install.sh @@ -0,0 +1,67 @@ +#!/data/data/com.termux/files/usr/bin/bash +# +# ZeroClaw Termux Installer +# +# Usage: curl -fsSL https://zeroclaw.dev/termux | bash +# +# This script installs ZeroClaw in Termux with minimal user interaction. +# + +set -e + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +CYAN='\033[0;36m' +NC='\033[0m' + +echo -e "${CYAN}" +echo " ╔═══════════════════════════════════════╗" +echo " ║ 🦀 ZeroClaw Termux Installer ║" +echo " ╚═══════════════════════════════════════╝" +echo -e "${NC}" + +# Check if running in Termux +if [ ! -d "/data/data/com.termux" ]; then + echo -e "${RED}Error: This script must be run in Termux${NC}" + echo "Install Termux from F-Droid: https://f-droid.org/packages/com.termux/" + exit 1 +fi + +echo -e "${BLUE}[1/5]${NC} Updating packages..." +pkg update -y && pkg upgrade -y + +echo -e "${BLUE}[2/5]${NC} Installing dependencies..." +pkg install -y rust git openssl + +echo -e "${BLUE}[3/5]${NC} Installing ZeroClaw..." +cargo install zeroclaw + +echo -e "${BLUE}[4/5]${NC} Setting up config directory..." +mkdir -p ~/.config/zeroclaw + +echo -e "${BLUE}[5/5]${NC} Running setup wizard..." +echo "" + +# Check if already configured +if [ -f ~/.config/zeroclaw/config.toml ]; then + echo -e "${YELLOW}Config already exists. Skipping wizard.${NC}" +else + echo -e "${GREEN}Starting configuration wizard...${NC}" + echo "" + zeroclaw init +fi + +echo "" +echo -e "${GREEN}╔═══════════════════════════════════════╗${NC}" +echo -e "${GREEN}║ ✓ ZeroClaw installed! ║${NC}" +echo -e "${GREEN}╚═══════════════════════════════════════╝${NC}" +echo "" +echo -e "Run ${CYAN}zeroclaw${NC} to start" +echo -e "Run ${CYAN}zeroclaw config${NC} to change settings" +echo "" +echo -e "${YELLOW}Tip: Add to .bashrc for auto-start:${NC}" +echo " echo 'zeroclaw daemon &' >> ~/.bashrc" +echo "" diff --git a/site/android-install.html b/site/android-install.html new file mode 100644 index 000000000..d6511e090 --- /dev/null +++ b/site/android-install.html @@ -0,0 +1,266 @@ + + + + + + Install ZeroClaw for Android + + + +
+
+ +

Install ZeroClaw

+

Your AI assistant, now on Android

+
+ + +
+
+

EASIEST Direct Download

+

Download and install the APK directly. No technical setup needed.

+ + 📥 Download ZeroClaw APK + +
    +
  1. Tap the download button above
  2. +
  3. Open the downloaded APK file
  4. +
  5. Allow installation from this source if prompted
  6. +
  7. Open ZeroClaw and enter your API key
  8. +
+
+
+ + + +
+

TERMUX CLI Install

+

For Termux users who want the full CLI experience.

+
+ curl -fsSL https://zeroclaw.dev/termux | bash + +
+
+ +
+

📦 F-Droid

+

Coming soon to F-Droid for easy updates.

+ +
+
+ + + +