Skip to content

Commit 549a132

Browse files
I need to request foreground service permission for media projection.
1 parent 8a11629 commit 549a132

6 files changed

Lines changed: 133 additions & 143 deletions

File tree

app/src/main/kotlin/com/google/ai/sample/GenerativeAiViewModelFactory.kt

Lines changed: 10 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -21,38 +21,6 @@ enum class ModelOption(val displayName: String, val modelName: String) {
2121
}
2222

2323
val GenerativeViewModelFactory = object : ViewModelProvider.Factory {
24-
// Current selected model name
25-
private var currentModelName = ModelOption.GEMINI_FLASH_PREVIEW.modelName
26-
27-
/**
28-
* Set the model to high reasoning capability (gemini-2.5-pro-preview-03-25)
29-
*/
30-
fun highReasoningModel() {
31-
currentModelName = ModelOption.GEMINI_PRO.modelName
32-
}
33-
34-
/**
35-
* Set the model to low reasoning capability (gemini-2.0-flash-lite)
36-
*/
37-
fun lowReasoningModel() {
38-
currentModelName = ModelOption.GEMINI_FLASH_LITE.modelName
39-
}
40-
41-
/**
42-
* Set the model to a specific model option
43-
*/
44-
fun setModel(modelOption: ModelOption) {
45-
currentModelName = modelOption.modelName
46-
}
47-
48-
/**
49-
* Get the current model option
50-
*/
51-
fun getCurrentModel(): ModelOption {
52-
return ModelOption.values().find { it.modelName == currentModelName }
53-
?: ModelOption.GEMINI_FLASH_LITE
54-
}
55-
5624
override fun <T : ViewModel> create(
5725
viewModelClass: Class<T>,
5826
extras: CreationExtras
@@ -63,106 +31,46 @@ val GenerativeViewModelFactory = object : ViewModelProvider.Factory {
6331

6432
// Get the application context from extras
6533
val application = checkNotNull(extras[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY])
66-
34+
6735
// Get the API key from MainActivity
6836
val mainActivity = MainActivity.getInstance()
6937
val apiKey = mainActivity?.getCurrentApiKey() ?: ""
70-
38+
7139
if (apiKey.isEmpty()) {
7240
throw IllegalStateException("API key is not available. Please set an API key.")
7341
}
7442

7543
return with(viewModelClass) {
7644
when {
77-
7845
isAssignableFrom(PhotoReasoningViewModel::class.java) -> {
46+
val currentModel = GenerativeAiViewModelFactory.getCurrentModel()
7947
// Initialize a GenerativeModel with the currently selected model
8048
// for multimodal text generation
8149
val generativeModel = GenerativeModel(
82-
modelName = currentModelName,
50+
modelName = currentModel.modelName,
8351
apiKey = apiKey,
8452
generationConfig = config
8553
)
8654
// Pass the ApiKeyManager to the ViewModel for key rotation
8755
val apiKeyManager = ApiKeyManager.getInstance(application)
88-
PhotoReasoningViewModel(generativeModel, apiKeyManager)
56+
PhotoReasoningViewModel(generativeModel, currentModel.modelName)
8957
}
9058

91-
9259
else ->
9360
throw IllegalArgumentException("Unknown ViewModel class: ${viewModelClass.name}")
9461
}
9562
} as T
9663
}
9764
}
9865

99-
// Add companion object with static methods for easier access
10066
object GenerativeAiViewModelFactory {
101-
// Current selected model name - duplicated from GenerativeViewModelFactory
102-
private var currentModelName = ModelOption.GEMINI_FLASH_PREVIEW.modelName
103-
104-
/**
105-
* Set the model to high reasoning capability (gemini-2.5-pro-preview-03-25)
106-
*/
107-
fun highReasoningModel() {
108-
currentModelName = ModelOption.GEMINI_PRO.modelName
109-
// Also update the original factory to keep them in sync
110-
(GenerativeViewModelFactory as ViewModelProvider.Factory).apply {
111-
if (this is ViewModelProvider.Factory) {
112-
try {
113-
val field = this.javaClass.getDeclaredField("currentModelName")
114-
field.isAccessible = true
115-
field.set(this, currentModelName)
116-
} catch (e: Exception) {
117-
// Fallback if reflection fails
118-
}
119-
}
120-
}
121-
}
122-
123-
/**
124-
* Set the model to low reasoning capability (gemini-2.0-flash-lite)
125-
*/
126-
fun lowReasoningModel() {
127-
currentModelName = ModelOption.GEMINI_FLASH_LITE.modelName
128-
// Also update the original factory to keep them in sync
129-
(GenerativeViewModelFactory as ViewModelProvider.Factory).apply {
130-
if (this is ViewModelProvider.Factory) {
131-
try {
132-
val field = this.javaClass.getDeclaredField("currentModelName")
133-
field.isAccessible = true
134-
field.set(this, currentModelName)
135-
} catch (e: Exception) {
136-
// Fallback if reflection fails
137-
}
138-
}
139-
}
140-
}
141-
142-
/**
143-
* Set the model to a specific model option
144-
*/
67+
private var currentModel: ModelOption = ModelOption.GEMINI_FLASH_PREVIEW
68+
14569
fun setModel(modelOption: ModelOption) {
146-
currentModelName = modelOption.modelName
147-
// Also update the original factory to keep them in sync
148-
(GenerativeViewModelFactory as ViewModelProvider.Factory).apply {
149-
if (this is ViewModelProvider.Factory) {
150-
try {
151-
val field = this.javaClass.getDeclaredField("currentModelName")
152-
field.isAccessible = true
153-
field.set(this, currentModelName)
154-
} catch (e: Exception) {
155-
// Fallback if reflection fails
156-
}
157-
}
158-
}
70+
currentModel = modelOption
15971
}
160-
161-
/**
162-
* Get the current model option
163-
*/
72+
16473
fun getCurrentModel(): ModelOption {
165-
return ModelOption.values().find { it.modelName == currentModelName }
166-
?: ModelOption.GEMINI_FLASH_LITE
74+
return currentModel
16775
}
16876
}

app/src/main/kotlin/com/google/ai/sample/MainActivity.kt

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ import androidx.compose.ui.unit.dp
6767
import androidx.compose.ui.window.Dialog
6868
import androidx.core.content.ContextCompat
6969
import androidx.lifecycle.lifecycleScope
70+
import android.Manifest
7071
import androidx.navigation.NavHostController
7172
import androidx.navigation.compose.NavHost
7273
import androidx.navigation.compose.composable
@@ -122,6 +123,7 @@ class MainActivity : ComponentActivity() {
122123

123124
private lateinit var navController: NavHostController
124125
private var isProcessingExplicitScreenshotRequest: Boolean = false
126+
private var onMediaProjectionPermissionGranted: (() -> Unit)? = null
125127

126128
private val screenshotRequestHandler = object : BroadcastReceiver() {
127129
override fun onReceive(context: Context?, intent: Intent?) {
@@ -166,10 +168,19 @@ class MainActivity : ComponentActivity() {
166168

167169
// Permission Launchers
168170
private lateinit var requestNotificationPermissionLauncher: ActivityResultLauncher<String>
171+
private lateinit var requestForegroundServicePermissionLauncher: ActivityResultLauncher<String>
169172
// private val requestPermissionLauncher = registerForActivityResult(...) // Deleted
170173

171-
private fun requestMediaProjectionPermission() {
174+
fun requestMediaProjectionPermission(onGranted: (() -> Unit)? = null) {
172175
Log.d(TAG, "Requesting MediaProjection permission")
176+
onMediaProjectionPermissionGranted = onGranted
177+
178+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
179+
if (ContextCompat.checkSelfPermission(this, Manifest.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION) != PackageManager.PERMISSION_GRANTED) {
180+
requestForegroundServicePermissionLauncher.launch(Manifest.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION)
181+
}
182+
}
183+
173184
// Ensure mediaProjectionManager is initialized before using it.
174185
// This should be guaranteed by its placement in onCreate.
175186
if (!::mediaProjectionManager.isInitialized) {
@@ -222,6 +233,11 @@ class MainActivity : ComponentActivity() {
222233
val isAccessibilityServiceEnabledFlow: StateFlow<Boolean> = _isAccessibilityServiceEnabled.asStateFlow()
223234
// END: Added for Accessibility Service Status
224235

236+
// START: Added for MediaProjection Permission Status
237+
private val _isMediaProjectionPermissionGranted = MutableStateFlow(false)
238+
val isMediaProjectionPermissionGrantedFlow: StateFlow<Boolean> = _isMediaProjectionPermissionGranted.asStateFlow()
239+
// END: Added for MediaProjection Permission Status
240+
225241
// SharedPreferences for first launch info
226242
private lateinit var prefs: SharedPreferences
227243
private var showFirstLaunchInfoDialog by mutableStateOf(false)
@@ -444,6 +460,9 @@ class MainActivity : ComponentActivity() {
444460
Log.d(TAG, "Resetting isProcessingExplicitScreenshotRequest flag after successful explicit grant.")
445461
this@MainActivity.isProcessingExplicitScreenshotRequest = false
446462
}
463+
_isMediaProjectionPermissionGranted.value = true
464+
onMediaProjectionPermissionGranted?.invoke()
465+
onMediaProjectionPermissionGranted = null
447466
} else {
448467
Log.w(TAG, "MediaProjection permission denied or cancelled by user.")
449468
Toast.makeText(this, "Screen capture permission denied", Toast.LENGTH_SHORT).show()
@@ -452,6 +471,7 @@ class MainActivity : ComponentActivity() {
452471
Log.d(TAG, "Resetting isProcessingExplicitScreenshotRequest flag after explicit denial.")
453472
this@MainActivity.isProcessingExplicitScreenshotRequest = false
454473
}
474+
_isMediaProjectionPermissionGranted.value = false
455475
}
456476
}
457477

@@ -572,6 +592,14 @@ class MainActivity : ComponentActivity() {
572592
}
573593
}
574594

595+
requestForegroundServicePermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted: Boolean ->
596+
if (isGranted) {
597+
Toast.makeText(this, "Foreground service permission granted.", Toast.LENGTH_SHORT).show()
598+
} else {
599+
Toast.makeText(this, "Foreground service permission denied. The app may not function correctly.", Toast.LENGTH_LONG).show()
600+
}
601+
}
602+
575603
if (photoReasoningViewModel != null) {
576604
lifecycleScope.launch {
577605
photoReasoningViewModel!!.showStopNotificationFlow.collect { show ->
@@ -589,11 +617,6 @@ class MainActivity : ComponentActivity() {
589617
Log.w(TAG, "photoReasoningViewModel is null at the end of onCreate. Notification flow collection might be delayed or not start if VM is set much later or never.")
590618
}
591619

592-
Log.d(TAG, "onCreate: Scheduling MediaProjection permission request")
593-
Handler(Looper.getMainLooper()).postDelayed({
594-
Log.d(TAG, "onCreate: Calling requestMediaProjectionPermission")
595-
requestMediaProjectionPermission()
596-
}, 1000) // 1 second delay to ensure everything is initialized
597620
}
598621

599622
fun showStopOperationNotification() {

app/src/main/kotlin/com/google/ai/sample/ScreenOperatorAccessibilityService.kt

Lines changed: 30 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -214,23 +214,37 @@ class ScreenOperatorAccessibilityService : AccessibilityService() {
214214
true // Asynchronous
215215
}
216216
is Command.TakeScreenshot -> {
217-
Log.d(TAG, "Command.TakeScreenshot: Capturing screen info and sending request broadcast to MainActivity.")
218-
this.showToast("Preparing screenshot...", false) // Updated toast message
217+
val modelName = GenerativeAiViewModelFactory.getCurrentModel().modelName
218+
if (modelName == "gemma-3n-e4b-it") {
219+
Log.d(TAG, "Command.TakeScreenshot: Model is gemma-3n-e4b-it, capturing screen info only.")
220+
this.showToast("Capturing screen info...", false)
221+
val screenInfo = captureScreenInformation()
222+
val mainActivity = MainActivity.getInstance()
223+
mainActivity?.getPhotoReasoningViewModel()?.addScreenshotToConversation(
224+
Uri.EMPTY,
225+
applicationContext,
226+
screenInfo
227+
)
228+
false
229+
} else {
230+
Log.d(TAG, "Command.TakeScreenshot: Capturing screen info and sending request broadcast to MainActivity.")
231+
this.showToast("Preparing screenshot...", false) // Updated toast message
219232

220-
val screenInfo = captureScreenInformation() // Capture fresh screen info
233+
val screenInfo = captureScreenInformation() // Capture fresh screen info
221234

222-
val intent = Intent(MainActivity.ACTION_REQUEST_MEDIAPROJECTION_SCREENSHOT).apply {
223-
putExtra(MainActivity.EXTRA_SCREEN_INFO, screenInfo)
224-
// Set package to ensure only our app's receiver gets it
225-
`package` = applicationContext.packageName
226-
}
227-
applicationContext.sendBroadcast(intent)
228-
Log.d(TAG, "Sent broadcast ACTION_REQUEST_MEDIAPROJECTION_SCREENSHOT to MainActivity with screenInfo.")
235+
val intent = Intent(MainActivity.ACTION_REQUEST_MEDIAPROJECTION_SCREENSHOT).apply {
236+
putExtra(MainActivity.EXTRA_SCREEN_INFO, screenInfo)
237+
// Set package to ensure only our app's receiver gets it
238+
`package` = applicationContext.packageName
239+
}
240+
applicationContext.sendBroadcast(intent)
241+
Log.d(TAG, "Sent broadcast ACTION_REQUEST_MEDIAPROJECTION_SCREENSHOT to MainActivity with screenInfo.")
229242

230-
// The command is considered "handled" once the broadcast is sent.
231-
// MainActivity and ScreenCaptureService will handle the rest asynchronously.
232-
// Return false to allow the command queue to proceed immediately.
233-
false
243+
// The command is considered "handled" once the broadcast is sent.
244+
// MainActivity and ScreenCaptureService will handle the rest asynchronously.
245+
// Return false to allow the command queue to proceed immediately.
246+
false
247+
}
234248
}
235249
is Command.PressHomeButton -> {
236250
Log.d(TAG, "Pressing home button")
@@ -325,13 +339,13 @@ class ScreenOperatorAccessibilityService : AccessibilityService() {
325339
is Command.UseHighReasoningModel -> {
326340
Log.d(TAG, "Switching to high reasoning model (gemini-2.5-pro-preview-03-25)")
327341
this.showToast("Switching to more powerful model (gemini-2.5-pro-preview-03-25)", false)
328-
GenerativeAiViewModelFactory.highReasoningModel()
342+
GenerativeAiViewModelFactory.setModel(ModelOption.GEMINI_PRO)
329343
false // Synchronous
330344
}
331345
is Command.UseLowReasoningModel -> {
332346
Log.d(TAG, "Switching to low reasoning model (gemini-2.0-flash-lite)")
333347
this.showToast("Switching to faster model (gemini-2.0-flash-lite)", false)
334-
GenerativeAiViewModelFactory.lowReasoningModel()
348+
GenerativeAiViewModelFactory.setModel(ModelOption.GEMINI_FLASH_LITE)
335349
false // Synchronous
336350
}
337351
is Command.PressEnterKey -> {

0 commit comments

Comments
 (0)