fix: resolve black camera preview and permission issues in assistant overlay
- Use PreviewView COMPATIBLE mode (TextureView) for overlay window rendering - Add generic PermissionRequestActivity for runtime permission requests from Service context, with broadcast-based result notification - Auto-enable camera after permission grant via BroadcastReceiver - Fix camera bind/unbind race condition with PreviewView tracking - Change AssistantActivity theme to Translucent for reliable permission dialogs Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
f94e299f65
commit
e21e2e90ef
5 changed files with 147 additions and 21 deletions
|
|
@ -58,13 +58,21 @@
|
|||
android:excludeFromRecents="true"
|
||||
android:taskAffinity=""
|
||||
android:launchMode="singleTask"
|
||||
android:theme="@android:style/Theme.NoDisplay">
|
||||
android:theme="@android:style/Theme.Translucent.NoTitleBar">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.ASSIST" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".PermissionRequestActivity"
|
||||
android:exported="false"
|
||||
android:excludeFromRecents="true"
|
||||
android:taskAffinity=""
|
||||
android:launchMode="singleTop"
|
||||
android:theme="@android:style/Theme.Translucent.NoTitleBar" />
|
||||
|
||||
<service
|
||||
android:name=".assistant.AssistantService"
|
||||
android:exported="false"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,59 @@
|
|||
package io.picoclaw.android
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.core.content.ContextCompat
|
||||
|
||||
class PermissionRequestActivity : ComponentActivity() {
|
||||
|
||||
private val launcher = registerForActivityResult(
|
||||
ActivityResultContracts.RequestPermission()
|
||||
) { granted ->
|
||||
broadcastResult(
|
||||
intent.getStringExtra(EXTRA_PERMISSION).orEmpty(),
|
||||
granted
|
||||
)
|
||||
finish()
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
val permission = intent.getStringExtra(EXTRA_PERMISSION)
|
||||
if (permission == null) {
|
||||
finish()
|
||||
return
|
||||
}
|
||||
if (ContextCompat.checkSelfPermission(this, permission)
|
||||
== PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
broadcastResult(permission, true)
|
||||
finish()
|
||||
return
|
||||
}
|
||||
launcher.launch(permission)
|
||||
}
|
||||
|
||||
private fun broadcastResult(permission: String, granted: Boolean) {
|
||||
sendBroadcast(
|
||||
Intent(ACTION_RESULT)
|
||||
.setPackage(packageName)
|
||||
.putExtra(EXTRA_PERMISSION, permission)
|
||||
.putExtra(EXTRA_GRANTED, granted)
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val EXTRA_PERMISSION = "permission"
|
||||
const val EXTRA_GRANTED = "granted"
|
||||
const val ACTION_RESULT = "io.picoclaw.android.PERMISSION_RESULT"
|
||||
|
||||
fun intent(context: Context, permission: String): Intent =
|
||||
Intent(context, PermissionRequestActivity::class.java)
|
||||
.putExtra(EXTRA_PERMISSION, permission)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,12 @@
|
|||
package io.picoclaw.android.assistant
|
||||
|
||||
import android.Manifest
|
||||
import android.app.Notification
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.pm.ServiceInfo
|
||||
import android.graphics.PixelFormat
|
||||
import android.os.IBinder
|
||||
|
|
@ -20,6 +25,7 @@ import androidx.compose.ui.layout.onGloballyPositioned
|
|||
import androidx.compose.ui.layout.positionInWindow
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.LifecycleService
|
||||
import androidx.lifecycle.setViewTreeLifecycleOwner
|
||||
import androidx.savedstate.SavedStateRegistry
|
||||
|
|
@ -27,6 +33,7 @@ import androidx.savedstate.SavedStateRegistryController
|
|||
import androidx.savedstate.SavedStateRegistryOwner
|
||||
import androidx.savedstate.setViewTreeSavedStateRegistryOwner
|
||||
import io.ktor.client.HttpClient
|
||||
import io.picoclaw.android.PermissionRequestActivity
|
||||
import io.picoclaw.android.core.data.remote.WebSocketClient
|
||||
import io.picoclaw.android.core.data.repository.AssistantConnectionImpl
|
||||
import io.picoclaw.android.core.domain.repository.AssistantConnection
|
||||
|
|
@ -63,6 +70,16 @@ class AssistantService : LifecycleService(), SavedStateRegistryOwner {
|
|||
override val savedStateRegistry: SavedStateRegistry
|
||||
get() = savedStateRegistryController.savedStateRegistry
|
||||
|
||||
private val permissionReceiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
val permission = intent.getStringExtra(PermissionRequestActivity.EXTRA_PERMISSION)
|
||||
val granted = intent.getBooleanExtra(PermissionRequestActivity.EXTRA_GRANTED, false)
|
||||
if (permission == Manifest.permission.CAMERA && granted) {
|
||||
assistantManager.toggleCamera()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
savedStateRegistryController.performAttach()
|
||||
savedStateRegistryController.performRestore(null)
|
||||
|
|
@ -83,6 +100,13 @@ class AssistantService : LifecycleService(), SavedStateRegistryOwner {
|
|||
cameraCaptureManager = cameraCaptureManager,
|
||||
contentResolver = contentResolver
|
||||
)
|
||||
|
||||
ContextCompat.registerReceiver(
|
||||
this,
|
||||
permissionReceiver,
|
||||
IntentFilter(PermissionRequestActivity.ACTION_RESULT),
|
||||
ContextCompat.RECEIVER_NOT_EXPORTED
|
||||
)
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
|
|
@ -106,6 +130,7 @@ class AssistantService : LifecycleService(), SavedStateRegistryOwner {
|
|||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
unregisterReceiver(permissionReceiver)
|
||||
removeOverlay()
|
||||
assistantManager.destroy()
|
||||
ttsWrapper.destroy()
|
||||
|
|
@ -119,6 +144,20 @@ class AssistantService : LifecycleService(), SavedStateRegistryOwner {
|
|||
return null
|
||||
}
|
||||
|
||||
private fun handleCameraToggle() {
|
||||
if (assistantManager.state.value.isCameraActive) {
|
||||
assistantManager.toggleCamera()
|
||||
return
|
||||
}
|
||||
if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
|
||||
== PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
assistantManager.toggleCamera()
|
||||
} else {
|
||||
startActivity(PermissionRequestActivity.intent(this, Manifest.permission.CAMERA))
|
||||
}
|
||||
}
|
||||
|
||||
private fun shutdown() {
|
||||
stopForeground(STOP_FOREGROUND_REMOVE)
|
||||
stopSelf()
|
||||
|
|
@ -168,7 +207,7 @@ class AssistantService : LifecycleService(), SavedStateRegistryOwner {
|
|||
state = state,
|
||||
onClose = { shutdown() },
|
||||
onInterrupt = { assistantManager.interrupt() },
|
||||
onCameraToggle = { assistantManager.toggleCamera() },
|
||||
onCameraToggle = { handleCameraToggle() },
|
||||
cameraCaptureManager = cameraCaptureManager,
|
||||
modifier = Modifier.onGloballyPositioned { coordinates ->
|
||||
wrapper.contentTop = coordinates.positionInWindow().y.toInt()
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package io.picoclaw.android.feature.chat.assistant
|
||||
|
||||
import androidx.camera.view.PreviewView
|
||||
import androidx.camera.view.PreviewView.ImplementationMode
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.animation.core.LinearEasing
|
||||
|
|
@ -104,8 +105,9 @@ fun AssistantPillBar(
|
|||
) {
|
||||
AndroidView(
|
||||
factory = { ctx ->
|
||||
PreviewView(ctx).also { preview ->
|
||||
cameraCaptureManager.bind(lifecycleOwner, preview)
|
||||
PreviewView(ctx).apply {
|
||||
implementationMode = ImplementationMode.COMPATIBLE
|
||||
cameraCaptureManager.bind(lifecycleOwner, this)
|
||||
}
|
||||
},
|
||||
modifier = Modifier.fillMaxSize()
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package io.picoclaw.android.feature.chat.voice
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import androidx.camera.core.CameraSelector
|
||||
import androidx.camera.core.ImageCapture
|
||||
import androidx.camera.core.ImageCaptureException
|
||||
|
|
@ -18,10 +19,19 @@ class CameraCaptureManager(private val context: Context) {
|
|||
|
||||
private var imageCapture: ImageCapture? = null
|
||||
private var cameraProvider: ProcessCameraProvider? = null
|
||||
private var currentPreviewView: PreviewView? = null
|
||||
|
||||
fun bind(lifecycleOwner: LifecycleOwner, previewView: PreviewView) {
|
||||
if (currentPreviewView === previewView && cameraProvider != null) return
|
||||
unbind()
|
||||
currentPreviewView = previewView
|
||||
|
||||
val providerFuture = ProcessCameraProvider.getInstance(context)
|
||||
providerFuture.addListener({
|
||||
try {
|
||||
// Stale callback – unbind() was called while waiting
|
||||
if (currentPreviewView !== previewView) return@addListener
|
||||
|
||||
val provider = providerFuture.get()
|
||||
cameraProvider = provider
|
||||
|
||||
|
|
@ -41,6 +51,9 @@ class CameraCaptureManager(private val context: Context) {
|
|||
preview,
|
||||
capture
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Failed to bind camera", e)
|
||||
}
|
||||
}, ContextCompat.getMainExecutor(context))
|
||||
}
|
||||
|
||||
|
|
@ -48,6 +61,7 @@ class CameraCaptureManager(private val context: Context) {
|
|||
cameraProvider?.unbindAll()
|
||||
cameraProvider = null
|
||||
imageCapture = null
|
||||
currentPreviewView = null
|
||||
}
|
||||
|
||||
suspend fun captureFrame(): ImageAttachment? {
|
||||
|
|
@ -77,4 +91,8 @@ class CameraCaptureManager(private val context: Context) {
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "CameraCaptureManager"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue