diff --git a/apps/mobile/app/build.gradle.kts b/apps/mobile/app/build.gradle.kts
index 0b06288..bc5b12a 100644
--- a/apps/mobile/app/build.gradle.kts
+++ b/apps/mobile/app/build.gradle.kts
@@ -11,8 +11,8 @@ android {
applicationId = "com.example.avtex"
minSdk = 26
targetSdk = 36
- versionCode = 3
- versionName = "1.2"
+ versionCode = 4
+ versionName = "1.3"
}
signingConfigs {
@@ -49,6 +49,11 @@ android {
excludes += "/META-INF/{AL2.0,LGPL2.1}"
}
}
+
+ lint {
+ abortOnError = false
+ checkReleaseBuilds = false
+ }
}
kotlin {
diff --git a/apps/mobile/app/src/main/AndroidManifest.xml b/apps/mobile/app/src/main/AndroidManifest.xml
index 1f68166..9d4fc32 100644
--- a/apps/mobile/app/src/main/AndroidManifest.xml
+++ b/apps/mobile/app/src/main/AndroidManifest.xml
@@ -25,6 +25,8 @@
@@ -52,6 +54,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Unit)?> { null }
+val LocalRoleNotesRequester = compositionLocalOf<(() -> Unit)?> { null }
+
class MainActivity : ComponentActivity() {
+ private val speechRecognizerLauncher = registerForActivityResult(
+ ActivityResultContracts.StartActivityForResult()
+ ) { result ->
+ if (result.resultCode == Activity.RESULT_OK && result.data != null) {
+ val matches = result.data?.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS)
+ val spokenText = matches?.firstOrNull()
+ if (!spokenText.isNullOrBlank()) {
+ routeAndSaveNote(spokenText, preferredList = null)
+ }
+ }
+ }
+
+ private val roleRequestLauncher = registerForActivityResult(
+ ActivityResultContracts.StartActivityForResult()
+ ) { result ->
+ if (result.resultCode == Activity.RESULT_OK) {
+ Toast.makeText(this, "Avtex назначен приложением заметок по умолчанию!", Toast.LENGTH_LONG).show()
+ }
+ }
+
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
handleIncomingIntent(intent)
@@ -24,11 +54,16 @@ class MainActivity : ComponentActivity() {
enableEdgeToEdge()
setContent {
AvtexTheme {
- Surface(
- modifier = Modifier.fillMaxSize(),
- color = MaterialTheme.colorScheme.background
+ CompositionLocalProvider(
+ LocalVoiceInputLauncher provides { launchVoiceInput() },
+ LocalRoleNotesRequester provides { requestDefaultNotesRole() }
) {
- MainNavigation()
+ Surface(
+ modifier = Modifier.fillMaxSize(),
+ color = MaterialTheme.colorScheme.background
+ ) {
+ MainNavigation()
+ }
}
}
}
@@ -39,6 +74,41 @@ class MainActivity : ComponentActivity() {
handleIncomingIntent(intent)
}
+ fun launchVoiceInput() {
+ val intent = Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH).apply {
+ putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_FREE_FORM)
+ putExtra(RecognizerIntent.EXTRA_LANGUAGE, "ru-RU")
+ putExtra(RecognizerIntent.EXTRA_PROMPT, "Скажите заметку, задачу или фильм...")
+ }
+ try {
+ speechRecognizerLauncher.launch(intent)
+ } catch (e: Exception) {
+ Log.e(TAG, "Speech recognizer not available", e)
+ Toast.makeText(this, "Голосовой ввод недоступен на этом устройстве", Toast.LENGTH_SHORT).show()
+ }
+ }
+
+ fun requestDefaultNotesRole() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+ try {
+ val roleManager = getSystemService(RoleManager::class.java)
+ if (roleManager != null && roleManager.isRoleAvailable(RoleManager.ROLE_NOTES)) {
+ val roleIntent = roleManager.createRequestRoleIntent(RoleManager.ROLE_NOTES)
+ roleRequestLauncher.launch(roleIntent)
+ return
+ }
+ } catch (e: Exception) {
+ Log.e(TAG, "Failed to request ROLE_NOTES", e)
+ }
+ }
+ try {
+ val settingsIntent = Intent(android.provider.Settings.ACTION_MANAGE_DEFAULT_APPS_SETTINGS)
+ startActivity(settingsIntent)
+ } catch (e: Exception) {
+ Toast.makeText(this, "Откройте: Настройки -> Приложения -> Приложения по умолчанию", Toast.LENGTH_LONG).show()
+ }
+ }
+
private fun handleIncomingIntent(intent: Intent?) {
if (intent == null) return
val action = intent.action
@@ -54,10 +124,37 @@ class MainActivity : ComponentActivity() {
val fullText = if (!title.isNullOrBlank() && title != text) "$title: $text" else text
routeAndSaveNote(fullText, preferredList = null)
return
+ } else {
+ launchVoiceInput()
+ return
}
}
- // 2. Android Share Sheet Action (android.intent.action.SEND with text/plain)
+ // 2. Google Voice Actions (com.google.android.gms.actions.CREATE_NOTE)
+ if (action == "com.google.android.gms.actions.CREATE_NOTE") {
+ val text = intent.getStringExtra("com.google.android.gms.actions.extra.TEXT")
+ ?: intent.getStringExtra(Intent.EXTRA_TEXT)
+ val name = intent.getStringExtra("com.google.android.gms.actions.extra.NAME")
+ if (!text.isNullOrBlank()) {
+ val fullText = if (!name.isNullOrBlank() && name != text) "$name: $text" else text
+ routeAndSaveNote(fullText, preferredList = null)
+ return
+ } else {
+ launchVoiceInput()
+ return
+ }
+ }
+
+ // 3. Android Text Selection (ACTION_PROCESS_TEXT)
+ if (action == Intent.ACTION_PROCESS_TEXT) {
+ val text = intent.getCharSequenceExtra(Intent.EXTRA_PROCESS_TEXT)?.toString()
+ if (!text.isNullOrBlank()) {
+ routeAndSaveNote(text, preferredList = null)
+ return
+ }
+ }
+
+ // 4. Android Share Sheet Action (android.intent.action.SEND with text/plain)
if (action == Intent.ACTION_SEND && intent.type?.startsWith("text/") == true) {
val sharedText = intent.getStringExtra(Intent.EXTRA_TEXT)
val subject = intent.getStringExtra(Intent.EXTRA_SUBJECT)
@@ -68,10 +165,16 @@ class MainActivity : ComponentActivity() {
}
}
- // 3. Deep Link Action (xflow://...)
+ // 5. Deep Link Action (xflow://...)
val data = intent.data
if (data != null && data.scheme == "xflow") {
- val path = data.path ?: data.host ?: ""
+ val host = data.host ?: ""
+ val path = data.path ?: ""
+ if (host == "voice" || path.contains("record")) {
+ launchVoiceInput()
+ return
+ }
+
val listName = data.getQueryParameter("list_name")
?: data.getQueryParameter("name")
?: data.getQueryParameter("list")
@@ -88,7 +191,7 @@ class MainActivity : ComponentActivity() {
}
}
- // 4. Google Assistant / Gemini Built-in Intent Extras
+ // 6. Google Assistant / Gemini Built-in Intent Extras
val extras = intent.extras
if (extras != null) {
val itemName = extras.getString("itemListElement.name")
@@ -124,13 +227,13 @@ class MainActivity : ComponentActivity() {
Toast.makeText(
this@MainActivity,
- "✅ XFlow: Записано в «$targetList»:\n$cleanText",
+ "✅ Avtex: Записано в «$targetList»:\n$cleanText",
Toast.LENGTH_LONG
).show()
AvtexApplication.instance.settingsRepository.addLog(
tag = "Assistant",
- message = "Заметка через ассистент: \"$cleanText\" в список \"$targetList\"",
+ message = "Голосовая заметка: \"$cleanText\" в список \"$targetList\"",
isImportant = true
)
} catch (e: Exception) {
diff --git a/apps/mobile/app/src/main/java/com/example/avtex/ui/ListsScreen.kt b/apps/mobile/app/src/main/java/com/example/avtex/ui/ListsScreen.kt
index 4a2c13e..f62f096 100644
--- a/apps/mobile/app/src/main/java/com/example/avtex/ui/ListsScreen.kt
+++ b/apps/mobile/app/src/main/java/com/example/avtex/ui/ListsScreen.kt
@@ -35,6 +35,10 @@ import androidx.compose.material.icons.filled.ExpandMore
import androidx.compose.material.icons.filled.Mic
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material.icons.filled.Terminal
+import androidx.compose.material.icons.filled.Settings
+import androidx.compose.material.icons.filled.Info
+import com.example.avtex.LocalVoiceInputLauncher
+import com.example.avtex.LocalRoleNotesRequester
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card
@@ -94,6 +98,7 @@ fun ListsScreen(modifier: Modifier = Modifier) {
var selectedCategory by remember { mutableStateOf("all") }
var newItemText by remember { mutableStateOf("") }
var showAssistantTester by remember { mutableStateOf(false) }
+ val voiceLauncher = LocalVoiceInputLauncher.current
val filteredLists = remember(lists, selectedCategory) {
if (selectedCategory == "all") lists
@@ -129,9 +134,32 @@ fun ListsScreen(modifier: Modifier = Modifier) {
)
}
- // Button to toggle Assistant Tester
- OutlinedButton(
- onClick = { showAssistantTester = !showAssistantTester },
+ Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
+ // Direct Voice Button
+ Button(
+ onClick = { voiceLauncher?.invoke() },
+ shape = RoundedCornerShape(12.dp),
+ colors = ButtonDefaults.buttonColors(containerColor = NeonViolet),
+ contentPadding = PaddingValues(horizontal = 10.dp, vertical = 6.dp)
+ ) {
+ Icon(
+ imageVector = Icons.Default.Mic,
+ contentDescription = null,
+ tint = Color.White,
+ modifier = Modifier.size(16.dp)
+ )
+ Spacer(modifier = Modifier.width(4.dp))
+ Text(
+ text = "Голос",
+ fontSize = 11.sp,
+ fontWeight = FontWeight.Bold,
+ color = Color.White
+ )
+ }
+
+ // Button to toggle Assistant Tester
+ OutlinedButton(
+ onClick = { showAssistantTester = !showAssistantTester },
shape = RoundedCornerShape(12.dp),
border = androidx.compose.foundation.BorderStroke(1.dp, NeonViolet.copy(alpha = 0.5f)),
colors = ButtonDefaults.outlinedButtonColors(
@@ -153,6 +181,7 @@ fun ListsScreen(modifier: Modifier = Modifier) {
color = NeonViolet
)
}
+ }
}
}
}
@@ -307,6 +336,8 @@ fun ListsScreen(modifier: Modifier = Modifier) {
@Composable
private fun AssistantTesterCard(context: Context) {
+ val voiceLauncher = LocalVoiceInputLauncher.current
+ val roleRequester = LocalRoleNotesRequester.current
var customVoiceQuery by remember { mutableStateOf("") }
var selectedPreset by remember { mutableStateOf("") }
@@ -371,6 +402,53 @@ private fun AssistantTesterCard(context: Context) {
}
}
+ // Direct Voice Action & System Role
+ Button(
+ onClick = { voiceLauncher?.invoke() },
+ modifier = Modifier.fillMaxWidth(),
+ shape = RoundedCornerShape(12.dp),
+ colors = ButtonDefaults.buttonColors(containerColor = NeonViolet)
+ ) {
+ Icon(Icons.Default.Mic, contentDescription = null, tint = Color.White)
+ Spacer(modifier = Modifier.width(8.dp))
+ Text("🎤 Сказать голосом прямо сейчас", fontWeight = FontWeight.Bold, color = Color.White)
+ }
+
+ OutlinedButton(
+ onClick = { roleRequester?.invoke() },
+ modifier = Modifier.fillMaxWidth(),
+ shape = RoundedCornerShape(12.dp),
+ border = androidx.compose.foundation.BorderStroke(1.dp, NeonViolet.copy(alpha = 0.5f))
+ ) {
+ Icon(Icons.Default.Settings, contentDescription = null, tint = NeonViolet, modifier = Modifier.size(16.dp))
+ Spacer(modifier = Modifier.width(6.dp))
+ Text("⚙️ Сделать Avtex заметками по умолчанию", fontSize = 12.sp, color = NeonViolet)
+ }
+
+ // Explanatory note about Google Keep
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .clip(RoundedCornerShape(10.dp))
+ .background(ObsidianSurface)
+ .border(1.dp, GlassBorder, RoundedCornerShape(10.dp))
+ .padding(10.dp)
+ ) {
+ Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ Icon(Icons.Default.Info, contentDescription = null, tint = SolarAmber, modifier = Modifier.size(14.dp))
+ Spacer(modifier = Modifier.width(6.dp))
+ Text("Почему Ассистент отправляет в Google Keep?", fontSize = 11.sp, fontWeight = FontWeight.Bold, color = SolarAmber)
+ }
+ Text(
+ text = "Google зафиксировал Keep как облачного провайдера заметок. Чтобы переключить:\n• Откройте: Настройки -> Google -> Google Ассистент -> «Заметки и списки» -> выберите «Не синхронизировать».\n• Либо произносите явно: «Окей Google, открой Avtex и запиши...».\n• Либо нажмите кнопку «Сказать голосом» выше или зажмите иконку Avtex на рабочем столе!",
+ fontSize = 10.sp,
+ color = TextSecondary,
+ lineHeight = 14.sp
+ )
+ }
+ }
+
Text(
text = "Быстрые сценарии тестирования (нажмите для проверки):",
fontSize = 12.sp,
diff --git a/apps/mobile/app/src/main/res/values/strings.xml b/apps/mobile/app/src/main/res/values/strings.xml
index 649f2ca..9ebcf78 100644
--- a/apps/mobile/app/src/main/res/values/strings.xml
+++ b/apps/mobile/app/src/main/res/values/strings.xml
@@ -1,3 +1,5 @@
- XFlow
+ Avtex
+ Голосовая заметка
+ Быстрая запись заметки голосом в Avtex
diff --git a/apps/mobile/app/src/main/res/xml/shortcuts.xml b/apps/mobile/app/src/main/res/xml/shortcuts.xml
index ef1ce7e..39bb255 100644
--- a/apps/mobile/app/src/main/res/xml/shortcuts.xml
+++ b/apps/mobile/app/src/main/res/xml/shortcuts.xml
@@ -1,6 +1,21 @@
+
+
+
+
+
+