Merge pull request #39 from yuga-hashimoto/fix/permission-handling-and-release-config

Improve mic permission handling and release config
This commit is contained in:
Yu-ga
2026-02-15 01:50:31 +09:00
committed by GitHub
6 changed files with 137 additions and 21 deletions
+1 -1
View File
@@ -69,7 +69,7 @@ jobs:
files: |
app/build/outputs/apk/release/OpenClawAssistant-${{ github.ref_name }}.apk
app/build/outputs/bundle/release/OpenClawAssistant-${{ github.ref_name }}.aab
draft: true
draft: false
prerelease: false
- name: Upload Artifacts (Non-tag push)
@@ -1,13 +1,17 @@
package com.openclaw.assistant
import android.Manifest
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Bundle
import android.provider.Settings
import android.speech.tts.TextToSpeech
import android.util.Log
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
@@ -63,6 +67,16 @@ class ChatActivity : ComponentActivity(), TextToSpeech.OnInitListener {
private var isRetry = false
private lateinit var settings: SettingsRepository
private val permissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { granted ->
if (!granted) {
if (!ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.RECORD_AUDIO)) {
showPermissionSettingsDialog()
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
settings = SettingsRepository.getInstance(this)
@@ -72,13 +86,9 @@ class ChatActivity : ComponentActivity(), TextToSpeech.OnInitListener {
initializeTTS()
// Request Microphone permission if not granted
if (ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO)
if (ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO)
!= PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(
this,
arrayOf(Manifest.permission.RECORD_AUDIO),
REQUEST_RECORD_AUDIO
)
permissionLauncher.launch(Manifest.permission.RECORD_AUDIO)
}
setContent {
@@ -97,7 +107,7 @@ class ChatActivity : ComponentActivity(), TextToSpeech.OnInitListener {
if (checkPermission()) {
viewModel.startListening()
} else {
Toast.makeText(this, getString(R.string.mic_permission_required), Toast.LENGTH_SHORT).show()
requestMicPermissionForListening()
}
},
onStopListening = { viewModel.stopListening() },
@@ -157,8 +167,26 @@ class ChatActivity : ComponentActivity(), TextToSpeech.OnInitListener {
return ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED
}
companion object {
private const val REQUEST_RECORD_AUDIO = 200
private fun requestMicPermissionForListening() {
if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.RECORD_AUDIO)) {
permissionLauncher.launch(Manifest.permission.RECORD_AUDIO)
} else if (!checkPermission()) {
// First-time request or permanently denied
permissionLauncher.launch(Manifest.permission.RECORD_AUDIO)
}
}
private fun showPermissionSettingsDialog() {
android.app.AlertDialog.Builder(this)
.setTitle(getString(R.string.mic_permission_required))
.setMessage(getString(R.string.mic_permission_denied_permanently))
.setPositiveButton(getString(R.string.open_settings)) { _, _ ->
startActivity(Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
data = Uri.fromParts("package", packageName, null)
})
}
.setNegativeButton(getString(R.string.cancel), null)
.show()
}
}
@@ -3,6 +3,7 @@ package com.openclaw.assistant
import android.Manifest
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.provider.Settings
@@ -55,13 +56,30 @@ class MainActivity : ComponentActivity(), TextToSpeech.OnInitListener {
private lateinit var settings: SettingsRepository
private var tts: TextToSpeech? = null
private var voiceDiagnostic by mutableStateOf<VoiceDiagnostic?>(null)
private var pendingHotwordStart = false
private val permissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestMultiplePermissions()
) { permissions ->
val allGranted = permissions.values.all { it }
if (!allGranted) {
Toast.makeText(this, getString(R.string.permissions_required), Toast.LENGTH_SHORT).show()
val recordAudioGranted = permissions[Manifest.permission.RECORD_AUDIO] ?: false
if (pendingHotwordStart) {
pendingHotwordStart = false
if (recordAudioGranted) {
settings.hotwordEnabled = true
HotwordService.start(this)
Toast.makeText(this, getString(R.string.hotword_started), Toast.LENGTH_SHORT).show()
} else {
if (!ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.RECORD_AUDIO)) {
showPermissionSettingsDialog()
} else {
Toast.makeText(this, getString(R.string.mic_permission_required), Toast.LENGTH_SHORT).show()
}
}
} else {
val allGranted = permissions.values.all { it }
if (!allGranted) {
Toast.makeText(this, getString(R.string.permissions_required), Toast.LENGTH_SHORT).show()
}
}
}
@@ -137,14 +155,19 @@ class MainActivity : ComponentActivity(), TextToSpeech.OnInitListener {
private fun toggleHotwordService(enabled: Boolean) {
if (enabled) {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO)
!= PackageManager.PERMISSION_GRANTED) {
== PackageManager.PERMISSION_GRANTED) {
settings.hotwordEnabled = true
HotwordService.start(this)
Toast.makeText(this, getString(R.string.hotword_started), Toast.LENGTH_SHORT).show()
} else if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.RECORD_AUDIO)) {
pendingHotwordStart = true
Toast.makeText(this, getString(R.string.mic_permission_required), Toast.LENGTH_SHORT).show()
permissionLauncher.launch(arrayOf(Manifest.permission.RECORD_AUDIO))
return
} else {
// First-time request or permanently denied: launch and decide in callback
pendingHotwordStart = true
permissionLauncher.launch(arrayOf(Manifest.permission.RECORD_AUDIO))
}
settings.hotwordEnabled = true
HotwordService.start(this)
Toast.makeText(this, getString(R.string.hotword_started), Toast.LENGTH_SHORT).show()
} else {
settings.hotwordEnabled = false
HotwordService.stop(this)
@@ -152,6 +175,22 @@ class MainActivity : ComponentActivity(), TextToSpeech.OnInitListener {
}
}
private fun showPermissionSettingsDialog() {
android.app.AlertDialog.Builder(this)
.setTitle(getString(R.string.mic_permission_required))
.setMessage(getString(R.string.mic_permission_denied_permanently))
.setPositiveButton(getString(R.string.open_settings)) { _, _ -> openAppSettings() }
.setNegativeButton(getString(R.string.cancel), null)
.show()
}
private fun openAppSettings() {
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
data = Uri.fromParts("package", packageName, null)
}
startActivity(intent)
}
fun isAssistantActive(): Boolean {
return try {
Settings.Secure.getString(contentResolver, "assistant")?.contains(packageName) == true
@@ -1,9 +1,12 @@
package com.openclaw.assistant.speech
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Bundle
import android.speech.RecognitionListener
import android.speech.RecognitionService
import android.speech.RecognizerIntent
import android.speech.SpeechRecognizer
import kotlinx.coroutines.channels.awaitClose
@@ -44,9 +47,13 @@ class SpeechRecognizerManager(private val context: Context) {
// Ensure creation on Main thread
withContext(Dispatchers.Main) {
if (recognizer == null && SpeechRecognizer.isRecognitionAvailable(context)) {
// Use application context to avoid activity/service lifecycle leaks
val appContext = context.applicationContext
recognizer = SpeechRecognizer.createSpeechRecognizer(appContext)
val serviceComponent = findRecognitionService(appContext)
recognizer = if (serviceComponent != null) {
SpeechRecognizer.createSpeechRecognizer(appContext, serviceComponent)
} else {
SpeechRecognizer.createSpeechRecognizer(appContext)
}
}
}
}
@@ -177,7 +184,7 @@ class SpeechRecognizerManager(private val context: Context) {
/**
* Completely destroy the recognizer resources
*/
fun destroy() {
fun destroy() {
try {
recognizer?.destroy()
} catch (e: Exception) {
@@ -185,6 +192,38 @@ class SpeechRecognizerManager(private val context: Context) {
}
recognizer = null
}
/**
* Find a real speech recognition service, skipping our own stub service.
* This app registers a no-op RecognitionService (required for VoiceInteractionService),
* which some devices may select as the default, breaking SpeechRecognizer.
*/
private fun findRecognitionService(context: Context): ComponentName? {
val pm = context.packageManager
val services = pm.queryIntentServices(
Intent(RecognitionService.SERVICE_INTERFACE),
PackageManager.GET_META_DATA
)
val ownPackage = context.packageName
// Prefer Google's service
val google = services.firstOrNull {
it.serviceInfo.packageName == "com.google.android.googlequicksearchbox"
}
if (google != null) {
return ComponentName(google.serviceInfo.packageName, google.serviceInfo.name)
}
// Use any other service that is NOT our own stub
val other = services.firstOrNull { it.serviceInfo.packageName != ownPackage }
if (other != null) {
return ComponentName(other.serviceInfo.packageName, other.serviceInfo.name)
}
// No external service found; fall back to default
return null
}
}
/**
+5
View File
@@ -111,6 +111,11 @@
<string name="connection_status_disconnected">未接続</string>
<string name="connection_status_reconnecting">再接続中…</string>
<!-- Permission handling -->
<string name="mic_permission_denied_permanently">マイクの権限が永久に拒否されています。ウェイクワード検知を使用するには、アプリの設定から権限を有効にしてください。</string>
<string name="open_settings">設定を開く</string>
<string name="cancel">キャンセル</string>
<!-- Chat UI -->
<string name="conversations_title">会話</string>
<string name="new_chat">新規チャット</string>
+5
View File
@@ -135,6 +135,11 @@
<string name="connection_status_disconnected">Disconnected</string>
<string name="connection_status_reconnecting">Reconnecting…</string>
<!-- Permission handling -->
<string name="mic_permission_denied_permanently">Microphone permission has been permanently denied. Please enable it in app settings to use wake word detection.</string>
<string name="open_settings">Open Settings</string>
<string name="cancel">Cancel</string>
<!-- Chat UI -->
<string name="conversations_title">Conversations</string>
<string name="new_chat">New Chat</string>