mirror of
https://github.com/yuga-hashimoto/openclaw-assistant.git
synced 2026-08-14 08:35:19 +00:00
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:
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user