Merge pull request #159 from cogwheel0/android-assistant-feature-implementation

android-assistant-feature-implementation
This commit is contained in:
cogwheel
2025-11-21 21:14:26 +05:30
committed by GitHub
20 changed files with 694 additions and 8 deletions

View File

@@ -32,6 +32,18 @@
android:allowBackup="false" android:allowBackup="false"
android:fullBackupContent="false" android:fullBackupContent="false"
android:usesCleartextTraffic="true"> android:usesCleartextTraffic="true">
<service
android:name=".ConduitVoiceInteractionSessionService"
android:permission="android.permission.BIND_VOICE_INTERACTION"
android:exported="true">
<meta-data
android:name="android.voice_interaction"
android:resource="@xml/voice_interaction_service" />
<intent-filter>
<action android:name="android.service.voice.VoiceInteractionService" />
</intent-filter>
</service>
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true" android:exported="true"

View File

@@ -0,0 +1,195 @@
package app.cogwheel.conduit
import android.content.Context
import android.content.Intent
import android.service.voice.VoiceInteractionSession
import android.os.Bundle
import android.app.assist.AssistStructure
import android.app.assist.AssistContent
import android.graphics.Bitmap
class ConduitVoiceInteractionSession(context: Context) : VoiceInteractionSession(context) {
private var capturedContext: String? = null
private var capturedScreenshot: Bitmap? = null
override fun onCreateContentView(): android.view.View {
val view = layoutInflater.inflate(app.cogwheel.conduit.R.layout.assistant_overlay, null)
// Summarize page button - sends screen context
val summarizeButton = view.findViewById<android.view.View>(app.cogwheel.conduit.R.id.btn_summarize)
summarizeButton?.setOnClickListener {
launchAppWithContext(includeScreenshot = false)
}
// Ask about page button - sends screenshot
val askAboutButton = view.findViewById<android.view.View>(app.cogwheel.conduit.R.id.btn_ask_about)
askAboutButton?.setOnClickListener {
launchAppWithScreenshot()
}
// Input area (opens text input)
val inputArea = view.findViewById<android.view.View>(app.cogwheel.conduit.R.id.input_area)
inputArea?.setOnClickListener {
launchApp()
}
// Voice button - opens voice call directly
val voiceButton = view.findViewById<android.view.View>(app.cogwheel.conduit.R.id.btn_voice)
voiceButton?.setOnClickListener {
launchAppForVoiceCall()
}
return view
}
override fun onHandleAssist(
data: Bundle?,
structure: AssistStructure?,
content: AssistContent?
) {
super.onHandleAssist(data, structure, content)
android.util.Log.d("ConduitVoiceSession", "onHandleAssist called")
// Capture screen context
val screenContext = StringBuilder()
structure?.let {
val nodes = it.windowNodeCount
for (i in 0 until nodes) {
val windowNode = it.getWindowNodeAt(i)
traverseNode(windowNode.rootViewNode, screenContext)
}
}
capturedContext = screenContext.toString()
// Capture screenshot from assist data
data?.let {
try {
capturedScreenshot = it.getParcelable("screenshot")
if (capturedScreenshot == null) {
// Try alternative key
capturedScreenshot = it.getParcelable("android.intent.extra.ASSIST_SCREENSHOT")
}
android.util.Log.d("ConduitVoiceSession", "Screenshot captured: ${capturedScreenshot != null}")
} catch (e: Exception) {
android.util.Log.e("ConduitVoiceSession", "Failed to get screenshot from bundle", e)
}
}
}
override fun onHandleScreenshot(screenshot: Bitmap?) {
super.onHandleScreenshot(screenshot)
capturedScreenshot = screenshot
android.util.Log.d("ConduitVoiceSession", "Screenshot received via onHandleScreenshot: ${screenshot != null}")
}
private fun launchApp() {
try {
android.util.Log.d("ConduitVoiceSession", "Attempting to launch app")
val intent = Intent(context, MainActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
context.startActivity(intent)
android.util.Log.d("ConduitVoiceSession", "App launch requested")
finish() // Close the overlay
} catch (e: Exception) {
android.util.Log.e("ConduitVoiceSession", "Failed to launch app", e)
}
}
private fun launchAppWithContext(includeScreenshot: Boolean) {
try {
android.util.Log.d("ConduitVoiceSession", "Attempting to launch app with context")
val intent = Intent(context, MainActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
if (capturedContext != null) {
intent.putExtra("screen_context", capturedContext)
android.util.Log.d("ConduitVoiceSession", "Context attached: ${capturedContext?.take(50)}...")
} else {
android.util.Log.d("ConduitVoiceSession", "No context captured")
}
context.startActivity(intent)
android.util.Log.d("ConduitVoiceSession", "App launch requested")
finish() // Close the overlay
} catch (e: Exception) {
android.util.Log.e("ConduitVoiceSession", "Failed to launch app", e)
}
}
private fun launchAppWithScreenshot() {
try {
android.util.Log.d("ConduitVoiceSession", "Attempting to launch app with screenshot")
val intent = Intent(context, MainActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
// Save screenshot to cache and pass URI
capturedScreenshot?.let { bitmap ->
try {
val file = java.io.File(context.cacheDir, "assistant_screenshot_${System.currentTimeMillis()}.png")
val outputStream = java.io.FileOutputStream(file)
bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream)
outputStream.flush()
outputStream.close()
intent.putExtra("screenshot_path", file.absolutePath)
android.util.Log.d("ConduitVoiceSession", "Screenshot saved to: ${file.absolutePath}")
} catch (e: Exception) {
android.util.Log.e("ConduitVoiceSession", "Failed to save screenshot", e)
}
} ?: run {
android.util.Log.d("ConduitVoiceSession", "No screenshot captured")
}
context.startActivity(intent)
android.util.Log.d("ConduitVoiceSession", "App launch requested with screenshot")
finish() // Close the overlay
} catch (e: Exception) {
android.util.Log.e("ConduitVoiceSession", "Failed to launch app with screenshot", e)
}
}
private fun launchAppForVoiceCall() {
try {
android.util.Log.d("ConduitVoiceSession", "Attempting to launch app for voice call")
val intent = Intent(context, MainActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
intent.putExtra("start_voice_call", true)
android.util.Log.d("ConduitVoiceSession", "Voice call flag attached")
context.startActivity(intent)
android.util.Log.d("ConduitVoiceSession", "App launch requested for voice call")
finish() // Close the overlay
} catch (e: Exception) {
android.util.Log.e("ConduitVoiceSession", "Failed to launch app for voice call", e)
}
}
private fun traverseNode(node: AssistStructure.ViewNode?, builder: StringBuilder) {
if (node == null) return
if (node.text != null) {
builder.append(node.text).append("\n")
}
// Also check content description for accessibility text
if (node.contentDescription != null) {
builder.append(node.contentDescription).append("\n")
}
for (i in 0 until node.childCount) {
traverseNode(node.getChildAt(i), builder)
}
}
}

View File

@@ -0,0 +1,11 @@
package app.cogwheel.conduit
import android.service.voice.VoiceInteractionSession
import android.service.voice.VoiceInteractionSessionService
import android.os.Bundle
class ConduitVoiceInteractionSessionService : VoiceInteractionSessionService() {
override fun onNewSession(args: Bundle?): VoiceInteractionSession {
return ConduitVoiceInteractionSession(this)
}
}

View File

@@ -23,12 +23,52 @@ class MainActivity : FlutterActivity() {
windowInsetsController.isAppearanceLightNavigationBars = false windowInsetsController.isAppearanceLightNavigationBars = false
} }
private val CHANNEL = "app.cogwheel.conduit/assistant"
private var methodChannel: io.flutter.plugin.common.MethodChannel? = null
override fun configureFlutterEngine(flutterEngine: FlutterEngine) { override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine) super.configureFlutterEngine(flutterEngine)
// Initialize background streaming handler // Initialize background streaming handler
backgroundStreamingHandler = BackgroundStreamingHandler(this) backgroundStreamingHandler = BackgroundStreamingHandler(this)
backgroundStreamingHandler.setup(flutterEngine) backgroundStreamingHandler.setup(flutterEngine)
methodChannel = io.flutter.plugin.common.MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL)
// Check if started with context
handleIntent(intent)
}
override fun onNewIntent(intent: android.content.Intent) {
super.onNewIntent(intent)
handleIntent(intent)
}
private fun handleIntent(intent: android.content.Intent) {
android.util.Log.d("MainActivity", "handleIntent called")
android.util.Log.d("MainActivity", "Intent extras: ${intent.extras?.keySet()}")
val screenContext = intent.getStringExtra("screen_context")
val screenshotPath = intent.getStringExtra("screenshot_path")
val startVoiceCall = intent.getBooleanExtra("start_voice_call", false)
android.util.Log.d("MainActivity", "screenContext: $screenContext")
android.util.Log.d("MainActivity", "screenshotPath: $screenshotPath")
android.util.Log.d("MainActivity", "startVoiceCall: $startVoiceCall")
android.util.Log.d("MainActivity", "methodChannel: $methodChannel")
if (startVoiceCall) {
android.util.Log.d("MainActivity", "Invoking startVoiceCall")
methodChannel?.invokeMethod("startVoiceCall", null)
} else if (screenContext != null) {
android.util.Log.d("MainActivity", "Invoking analyzeScreen")
methodChannel?.invokeMethod("analyzeScreen", screenContext)
} else if (screenshotPath != null) {
android.util.Log.d("MainActivity", "Invoking analyzeScreenshot with path: $screenshotPath")
methodChannel?.invokeMethod("analyzeScreenshot", screenshotPath)
} else {
android.util.Log.d("MainActivity", "No screen context or screenshot path found")
}
} }
override fun onDestroy() { override fun onDestroy() {

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#1F1F1F" />
<corners android:radius="28dp" />
</shape>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFF"
android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
</vector>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFF"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,19h-2v-2h2V19zM15.07,11.25l-0.9,0.92C13.45,12.9 13,13.5 13,15h-2v-0.5c0,-1.1 0.45,-2.1 1.17,-2.83l1.24,-1.26c0.37,-0.36 0.59,-0.86 0.59,-1.41 0,-1.1 -0.9,-2 -2,-2s-2,0.9 -2,2H8c0,-2.21 1.79,-4 4,-4s4,1.79 4,4c0,0.88 -0.36,1.68 -0.93,2.25z"/>
</vector>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFF"
android:pathData="M18,16.08c-0.76,0 -1.44,0.3 -1.96,0.77L8.91,12.7c0.05,-0.23 0.09,-0.46 0.09,-0.7s-0.04,-0.47 -0.09,-0.7l7.05,-4.11c0.54,0.5 1.25,0.81 2.04,0.81 1.66,0 3,-1.34 3,-3s-1.34,-3 -3,-3 -3,1.34 -3,3c0,0.24 0.04,0.47 0.09,0.7L8.04,9.81C7.5,9.31 6.79,9 6,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3c0.79,0 1.5,-0.31 2.04,-0.81l7.12,4.16c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.61 1.31,2.92 2.92,2.92 1.61,0 2.92,-1.31 2.92,-2.92s-1.31,-2.92 -2.92,-2.92z"/>
</vector>

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFF"
android:pathData="M12,2l1.5,5.5L19,9l-5.5,1.5L12,16l-1.5,-5.5L5,9l5.5,-1.5L12,2z"/>
<path
android:fillColor="#FFFFFF"
android:pathData="M19,15l0.94,2.06L22,18l-2.06,0.94L19,21l-0.94,-2.06L16,18l2.06,-0.94L19,15z"/>
</vector>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFF"
android:pathData="M14,2H6C4.9,2 4,2.9 4,4v16c0,1.1 0.89,2 1.99,2H18c1.1,0 2,-0.9 2,-2V8L14,2zM16,18H8v-2h8V18zM16,14H8v-2h8V14zM13,9V3.5L18.5,9H13z"/>
</vector>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFF"
android:pathData="M12,14c1.66,0 2.99,-1.34 2.99,-3L15,5c0,-1.66 -1.34,-3 -3,-3S9,3.34 9,5v6c0,1.66 1.34,3 3,3zM17.3,11c0,3 -2.54,5.1 -5.3,5.1S6.7,14 6.7,11H5c0,3.41 2.72,6.23 6,6.72V21h2v-3.28c3.28,-0.48 6,-3.3 6,-6.72h-1.7z"/>
</vector>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#2D2D2D" />
<corners android:radius="28dp" />
</shape>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#2196F3" />
<corners android:radius="20dp" />
</shape>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@android:color/white" />
<corners android:radius="28dp" />
</shape>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="#3D3D3D" />
</shape>

View File

@@ -0,0 +1,128 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#99000000">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:orientation="vertical"
android:padding="20dp"
android:layout_margin="16dp">
<!-- Action Buttons -->
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_gravity="start"
android:layout_marginBottom="16dp">
<!-- Summarize page button -->
<LinearLayout
android:id="@+id/btn_summarize"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:background="@drawable/dark_rounded_button_bg"
android:paddingStart="16dp"
android:paddingEnd="20dp"
android:paddingTop="14dp"
android:paddingBottom="14dp"
android:gravity="center_vertical"
android:layout_marginBottom="12dp"
android:clickable="true"
android:focusable="true">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/ic_summarize"
android:layout_marginEnd="12dp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Summarize page"
android:textColor="@android:color/white"
android:textSize="16sp"/>
</LinearLayout>
<!-- Ask about page button -->
<LinearLayout
android:id="@+id/btn_ask_about"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:background="@drawable/dark_rounded_button_bg"
android:paddingStart="16dp"
android:paddingEnd="20dp"
android:paddingTop="14dp"
android:paddingBottom="14dp"
android:gravity="center_vertical"
android:clickable="true"
android:focusable="true">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/ic_ask"
android:layout_marginEnd="12dp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Ask about page"
android:textColor="@android:color/white"
android:textSize="16sp"/>
</LinearLayout>
</LinearLayout>
<!-- Bottom Input Bar -->
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="56dp"
android:background="@drawable/input_bar_bg"
android:paddingStart="20dp"
android:paddingEnd="8dp">
<!-- Ask Conduit text -->
<TextView
android:id="@+id/input_area"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_toStartOf="@id/btn_voice"
android:layout_centerVertical="true"
android:text="Ask Conduit"
android:textColor="@android:color/white"
android:textSize="16sp"
android:alpha="0.6"
android:clickable="true"
android:focusable="true"/>
<!-- Voice button -->
<FrameLayout
android:id="@+id/btn_voice"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:background="@drawable/voice_button_bg"
android:clickable="true"
android:focusable="true">
<ImageView
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_gravity="center"
android:src="@drawable/ic_voice"/>
</FrameLayout>
</RelativeLayout>
</LinearLayout>
</FrameLayout>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<voice-interaction-service xmlns:android="http://schemas.android.com/apk/res/android"
android:sessionService="app.cogwheel.conduit.ConduitVoiceInteractionSessionService"
android:recognitionService="android.speech.RecognitionService"
android:settingsActivity="app.cogwheel.conduit.MainActivity"
android:supportsAssist="true"
android:supportsLocalInteraction="true" />

View File

@@ -0,0 +1,178 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:path/path.dart' as path;
import '../../features/chat/providers/chat_providers.dart';
import '../../features/chat/services/file_attachment_service.dart';
import '../../features/chat/views/voice_call_page.dart';
import '../services/navigation_service.dart';
import '../../shared/services/tasks/task_queue.dart';
import '../providers/app_providers.dart';
import '../../features/auth/providers/unified_auth_providers.dart';
import 'debug_logger.dart';
final androidAssistantProvider = Provider(
(ref) => AndroidAssistantHandler(ref),
);
final screenContextProvider = NotifierProvider<ScreenContextNotifier, String?>(
ScreenContextNotifier.new,
);
class ScreenContextNotifier extends Notifier<String?> {
@override
String? build() => null;
void setContext(String? context) {
state = context;
}
}
class AndroidAssistantHandler {
static const platform = MethodChannel('app.cogwheel.conduit/assistant');
final Ref _ref;
AndroidAssistantHandler(this._ref) {
platform.setMethodCallHandler(_handleMethodCall);
}
Future<void> _handleMethodCall(MethodCall call) async {
if (call.method == 'analyzeScreen') {
final String context = call.arguments as String;
await _processScreenContext(context);
} else if (call.method == 'analyzeScreenshot') {
final String screenshotPath = call.arguments as String;
await _processScreenshot(screenshotPath);
} else if (call.method == 'startVoiceCall') {
await _startVoiceCall();
}
}
Future<void> _processScreenContext(String context) async {
try {
DebugLogger.log('Processing screen context', scope: 'assistant');
// Wait for app to be ready (authenticated and model available)
final navState = _ref.read(authNavigationStateProvider);
final model = _ref.read(selectedModelProvider);
if (navState != AuthNavigationState.authenticated || model == null) {
DebugLogger.log('App not ready for screen context processing', scope: 'assistant');
return;
}
// Navigate to chat if not already there
final isOnChatRoute = NavigationService.currentRoute == Routes.chat;
if (!isOnChatRoute) {
// Navigation will happen via auth state
return;
}
// Set the screen context
_ref.read(screenContextProvider.notifier).setContext(context);
DebugLogger.log('Screen context set successfully', scope: 'assistant');
} catch (e) {
DebugLogger.log('Failed to process screen context: $e', scope: 'assistant');
}
}
Future<void> _processScreenshot(String screenshotPath) async {
try {
DebugLogger.log('Processing screenshot: $screenshotPath', scope: 'assistant');
// Wait for app to be ready (authenticated and model available)
final navState = _ref.read(authNavigationStateProvider);
final model = _ref.read(selectedModelProvider);
if (navState != AuthNavigationState.authenticated || model == null) {
DebugLogger.log('App not ready for screenshot processing', scope: 'assistant');
return;
}
// Navigate to chat if not already there
final isOnChatRoute = NavigationService.currentRoute == Routes.chat;
if (!isOnChatRoute) {
// Navigation will happen via auth state
return;
}
// Start a fresh chat context
startNewChat(_ref);
// Add screenshot as attachment
final file = File(screenshotPath);
if (!await file.exists()) {
DebugLogger.log('Screenshot file not found: $screenshotPath', scope: 'assistant');
return;
}
final svc = _ref.read(fileAttachmentServiceProvider);
if (svc != null) {
final attachment = LocalAttachment(
file: file,
displayName: path.basename(screenshotPath),
);
_ref.read(attachedFilesProvider.notifier).addFiles([attachment]);
// Enqueue upload via task queue
final activeConv = _ref.read(activeConversationProvider);
try {
await _ref.read(taskQueueProvider.notifier).enqueueUploadMedia(
conversationId: activeConv?.id,
filePath: attachment.file.path,
fileName: attachment.displayName,
fileSize: await attachment.file.length(),
);
DebugLogger.log('Screenshot uploaded successfully', scope: 'assistant');
} catch (e) {
DebugLogger.log('Failed to upload screenshot: $e', scope: 'assistant');
}
}
} catch (e) {
DebugLogger.log('Failed to process screenshot: $e', scope: 'assistant');
}
}
Future<void> _startVoiceCall() async {
try {
DebugLogger.log('Starting voice call from assistant', scope: 'assistant');
// Wait for app to be ready (authenticated and model available)
final navState = _ref.read(authNavigationStateProvider);
final model = _ref.read(selectedModelProvider);
if (navState != AuthNavigationState.authenticated || model == null) {
DebugLogger.log('App not ready for voice call', scope: 'assistant');
return;
}
// Navigate to chat if not already there
final isOnChatRoute = NavigationService.currentRoute == Routes.chat;
if (!isOnChatRoute) {
// Navigation will happen via auth state
return;
}
// Get the current BuildContext from the navigation service
final context = NavigationService.navigatorKey.currentContext;
if (context == null) {
DebugLogger.log('No context available for voice call navigation', scope: 'assistant');
return;
}
// Navigate to voice call page
await Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const VoiceCallPage(),
fullscreenDialog: true,
),
);
DebugLogger.log('Voice call page launched', scope: 'assistant');
} catch (e) {
DebugLogger.log('Failed to start voice call: $e', scope: 'assistant');
}
}
}

View File

@@ -17,6 +17,7 @@ import '../../../core/utils/debug_logger.dart';
import '../../../core/utils/user_display_name.dart'; import '../../../core/utils/user_display_name.dart';
import '../../../core/utils/model_icon_utils.dart'; import '../../../core/utils/model_icon_utils.dart';
import '../../auth/providers/unified_auth_providers.dart'; import '../../auth/providers/unified_auth_providers.dart';
import '../../../core/utils/android_assistant_handler.dart';
import '../widgets/modern_chat_input.dart'; import '../widgets/modern_chat_input.dart';
import '../widgets/user_message_bubble.dart'; import '../widgets/user_message_bubble.dart';
@@ -288,6 +289,10 @@ class _ChatPageState extends ConsumerState<ChatPage> {
// Initialize chat page components // Initialize chat page components
WidgetsBinding.instance.addPostFrameCallback((_) async { WidgetsBinding.instance.addPostFrameCallback((_) async {
if (!mounted) return; if (!mounted) return;
// Initialize Android Assistant Handler
ref.read(androidAssistantProvider);
// First, ensure a model is selected // First, ensure a model is selected
await _checkAndAutoSelectModel(); await _checkAndAutoSelectModel();
if (!mounted) return; if (!mounted) return;
@@ -301,6 +306,24 @@ class _ChatPageState extends ConsumerState<ChatPage> {
}); });
} }
@override
void didChangeDependencies() {
super.didChangeDependencies();
// Listen for screen context from Android Assistant
final screenContext = ref.watch(screenContextProvider);
if (screenContext != null && screenContext.isNotEmpty) {
// Clear the context so we don't process it again
WidgetsBinding.instance.addPostFrameCallback((_) {
ref.read(screenContextProvider.notifier).setContext(null);
final currentModel = ref.read(selectedModelProvider);
_handleMessageSend(
"Here is the content of my screen:\n\n$screenContext\n\nCan you summarize this?",
currentModel,
);
});
}
}
@override @override
void dispose() { void dispose() {
_scrollController.dispose(); _scrollController.dispose();

View File

@@ -961,10 +961,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: meta name: meta
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.16.0" version: "1.17.0"
mime: mime:
dependency: transitive dependency: transitive
description: description:
@@ -1606,26 +1606,26 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: test name: test
sha256: "65e29d831719be0591f7b3b1a32a3cda258ec98c58c7b25f7b84241bc31215bb" sha256: "75906bf273541b676716d1ca7627a17e4c4070a3a16272b7a3dc7da3b9f3f6b7"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.26.2" version: "1.26.3"
test_api: test_api:
dependency: transitive dependency: transitive
description: description:
name: test_api name: test_api
sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.7.6" version: "0.7.7"
test_core: test_core:
dependency: transitive dependency: transitive
description: description:
name: test_core name: test_core
sha256: "80bf5a02b60af04b09e14f6fe68b921aad119493e26e490deaca5993fef1b05a" sha256: "0cc24b5ff94b38d2ae73e1eb43cc302b77964fbf67abad1e296025b78deb53d0"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.6.11" version: "0.6.12"
timezone: timezone:
dependency: transitive dependency: transitive
description: description: