feat(profile): Add Android assistant trigger customization option
This commit is contained in:
@@ -10,10 +10,29 @@ import android.graphics.Bitmap
|
|||||||
|
|
||||||
class ConduitVoiceInteractionSession(context: Context) : VoiceInteractionSession(context) {
|
class ConduitVoiceInteractionSession(context: Context) : VoiceInteractionSession(context) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val PREFS_FILE = "FlutterSharedPreferences"
|
||||||
|
private const val TRIGGER_KEY = "flutter.android_assistant_trigger"
|
||||||
|
private const val TRIGGER_OVERLAY = "overlay"
|
||||||
|
private const val TRIGGER_NEW_CHAT = "new_chat"
|
||||||
|
private const val TRIGGER_VOICE_CALL = "voice_call"
|
||||||
|
}
|
||||||
|
|
||||||
private var capturedContext: String? = null
|
private var capturedContext: String? = null
|
||||||
private var capturedScreenshot: Bitmap? = null
|
private var capturedScreenshot: Bitmap? = null
|
||||||
|
|
||||||
override fun onCreateContentView(): android.view.View {
|
override fun onCreateContentView(): android.view.View {
|
||||||
|
when (getTriggerPreference()) {
|
||||||
|
TRIGGER_NEW_CHAT -> {
|
||||||
|
launchAppForNewChat()
|
||||||
|
return android.view.View(context)
|
||||||
|
}
|
||||||
|
TRIGGER_VOICE_CALL -> {
|
||||||
|
launchAppForVoiceCall()
|
||||||
|
return android.view.View(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val view = layoutInflater.inflate(app.cogwheel.conduit.R.layout.assistant_overlay, null)
|
val view = layoutInflater.inflate(app.cogwheel.conduit.R.layout.assistant_overlay, null)
|
||||||
|
|
||||||
// Summarize page button - sends screen context
|
// Summarize page button - sends screen context
|
||||||
@@ -157,6 +176,25 @@ class ConduitVoiceInteractionSession(context: Context) : VoiceInteractionSession
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun launchAppForNewChat() {
|
||||||
|
try {
|
||||||
|
android.util.Log.d("ConduitVoiceSession", "Attempting to launch app for new chat")
|
||||||
|
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_new_chat", true)
|
||||||
|
android.util.Log.d("ConduitVoiceSession", "New chat flag attached")
|
||||||
|
|
||||||
|
context.startActivity(intent)
|
||||||
|
android.util.Log.d("ConduitVoiceSession", "App launch requested for new chat")
|
||||||
|
finish()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
android.util.Log.e("ConduitVoiceSession", "Failed to launch app for new chat", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun launchAppForVoiceCall() {
|
private fun launchAppForVoiceCall() {
|
||||||
try {
|
try {
|
||||||
android.util.Log.d("ConduitVoiceSession", "Attempting to launch app for voice call")
|
android.util.Log.d("ConduitVoiceSession", "Attempting to launch app for voice call")
|
||||||
@@ -176,6 +214,15 @@ class ConduitVoiceInteractionSession(context: Context) : VoiceInteractionSession
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getTriggerPreference(): String {
|
||||||
|
return try {
|
||||||
|
val prefs = context.getSharedPreferences(PREFS_FILE, Context.MODE_PRIVATE)
|
||||||
|
prefs.getString(TRIGGER_KEY, TRIGGER_OVERLAY) ?: TRIGGER_OVERLAY
|
||||||
|
} catch (e: Exception) {
|
||||||
|
TRIGGER_OVERLAY
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun traverseNode(node: AssistStructure.ViewNode?, builder: StringBuilder) {
|
private fun traverseNode(node: AssistStructure.ViewNode?, builder: StringBuilder) {
|
||||||
if (node == null) return
|
if (node == null) return
|
||||||
|
|
||||||
|
|||||||
@@ -51,15 +51,20 @@ class MainActivity : FlutterActivity() {
|
|||||||
val screenContext = intent.getStringExtra("screen_context")
|
val screenContext = intent.getStringExtra("screen_context")
|
||||||
val screenshotPath = intent.getStringExtra("screenshot_path")
|
val screenshotPath = intent.getStringExtra("screenshot_path")
|
||||||
val startVoiceCall = intent.getBooleanExtra("start_voice_call", false)
|
val startVoiceCall = intent.getBooleanExtra("start_voice_call", false)
|
||||||
|
val startNewChat = intent.getBooleanExtra("start_new_chat", false)
|
||||||
|
|
||||||
android.util.Log.d("MainActivity", "screenContext: $screenContext")
|
android.util.Log.d("MainActivity", "screenContext: $screenContext")
|
||||||
android.util.Log.d("MainActivity", "screenshotPath: $screenshotPath")
|
android.util.Log.d("MainActivity", "screenshotPath: $screenshotPath")
|
||||||
android.util.Log.d("MainActivity", "startVoiceCall: $startVoiceCall")
|
android.util.Log.d("MainActivity", "startVoiceCall: $startVoiceCall")
|
||||||
|
android.util.Log.d("MainActivity", "startNewChat: $startNewChat")
|
||||||
android.util.Log.d("MainActivity", "methodChannel: $methodChannel")
|
android.util.Log.d("MainActivity", "methodChannel: $methodChannel")
|
||||||
|
|
||||||
if (startVoiceCall) {
|
if (startVoiceCall) {
|
||||||
android.util.Log.d("MainActivity", "Invoking startVoiceCall")
|
android.util.Log.d("MainActivity", "Invoking startVoiceCall")
|
||||||
methodChannel?.invokeMethod("startVoiceCall", null)
|
methodChannel?.invokeMethod("startVoiceCall", null)
|
||||||
|
} else if (startNewChat) {
|
||||||
|
android.util.Log.d("MainActivity", "Invoking startNewChat")
|
||||||
|
methodChannel?.invokeMethod("startNewChat", null)
|
||||||
} else if (screenContext != null) {
|
} else if (screenContext != null) {
|
||||||
android.util.Log.d("MainActivity", "Invoking analyzeScreen")
|
android.util.Log.d("MainActivity", "Invoking analyzeScreen")
|
||||||
methodChannel?.invokeMethod("analyzeScreen", screenContext)
|
methodChannel?.invokeMethod("analyzeScreen", screenContext)
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ final class PreferenceKeys {
|
|||||||
static const String ttsServerVoiceId = 'tts_server_voice_id';
|
static const String ttsServerVoiceId = 'tts_server_voice_id';
|
||||||
static const String ttsServerVoiceName = 'tts_server_voice_name';
|
static const String ttsServerVoiceName = 'tts_server_voice_name';
|
||||||
static const String voiceSilenceDuration = 'voice_silence_duration';
|
static const String voiceSilenceDuration = 'voice_silence_duration';
|
||||||
|
static const String androidAssistantTrigger = 'android_assistant_trigger';
|
||||||
}
|
}
|
||||||
|
|
||||||
final class LegacyPreferenceKeys {
|
final class LegacyPreferenceKeys {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import 'dart:developer' as developer;
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import 'package:hive_ce/hive.dart';
|
import 'package:hive_ce/hive.dart';
|
||||||
import '../persistence/hive_bootstrap.dart';
|
import '../persistence/hive_bootstrap.dart';
|
||||||
import '../persistence/hive_boxes.dart';
|
import '../persistence/hive_boxes.dart';
|
||||||
@@ -17,6 +18,22 @@ enum SttPreference { deviceOnly, serverOnly }
|
|||||||
/// TTS engine selection
|
/// TTS engine selection
|
||||||
enum TtsEngine { device, server }
|
enum TtsEngine { device, server }
|
||||||
|
|
||||||
|
/// Action to take when the Android digital assistant is triggered.
|
||||||
|
enum AndroidAssistantTrigger { overlay, newChat, voiceCall }
|
||||||
|
|
||||||
|
extension AndroidAssistantTriggerStorage on AndroidAssistantTrigger {
|
||||||
|
String get storageValue {
|
||||||
|
switch (this) {
|
||||||
|
case AndroidAssistantTrigger.overlay:
|
||||||
|
return 'overlay';
|
||||||
|
case AndroidAssistantTrigger.newChat:
|
||||||
|
return 'new_chat';
|
||||||
|
case AndroidAssistantTrigger.voiceCall:
|
||||||
|
return 'voice_call';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Service for managing app-wide settings including accessibility preferences
|
/// Service for managing app-wide settings including accessibility preferences
|
||||||
class SettingsService {
|
class SettingsService {
|
||||||
static const String _reduceMotionKey = PreferenceKeys.reduceMotion;
|
static const String _reduceMotionKey = PreferenceKeys.reduceMotion;
|
||||||
@@ -41,6 +58,8 @@ class SettingsService {
|
|||||||
// Voice silence duration for auto-stop (milliseconds)
|
// Voice silence duration for auto-stop (milliseconds)
|
||||||
static const String _voiceSilenceDurationKey =
|
static const String _voiceSilenceDurationKey =
|
||||||
PreferenceKeys.voiceSilenceDuration;
|
PreferenceKeys.voiceSilenceDuration;
|
||||||
|
static const String _androidAssistantTriggerKey =
|
||||||
|
PreferenceKeys.androidAssistantTrigger;
|
||||||
static Box<dynamic> _preferencesBox() =>
|
static Box<dynamic> _preferencesBox() =>
|
||||||
Hive.box<dynamic>(HiveBoxNames.preferences);
|
Hive.box<dynamic>(HiveBoxNames.preferences);
|
||||||
|
|
||||||
@@ -153,6 +172,8 @@ class SettingsService {
|
|||||||
PreferenceKeys.ttsEngine: settings.ttsEngine.name,
|
PreferenceKeys.ttsEngine: settings.ttsEngine.name,
|
||||||
PreferenceKeys.voiceSttPreference: settings.sttPreference.name,
|
PreferenceKeys.voiceSttPreference: settings.sttPreference.name,
|
||||||
_voiceSilenceDurationKey: settings.voiceSilenceDuration,
|
_voiceSilenceDurationKey: settings.voiceSilenceDuration,
|
||||||
|
_androidAssistantTriggerKey:
|
||||||
|
settings.androidAssistantTrigger.storageValue,
|
||||||
};
|
};
|
||||||
|
|
||||||
await box.putAll(updates);
|
await box.putAll(updates);
|
||||||
@@ -191,6 +212,8 @@ class SettingsService {
|
|||||||
} else {
|
} else {
|
||||||
await box.delete(PreferenceKeys.ttsServerVoiceName);
|
await box.delete(PreferenceKeys.ttsServerVoiceName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await _writeAssistantTriggerToSharedPrefs(settings.androidAssistantTrigger);
|
||||||
}
|
}
|
||||||
|
|
||||||
static TtsEngine _parseTtsEngine(String? raw) {
|
static TtsEngine _parseTtsEngine(String? raw) {
|
||||||
@@ -219,6 +242,20 @@ class SettingsService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static AndroidAssistantTrigger _parseAndroidAssistantTrigger(String? raw) {
|
||||||
|
switch ((raw ?? '').toLowerCase()) {
|
||||||
|
case 'new_chat':
|
||||||
|
case 'newchat':
|
||||||
|
return AndroidAssistantTrigger.newChat;
|
||||||
|
case 'voice_call':
|
||||||
|
case 'voicecall':
|
||||||
|
return AndroidAssistantTrigger.voiceCall;
|
||||||
|
case 'overlay':
|
||||||
|
default:
|
||||||
|
return AndroidAssistantTrigger.overlay;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Voice input specific settings
|
// Voice input specific settings
|
||||||
static Future<String?> getVoiceLocaleId() {
|
static Future<String?> getVoiceLocaleId() {
|
||||||
final value = _preferencesBox().get(_voiceLocaleKey) as String?;
|
final value = _preferencesBox().get(_voiceLocaleKey) as String?;
|
||||||
@@ -309,6 +346,30 @@ class SettingsService {
|
|||||||
return _preferencesBox().put(_voiceSilenceDurationKey, sanitized);
|
return _preferencesBox().put(_voiceSilenceDurationKey, sanitized);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Future<void> setAndroidAssistantTrigger(
|
||||||
|
AndroidAssistantTrigger trigger,
|
||||||
|
) async {
|
||||||
|
await _preferencesBox().put(
|
||||||
|
_androidAssistantTriggerKey,
|
||||||
|
trigger.storageValue,
|
||||||
|
);
|
||||||
|
await _writeAssistantTriggerToSharedPrefs(trigger);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<void> _writeAssistantTriggerToSharedPrefs(
|
||||||
|
AndroidAssistantTrigger trigger,
|
||||||
|
) async {
|
||||||
|
try {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
await prefs.setString(
|
||||||
|
PreferenceKeys.androidAssistantTrigger,
|
||||||
|
trigger.storageValue,
|
||||||
|
);
|
||||||
|
} catch (_) {
|
||||||
|
// SharedPreferences writes are best-effort for Android assistant access
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Get effective animation duration considering all settings
|
/// Get effective animation duration considering all settings
|
||||||
static Duration getEffectiveAnimationDuration(
|
static Duration getEffectiveAnimationDuration(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
@@ -372,6 +433,9 @@ class SettingsService {
|
|||||||
sttPreference: _parseSttPreference(
|
sttPreference: _parseSttPreference(
|
||||||
box.get(PreferenceKeys.voiceSttPreference) as String?,
|
box.get(PreferenceKeys.voiceSttPreference) as String?,
|
||||||
),
|
),
|
||||||
|
androidAssistantTrigger: _parseAndroidAssistantTrigger(
|
||||||
|
box.get(_androidAssistantTriggerKey) as String?,
|
||||||
|
),
|
||||||
voiceSilenceDuration: (box.get(_voiceSilenceDurationKey) as int? ?? 2000)
|
voiceSilenceDuration: (box.get(_voiceSilenceDurationKey) as int? ?? 2000)
|
||||||
.clamp(300, 3000),
|
.clamp(300, 3000),
|
||||||
);
|
);
|
||||||
@@ -406,6 +470,7 @@ class AppSettings {
|
|||||||
final TtsEngine ttsEngine;
|
final TtsEngine ttsEngine;
|
||||||
final String? ttsServerVoiceId;
|
final String? ttsServerVoiceId;
|
||||||
final String? ttsServerVoiceName;
|
final String? ttsServerVoiceName;
|
||||||
|
final AndroidAssistantTrigger androidAssistantTrigger;
|
||||||
final int voiceSilenceDuration;
|
final int voiceSilenceDuration;
|
||||||
const AppSettings({
|
const AppSettings({
|
||||||
this.reduceMotion = false,
|
this.reduceMotion = false,
|
||||||
@@ -429,6 +494,7 @@ class AppSettings {
|
|||||||
this.ttsEngine = TtsEngine.device,
|
this.ttsEngine = TtsEngine.device,
|
||||||
this.ttsServerVoiceId,
|
this.ttsServerVoiceId,
|
||||||
this.ttsServerVoiceName,
|
this.ttsServerVoiceName,
|
||||||
|
this.androidAssistantTrigger = AndroidAssistantTrigger.overlay,
|
||||||
this.voiceSilenceDuration = 2000,
|
this.voiceSilenceDuration = 2000,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -455,6 +521,7 @@ class AppSettings {
|
|||||||
Object? ttsServerVoiceId = const _DefaultValue(),
|
Object? ttsServerVoiceId = const _DefaultValue(),
|
||||||
Object? ttsServerVoiceName = const _DefaultValue(),
|
Object? ttsServerVoiceName = const _DefaultValue(),
|
||||||
int? voiceSilenceDuration,
|
int? voiceSilenceDuration,
|
||||||
|
AndroidAssistantTrigger? androidAssistantTrigger,
|
||||||
}) {
|
}) {
|
||||||
return AppSettings(
|
return AppSettings(
|
||||||
reduceMotion: reduceMotion ?? this.reduceMotion,
|
reduceMotion: reduceMotion ?? this.reduceMotion,
|
||||||
@@ -486,6 +553,8 @@ class AppSettings {
|
|||||||
ttsServerVoiceName: ttsServerVoiceName is _DefaultValue
|
ttsServerVoiceName: ttsServerVoiceName is _DefaultValue
|
||||||
? this.ttsServerVoiceName
|
? this.ttsServerVoiceName
|
||||||
: ttsServerVoiceName as String?,
|
: ttsServerVoiceName as String?,
|
||||||
|
androidAssistantTrigger:
|
||||||
|
androidAssistantTrigger ?? this.androidAssistantTrigger,
|
||||||
voiceSilenceDuration: voiceSilenceDuration ?? this.voiceSilenceDuration,
|
voiceSilenceDuration: voiceSilenceDuration ?? this.voiceSilenceDuration,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -513,6 +582,7 @@ class AppSettings {
|
|||||||
other.ttsEngine == ttsEngine &&
|
other.ttsEngine == ttsEngine &&
|
||||||
other.ttsServerVoiceId == ttsServerVoiceId &&
|
other.ttsServerVoiceId == ttsServerVoiceId &&
|
||||||
other.ttsServerVoiceName == ttsServerVoiceName &&
|
other.ttsServerVoiceName == ttsServerVoiceName &&
|
||||||
|
other.androidAssistantTrigger == androidAssistantTrigger &&
|
||||||
other.voiceSilenceDuration == voiceSilenceDuration &&
|
other.voiceSilenceDuration == voiceSilenceDuration &&
|
||||||
_listEquals(other.quickPills, quickPills);
|
_listEquals(other.quickPills, quickPills);
|
||||||
// socketTransportMode intentionally not included in == to avoid frequent rebuilds
|
// socketTransportMode intentionally not included in == to avoid frequent rebuilds
|
||||||
@@ -541,6 +611,7 @@ class AppSettings {
|
|||||||
ttsEngine,
|
ttsEngine,
|
||||||
ttsServerVoiceId,
|
ttsServerVoiceId,
|
||||||
ttsServerVoiceName,
|
ttsServerVoiceName,
|
||||||
|
androidAssistantTrigger,
|
||||||
voiceSilenceDuration,
|
voiceSilenceDuration,
|
||||||
Object.hashAllUnordered(quickPills),
|
Object.hashAllUnordered(quickPills),
|
||||||
]);
|
]);
|
||||||
@@ -715,6 +786,16 @@ class AppSettingsNotifier extends _$AppSettingsNotifier {
|
|||||||
await SettingsService.setVoiceSilenceDuration(milliseconds);
|
await SettingsService.setVoiceSilenceDuration(milliseconds);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> setAndroidAssistantTrigger(
|
||||||
|
AndroidAssistantTrigger trigger,
|
||||||
|
) async {
|
||||||
|
if (state.androidAssistantTrigger == trigger) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
state = state.copyWith(androidAssistantTrigger: trigger);
|
||||||
|
await SettingsService.setAndroidAssistantTrigger(trigger);
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> resetToDefaults() async {
|
Future<void> resetToDefaults() async {
|
||||||
const defaultSettings = AppSettings();
|
const defaultSettings = AppSettings();
|
||||||
await SettingsService.saveSettings(defaultSettings);
|
await SettingsService.saveSettings(defaultSettings);
|
||||||
|
|||||||
@@ -46,19 +46,27 @@ class AndroidAssistantHandler {
|
|||||||
await _processScreenshot(screenshotPath);
|
await _processScreenshot(screenshotPath);
|
||||||
} else if (call.method == 'startVoiceCall') {
|
} else if (call.method == 'startVoiceCall') {
|
||||||
await _startVoiceCall();
|
await _startVoiceCall();
|
||||||
|
} else if (call.method == 'startNewChat') {
|
||||||
|
await _startNewChat();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _processScreenshot(String screenshotPath) async {
|
Future<void> _processScreenshot(String screenshotPath) async {
|
||||||
try {
|
try {
|
||||||
DebugLogger.log('Processing screenshot: $screenshotPath', scope: 'assistant');
|
DebugLogger.log(
|
||||||
|
'Processing screenshot: $screenshotPath',
|
||||||
|
scope: 'assistant',
|
||||||
|
);
|
||||||
|
|
||||||
// Wait for app to be ready (authenticated and model available)
|
// Wait for app to be ready (authenticated and model available)
|
||||||
final navState = _ref.read(authNavigationStateProvider);
|
final navState = _ref.read(authNavigationStateProvider);
|
||||||
final model = _ref.read(selectedModelProvider);
|
final model = _ref.read(selectedModelProvider);
|
||||||
|
|
||||||
if (navState != AuthNavigationState.authenticated || model == null) {
|
if (navState != AuthNavigationState.authenticated || model == null) {
|
||||||
DebugLogger.log('App not ready for screenshot processing', scope: 'assistant');
|
DebugLogger.log(
|
||||||
|
'App not ready for screenshot processing',
|
||||||
|
scope: 'assistant',
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,7 +83,10 @@ class AndroidAssistantHandler {
|
|||||||
// Add screenshot as attachment
|
// Add screenshot as attachment
|
||||||
final file = File(screenshotPath);
|
final file = File(screenshotPath);
|
||||||
if (!await file.exists()) {
|
if (!await file.exists()) {
|
||||||
DebugLogger.log('Screenshot file not found: $screenshotPath', scope: 'assistant');
|
DebugLogger.log(
|
||||||
|
'Screenshot file not found: $screenshotPath',
|
||||||
|
scope: 'assistant',
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,15 +102,23 @@ class AndroidAssistantHandler {
|
|||||||
// Enqueue upload via task queue
|
// Enqueue upload via task queue
|
||||||
final activeConv = _ref.read(activeConversationProvider);
|
final activeConv = _ref.read(activeConversationProvider);
|
||||||
try {
|
try {
|
||||||
await _ref.read(taskQueueProvider.notifier).enqueueUploadMedia(
|
await _ref
|
||||||
|
.read(taskQueueProvider.notifier)
|
||||||
|
.enqueueUploadMedia(
|
||||||
conversationId: activeConv?.id,
|
conversationId: activeConv?.id,
|
||||||
filePath: attachment.file.path,
|
filePath: attachment.file.path,
|
||||||
fileName: attachment.displayName,
|
fileName: attachment.displayName,
|
||||||
fileSize: await attachment.file.length(),
|
fileSize: await attachment.file.length(),
|
||||||
);
|
);
|
||||||
DebugLogger.log('Screenshot uploaded successfully', scope: 'assistant');
|
DebugLogger.log(
|
||||||
|
'Screenshot uploaded successfully',
|
||||||
|
scope: 'assistant',
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
DebugLogger.log('Failed to upload screenshot: $e', scope: 'assistant');
|
DebugLogger.log(
|
||||||
|
'Failed to upload screenshot: $e',
|
||||||
|
scope: 'assistant',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -130,7 +149,10 @@ class AndroidAssistantHandler {
|
|||||||
// Get the current BuildContext from the navigation service
|
// Get the current BuildContext from the navigation service
|
||||||
final context = NavigationService.navigatorKey.currentContext;
|
final context = NavigationService.navigatorKey.currentContext;
|
||||||
if (context == null) {
|
if (context == null) {
|
||||||
DebugLogger.log('No context available for voice call navigation', scope: 'assistant');
|
DebugLogger.log(
|
||||||
|
'No context available for voice call navigation',
|
||||||
|
scope: 'assistant',
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,4 +169,28 @@ class AndroidAssistantHandler {
|
|||||||
DebugLogger.log('Failed to start voice call: $e', scope: 'assistant');
|
DebugLogger.log('Failed to start voice call: $e', scope: 'assistant');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _startNewChat() async {
|
||||||
|
try {
|
||||||
|
DebugLogger.log('Starting new chat from assistant', scope: 'assistant');
|
||||||
|
|
||||||
|
final navState = _ref.read(authNavigationStateProvider);
|
||||||
|
final model = _ref.read(selectedModelProvider);
|
||||||
|
|
||||||
|
if (navState != AuthNavigationState.authenticated || model == null) {
|
||||||
|
DebugLogger.log('App not ready for new chat', scope: 'assistant');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final isOnChatRoute = NavigationService.currentRoute == Routes.chat;
|
||||||
|
if (!isOnChatRoute) {
|
||||||
|
await NavigationService.navigateToChat();
|
||||||
|
}
|
||||||
|
|
||||||
|
startNewChat(_ref);
|
||||||
|
DebugLogger.log('New chat started from assistant', scope: 'assistant');
|
||||||
|
} catch (e) {
|
||||||
|
DebugLogger.log('Failed to start new chat: $e', scope: 'assistant');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -407,6 +407,10 @@ class AppCustomizationPage extends ConsumerWidget {
|
|||||||
final transportLabel = activeTransportMode == 'polling'
|
final transportLabel = activeTransportMode == 'polling'
|
||||||
? l10n.transportModePolling
|
? l10n.transportModePolling
|
||||||
: l10n.transportModeWs;
|
: l10n.transportModeWs;
|
||||||
|
final assistantTriggerLabel = _androidAssistantTriggerLabel(
|
||||||
|
l10n,
|
||||||
|
settings.androidAssistantTrigger,
|
||||||
|
);
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
@@ -467,10 +471,164 @@ class AppCustomizationPage extends ConsumerWidget {
|
|||||||
.read(appSettingsProvider.notifier)
|
.read(appSettingsProvider.notifier)
|
||||||
.setSendOnEnter(!settings.sendOnEnter),
|
.setSendOnEnter(!settings.sendOnEnter),
|
||||||
),
|
),
|
||||||
|
if (Platform.isAndroid) ...[
|
||||||
|
const SizedBox(height: Spacing.sm),
|
||||||
|
_CustomizationTile(
|
||||||
|
leading: _buildIconBadge(
|
||||||
|
context,
|
||||||
|
Icons.assistant,
|
||||||
|
color: theme.buttonPrimary,
|
||||||
|
),
|
||||||
|
title: l10n.androidAssistantTitle,
|
||||||
|
subtitle: assistantTriggerLabel,
|
||||||
|
onTap: () =>
|
||||||
|
_showAndroidAssistantTriggerSheet(context, ref, settings),
|
||||||
|
),
|
||||||
|
],
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String _androidAssistantTriggerLabel(
|
||||||
|
AppLocalizations l10n,
|
||||||
|
AndroidAssistantTrigger trigger,
|
||||||
|
) {
|
||||||
|
switch (trigger) {
|
||||||
|
case AndroidAssistantTrigger.overlay:
|
||||||
|
return l10n.androidAssistantOverlayOption;
|
||||||
|
case AndroidAssistantTrigger.newChat:
|
||||||
|
return l10n.androidAssistantNewChatOption;
|
||||||
|
case AndroidAssistantTrigger.voiceCall:
|
||||||
|
return l10n.androidAssistantVoiceCallOption;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _showAndroidAssistantTriggerSheet(
|
||||||
|
BuildContext context,
|
||||||
|
WidgetRef ref,
|
||||||
|
AppSettings settings,
|
||||||
|
) async {
|
||||||
|
final theme = context.conduitTheme;
|
||||||
|
final l10n = AppLocalizations.of(context)!;
|
||||||
|
final options = <({AndroidAssistantTrigger value, String label})>[
|
||||||
|
(
|
||||||
|
value: AndroidAssistantTrigger.overlay,
|
||||||
|
label: l10n.androidAssistantOverlayOption,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
value: AndroidAssistantTrigger.newChat,
|
||||||
|
label: l10n.androidAssistantNewChatOption,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
value: AndroidAssistantTrigger.voiceCall,
|
||||||
|
label: l10n.androidAssistantVoiceCallOption,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
await showModalBottomSheet<void>(
|
||||||
|
context: context,
|
||||||
|
backgroundColor: theme.sidebarBackground,
|
||||||
|
shape: const RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.vertical(
|
||||||
|
top: Radius.circular(AppBorderRadius.modal),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
builder: (sheetContext) {
|
||||||
|
return SafeArea(
|
||||||
|
top: false,
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: Spacing.lg,
|
||||||
|
vertical: Spacing.md,
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
l10n.androidAssistantTitle,
|
||||||
|
style:
|
||||||
|
theme.headingSmall?.copyWith(
|
||||||
|
color: theme.sidebarForeground,
|
||||||
|
) ??
|
||||||
|
TextStyle(
|
||||||
|
color: theme.sidebarForeground,
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: Spacing.xs),
|
||||||
|
Text(
|
||||||
|
l10n.androidAssistantDescription,
|
||||||
|
style:
|
||||||
|
theme.bodySmall?.copyWith(
|
||||||
|
color: theme.sidebarForeground.withValues(
|
||||||
|
alpha: 0.7,
|
||||||
|
),
|
||||||
|
) ??
|
||||||
|
TextStyle(
|
||||||
|
color: theme.sidebarForeground.withValues(
|
||||||
|
alpha: 0.7,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(Icons.close, color: theme.iconPrimary),
|
||||||
|
onPressed: () => Navigator.of(sheetContext).pop(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Divider(height: 1),
|
||||||
|
for (var i = 0; i < options.length; i++) ...[
|
||||||
|
() {
|
||||||
|
final option = options[i];
|
||||||
|
final selected =
|
||||||
|
settings.androidAssistantTrigger == option.value;
|
||||||
|
return ListTile(
|
||||||
|
leading: Icon(
|
||||||
|
selected ? Icons.check_circle : Icons.circle_outlined,
|
||||||
|
color: selected
|
||||||
|
? theme.buttonPrimary
|
||||||
|
: theme.iconSecondary,
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
option.label,
|
||||||
|
style: theme.bodyMedium?.copyWith(
|
||||||
|
color: theme.sidebarForeground,
|
||||||
|
fontWeight: selected
|
||||||
|
? FontWeight.w600
|
||||||
|
: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
if (!selected) {
|
||||||
|
ref
|
||||||
|
.read(appSettingsProvider.notifier)
|
||||||
|
.setAndroidAssistantTrigger(option.value);
|
||||||
|
}
|
||||||
|
Navigator.of(sheetContext).pop();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}(),
|
||||||
|
if (i != options.length - 1) const Divider(height: 1),
|
||||||
|
],
|
||||||
|
const SizedBox(height: Spacing.lg),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildSttSection(
|
Widget _buildSttSection(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
WidgetRef ref,
|
WidgetRef ref,
|
||||||
|
|||||||
@@ -308,6 +308,11 @@
|
|||||||
"chatSettings": "Chat",
|
"chatSettings": "Chat",
|
||||||
"sendOnEnter": "Mit Enter senden",
|
"sendOnEnter": "Mit Enter senden",
|
||||||
"sendOnEnterDescription": "Enter sendet (Soft-Tastatur). Cmd/Ctrl+Enter ebenfalls verfügbar",
|
"sendOnEnterDescription": "Enter sendet (Soft-Tastatur). Cmd/Ctrl+Enter ebenfalls verfügbar",
|
||||||
|
"androidAssistantTitle": "Android digital assistant",
|
||||||
|
"androidAssistantDescription": "Choose what happens when you trigger the Android digital assistant.",
|
||||||
|
"androidAssistantOverlayOption": "Show quick overlay (default)",
|
||||||
|
"androidAssistantNewChatOption": "Open Conduit with a new chat",
|
||||||
|
"androidAssistantVoiceCallOption": "Start a voice call",
|
||||||
"sttSettings": "Sprache zu Text",
|
"sttSettings": "Sprache zu Text",
|
||||||
"sttEngineLabel": "Erkennungs-Engine",
|
"sttEngineLabel": "Erkennungs-Engine",
|
||||||
"sttEngineDevice": "Auf dem Gerät",
|
"sttEngineDevice": "Auf dem Gerät",
|
||||||
|
|||||||
@@ -1223,6 +1223,26 @@
|
|||||||
"@sendOnEnterDescription": {
|
"@sendOnEnterDescription": {
|
||||||
"description": "Explanation of how the Send on Enter toggle behaves."
|
"description": "Explanation of how the Send on Enter toggle behaves."
|
||||||
},
|
},
|
||||||
|
"androidAssistantTitle": "Android digital assistant",
|
||||||
|
"@androidAssistantTitle": {
|
||||||
|
"description": "Tile title for configuring the Android digital assistant trigger."
|
||||||
|
},
|
||||||
|
"androidAssistantDescription": "Choose what happens when you trigger the Android digital assistant.",
|
||||||
|
"@androidAssistantDescription": {
|
||||||
|
"description": "Helper text describing the Android digital assistant trigger setting."
|
||||||
|
},
|
||||||
|
"androidAssistantOverlayOption": "Show quick overlay (default)",
|
||||||
|
"@androidAssistantOverlayOption": {
|
||||||
|
"description": "Option label for keeping the current assistant overlay."
|
||||||
|
},
|
||||||
|
"androidAssistantNewChatOption": "Open Conduit with a new chat",
|
||||||
|
"@androidAssistantNewChatOption": {
|
||||||
|
"description": "Option label for opening the app to a fresh chat from the assistant trigger."
|
||||||
|
},
|
||||||
|
"androidAssistantVoiceCallOption": "Start a voice call",
|
||||||
|
"@androidAssistantVoiceCallOption": {
|
||||||
|
"description": "Option label for jumping straight into voice call from assistant trigger."
|
||||||
|
},
|
||||||
"sttSettings": "Speech to Text",
|
"sttSettings": "Speech to Text",
|
||||||
"@sttSettings": {
|
"@sttSettings": {
|
||||||
"description": "Section header for speech-to-text settings."
|
"description": "Section header for speech-to-text settings."
|
||||||
|
|||||||
@@ -308,6 +308,11 @@
|
|||||||
"chatSettings": "Conversación",
|
"chatSettings": "Conversación",
|
||||||
"sendOnEnter": "Enviar con Enter",
|
"sendOnEnter": "Enviar con Enter",
|
||||||
"sendOnEnterDescription": "Enter envía (teclado virtual). Cmd/Ctrl+Enter también disponible",
|
"sendOnEnterDescription": "Enter envía (teclado virtual). Cmd/Ctrl+Enter también disponible",
|
||||||
|
"androidAssistantTitle": "Android digital assistant",
|
||||||
|
"androidAssistantDescription": "Choose what happens when you trigger the Android digital assistant.",
|
||||||
|
"androidAssistantOverlayOption": "Show quick overlay (default)",
|
||||||
|
"androidAssistantNewChatOption": "Open Conduit with a new chat",
|
||||||
|
"androidAssistantVoiceCallOption": "Start a voice call",
|
||||||
"sttSettings": "Voz a texto",
|
"sttSettings": "Voz a texto",
|
||||||
"sttEngineLabel": "Motor de reconocimiento",
|
"sttEngineLabel": "Motor de reconocimiento",
|
||||||
"sttEngineDevice": "En el dispositivo",
|
"sttEngineDevice": "En el dispositivo",
|
||||||
|
|||||||
@@ -308,6 +308,11 @@
|
|||||||
"chatSettings": "Discussion",
|
"chatSettings": "Discussion",
|
||||||
"sendOnEnter": "Envoyer avec Entrée",
|
"sendOnEnter": "Envoyer avec Entrée",
|
||||||
"sendOnEnterDescription": "Entrée envoie (clavier logiciel). Cmd/Ctrl+Entrée aussi disponible",
|
"sendOnEnterDescription": "Entrée envoie (clavier logiciel). Cmd/Ctrl+Entrée aussi disponible",
|
||||||
|
"androidAssistantTitle": "Android digital assistant",
|
||||||
|
"androidAssistantDescription": "Choose what happens when you trigger the Android digital assistant.",
|
||||||
|
"androidAssistantOverlayOption": "Show quick overlay (default)",
|
||||||
|
"androidAssistantNewChatOption": "Open Conduit with a new chat",
|
||||||
|
"androidAssistantVoiceCallOption": "Start a voice call",
|
||||||
"sttSettings": "Voix vers texte",
|
"sttSettings": "Voix vers texte",
|
||||||
"sttEngineLabel": "Moteur de reconnaissance",
|
"sttEngineLabel": "Moteur de reconnaissance",
|
||||||
"sttEngineDevice": "Sur l’appareil",
|
"sttEngineDevice": "Sur l’appareil",
|
||||||
|
|||||||
@@ -308,6 +308,11 @@
|
|||||||
"chatSettings": "Chat",
|
"chatSettings": "Chat",
|
||||||
"sendOnEnter": "Invia con Invio",
|
"sendOnEnter": "Invia con Invio",
|
||||||
"sendOnEnterDescription": "Invio invia (tastiera software). Cmd/Ctrl+Invio disponibile",
|
"sendOnEnterDescription": "Invio invia (tastiera software). Cmd/Ctrl+Invio disponibile",
|
||||||
|
"androidAssistantTitle": "Android digital assistant",
|
||||||
|
"androidAssistantDescription": "Choose what happens when you trigger the Android digital assistant.",
|
||||||
|
"androidAssistantOverlayOption": "Show quick overlay (default)",
|
||||||
|
"androidAssistantNewChatOption": "Open Conduit with a new chat",
|
||||||
|
"androidAssistantVoiceCallOption": "Start a voice call",
|
||||||
"sttSettings": "Voce in testo",
|
"sttSettings": "Voce in testo",
|
||||||
"sttEngineLabel": "Motore di riconoscimento",
|
"sttEngineLabel": "Motore di riconoscimento",
|
||||||
"sttEngineDevice": "Sul dispositivo",
|
"sttEngineDevice": "Sul dispositivo",
|
||||||
|
|||||||
@@ -434,6 +434,11 @@
|
|||||||
"chatSettings": "채팅",
|
"chatSettings": "채팅",
|
||||||
"sendOnEnter": "Enter로 전송",
|
"sendOnEnter": "Enter로 전송",
|
||||||
"sendOnEnterDescription": "Enter로 전송 (소프트 키보드). Cmd/Ctrl+Enter도 사용 가능",
|
"sendOnEnterDescription": "Enter로 전송 (소프트 키보드). Cmd/Ctrl+Enter도 사용 가능",
|
||||||
|
"androidAssistantTitle": "Android digital assistant",
|
||||||
|
"androidAssistantDescription": "Choose what happens when you trigger the Android digital assistant.",
|
||||||
|
"androidAssistantOverlayOption": "Show quick overlay (default)",
|
||||||
|
"androidAssistantNewChatOption": "Open Conduit with a new chat",
|
||||||
|
"androidAssistantVoiceCallOption": "Start a voice call",
|
||||||
"sttSettings": "음성 텍스트 변환",
|
"sttSettings": "음성 텍스트 변환",
|
||||||
"sttEngineLabel": "인식 엔진",
|
"sttEngineLabel": "인식 엔진",
|
||||||
"sttEngineDevice": "기기에서",
|
"sttEngineDevice": "기기에서",
|
||||||
|
|||||||
@@ -308,6 +308,11 @@
|
|||||||
"chatSettings": "Chat",
|
"chatSettings": "Chat",
|
||||||
"sendOnEnter": "Verzenden met Enter",
|
"sendOnEnter": "Verzenden met Enter",
|
||||||
"sendOnEnterDescription": "Enter verzendt (softtoetsenbord). Cmd/Ctrl+Enter ook beschikbaar",
|
"sendOnEnterDescription": "Enter verzendt (softtoetsenbord). Cmd/Ctrl+Enter ook beschikbaar",
|
||||||
|
"androidAssistantTitle": "Android digital assistant",
|
||||||
|
"androidAssistantDescription": "Choose what happens when you trigger the Android digital assistant.",
|
||||||
|
"androidAssistantOverlayOption": "Show quick overlay (default)",
|
||||||
|
"androidAssistantNewChatOption": "Open Conduit with a new chat",
|
||||||
|
"androidAssistantVoiceCallOption": "Start a voice call",
|
||||||
"sttSettings": "Spraak naar tekst",
|
"sttSettings": "Spraak naar tekst",
|
||||||
"sttEngineLabel": "Herkenningsengine",
|
"sttEngineLabel": "Herkenningsengine",
|
||||||
"sttEngineDevice": "Op het apparaat",
|
"sttEngineDevice": "Op het apparaat",
|
||||||
|
|||||||
@@ -308,6 +308,11 @@
|
|||||||
"chatSettings": "Чат",
|
"chatSettings": "Чат",
|
||||||
"sendOnEnter": "Отправка по Enter",
|
"sendOnEnter": "Отправка по Enter",
|
||||||
"sendOnEnterDescription": "Enter отправляет (программная клавиатура). Также доступно Cmd/Ctrl+Enter",
|
"sendOnEnterDescription": "Enter отправляет (программная клавиатура). Также доступно Cmd/Ctrl+Enter",
|
||||||
|
"androidAssistantTitle": "Android digital assistant",
|
||||||
|
"androidAssistantDescription": "Choose what happens when you trigger the Android digital assistant.",
|
||||||
|
"androidAssistantOverlayOption": "Show quick overlay (default)",
|
||||||
|
"androidAssistantNewChatOption": "Open Conduit with a new chat",
|
||||||
|
"androidAssistantVoiceCallOption": "Start a voice call",
|
||||||
"sttSettings": "Речь в текст",
|
"sttSettings": "Речь в текст",
|
||||||
"sttEngineLabel": "Движок распознавания",
|
"sttEngineLabel": "Движок распознавания",
|
||||||
"sttEngineDevice": "На устройстве",
|
"sttEngineDevice": "На устройстве",
|
||||||
|
|||||||
@@ -308,6 +308,11 @@
|
|||||||
"chatSettings": "对话",
|
"chatSettings": "对话",
|
||||||
"sendOnEnter": "回车发送",
|
"sendOnEnter": "回车发送",
|
||||||
"sendOnEnterDescription": "回车发送(软键盘)。Cmd/Ctrl+Enter 也可用",
|
"sendOnEnterDescription": "回车发送(软键盘)。Cmd/Ctrl+Enter 也可用",
|
||||||
|
"androidAssistantTitle": "Android digital assistant",
|
||||||
|
"androidAssistantDescription": "Choose what happens when you trigger the Android digital assistant.",
|
||||||
|
"androidAssistantOverlayOption": "Show quick overlay (default)",
|
||||||
|
"androidAssistantNewChatOption": "Open Conduit with a new chat",
|
||||||
|
"androidAssistantVoiceCallOption": "Start a voice call",
|
||||||
"sttSettings": "语音转文字",
|
"sttSettings": "语音转文字",
|
||||||
"sttEngineLabel": "识别引擎",
|
"sttEngineLabel": "识别引擎",
|
||||||
"sttEngineDevice": "本机",
|
"sttEngineDevice": "本机",
|
||||||
|
|||||||
@@ -308,6 +308,11 @@
|
|||||||
"chatSettings": "對話",
|
"chatSettings": "對話",
|
||||||
"sendOnEnter": "回車發送",
|
"sendOnEnter": "回車發送",
|
||||||
"sendOnEnterDescription": "回車發送(軟鍵盤)。Cmd/Ctrl+Enter 也可用",
|
"sendOnEnterDescription": "回車發送(軟鍵盤)。Cmd/Ctrl+Enter 也可用",
|
||||||
|
"androidAssistantTitle": "Android digital assistant",
|
||||||
|
"androidAssistantDescription": "Choose what happens when you trigger the Android digital assistant.",
|
||||||
|
"androidAssistantOverlayOption": "Show quick overlay (default)",
|
||||||
|
"androidAssistantNewChatOption": "Open Conduit with a new chat",
|
||||||
|
"androidAssistantVoiceCallOption": "Start a voice call",
|
||||||
"sttSettings": "語音轉文字",
|
"sttSettings": "語音轉文字",
|
||||||
"sttEngineLabel": "識別引擎",
|
"sttEngineLabel": "識別引擎",
|
||||||
"sttEngineDevice": "本機",
|
"sttEngineDevice": "本機",
|
||||||
|
|||||||
Reference in New Issue
Block a user