Skip to content

Commit 2674d58

Browse files
Merge pull request #31 from Android-PowerUser/stop-button
2 parents 5cef6f5 + 5b831f5 commit 2674d58

43 files changed

Lines changed: 8198 additions & 1051 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ This Android app operates the screen with commands from vision LLMs
1515

1616
Unfortunately, I need 12 testers for 14 days to publish the app on the Play Store.
1717

18+
1819
For the Play Store link to work you must first join the [Google Group](https://groups.google.com/g/Screen_Operator) (I didn't make that rule). You can then download it regularly from the [Play Store](https://play.google.com/store/apps/details?id=io.github.android_poweruser).
1920

2021
### Video

app/build.gradle.kts

Lines changed: 15 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,11 @@
1-
/*
2-
* Copyright 2023 Google LLC
3-
*
4-
* Licensed under the Apache License, Version 2.0 (the "License");
5-
* you may not use this file except in compliance with the License.
6-
* You may obtain a copy of the License at
7-
*
8-
* http://www.apache.org/licenses/LICENSE-2.0
9-
*
10-
* Unless required by applicable law or agreed to in writing, software
11-
* distributed under the License is distributed on an "AS IS" BASIS,
12-
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13-
* See the License for the specific language governing permissions and
14-
* limitations under the License.
15-
*/
1+
162

173
plugins {
184
id("com.android.application")
195
id("org.jetbrains.kotlin.android")
6+
id("org.jetbrains.kotlin.plugin.serialization") version "1.9.20"
207
id("com.google.android.libraries.mapsplatform.secrets-gradle-plugin")
8+
id("kotlin-parcelize")
219
}
2210

2311
android {
@@ -26,7 +14,7 @@ android {
2614

2715
defaultConfig {
2816
applicationId = "com.google.ai.sample"
29-
minSdk = 26
17+
minSdk = 33
3018
targetSdk = 34
3119
versionCode = 1
3220
versionName = "1.0"
@@ -69,9 +57,15 @@ dependencies {
6957
implementation("androidx.activity:activity-compose:1.8.1")
7058
implementation("androidx.navigation:navigation-compose:2.7.5")
7159

60+
// Required for Android Accessibility Service
61+
implementation("androidx.core:core:1.9.0")
62+
implementation("androidx.appcompat:appcompat:1.6.1")
63+
7264
// Required for one-shot operations (to use `ListenableFuture` from Guava Android)
7365
implementation("com.google.guava:guava:31.0.1-android")
7466

67+
implementation("com.google.code.gson:gson:2.10.1")
68+
7569
// Required for streaming operations (to use `Publisher` from Reactive Streams)
7670
implementation("org.reactivestreams:reactive-streams:1.0.4")
7771

@@ -83,6 +77,9 @@ dependencies {
8377

8478
implementation("io.coil-kt:coil-compose:2.5.0")
8579

80+
// Google Play Billing Library
81+
implementation("com.android.billingclient:billing-ktx:7.1.1") // Latest version as per documentation
82+
8683
testImplementation("junit:junit:4.13.2")
8784
androidTestImplementation("androidx.test.ext:junit:1.1.5")
8885
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
@@ -92,4 +89,6 @@ dependencies {
9289
debugImplementation("androidx.compose.ui:ui-test-manifest")
9390

9491
implementation("com.google.ai.client.generativeai:generativeai:0.9.0")
92+
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0")
9593
}
94+

app/src/main/AndroidManifest.xml

Lines changed: 45 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,69 @@
11
<?xml version="1.0" encoding="utf-8"?>
2-
<!-- Copyright 2023 Google LLC
3-
4-
Licensed under the Apache License, Version 2.0 (the "License");
5-
you may not use this file except in compliance with the License.
6-
You may obtain a copy of the License at
7-
8-
http://www.apache.org/licenses/LICENSE-2.0
9-
10-
Unless required by applicable law or agreed to in writing, software
11-
distributed under the License is distributed on an "AS IS" BASIS,
12-
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13-
See the License for the specific language governing permissions and
14-
limitations under the License.
15-
-->
162
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
173
xmlns:tools="http://schemas.android.com/tools">
18-
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
4+
<!-- Storage permissions -->
5+
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
6+
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28" />
7+
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" tools:ignore="ScopedStorage" />
8+
9+
<!-- Media permissions for Android 13+ -->
10+
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
11+
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
12+
13+
<!-- Accessibility service permission -->
14+
<uses-permission android:name="android.permission.BIND_ACCESSIBILITY_SERVICE" />
15+
<uses-permission android:name="android.permission.INJECT_EVENTS" />
16+
17+
<!-- Foreground service permission for background operation -->
18+
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
1919

2020
<application
21+
android:name=".PhotoReasoningApplication"
2122
android:allowBackup="true"
2223
android:dataExtractionRules="@xml/data_extraction_rules"
2324
android:fullBackupContent="@xml/backup_rules"
2425
android:icon="@mipmap/ic_launcher"
2526
android:label="@string/app_name"
2627
android:roundIcon="@mipmap/ic_launcher_round"
2728
android:supportsRtl="true"
29+
android:requestLegacyExternalStorage="true"
2830
tools:targetApi="31">
2931
<activity
3032
android:name=".MainActivity"
3133
android:exported="true"
3234
android:label="@string/app_name">
3335
<intent-filter>
3436
<action android:name="android.intent.action.MAIN" />
35-
3637
<category android:name="android.intent.category.LAUNCHER" />
3738
</intent-filter>
3839
</activity>
39-
</application>
4040

41+
<!-- Add the accessibility service configuration -->
42+
<service
43+
android:name=".ScreenOperatorAccessibilityService"
44+
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"
45+
android:exported="true"
46+
android:enabled="true">
47+
<intent-filter>
48+
<action android:name="android.accessibilityservice.AccessibilityService" />
49+
</intent-filter>
50+
<meta-data
51+
android:name="android.accessibilityservice"
52+
android:resource="@xml/accessibility_service_config" />
53+
</service>
54+
55+
<!-- Added TrialTimerService declaration -->
56+
<service android:name=".TrialTimerService" />
57+
58+
<provider
59+
android:name="androidx.core.content.FileProvider"
60+
android:authorities="${applicationId}.provider"
61+
android:exported="false"
62+
android:grantUriPermissions="true">
63+
<meta-data
64+
android:name="android.support.FILE_PROVIDER_PATHS"
65+
android:resource="@xml/file_paths" />
66+
</provider>
67+
</application>
4168
</manifest>
69+
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
package com.google.ai.sample
2+
3+
import android.content.Intent
4+
import android.net.Uri
5+
import androidx.compose.foundation.layout.*
6+
import androidx.compose.foundation.lazy.LazyColumn
7+
import androidx.compose.foundation.lazy.itemsIndexed
8+
import androidx.compose.material3.*
9+
import androidx.compose.runtime.*
10+
import androidx.compose.ui.Alignment
11+
import androidx.compose.ui.Modifier
12+
import androidx.compose.ui.platform.LocalContext
13+
import androidx.compose.ui.text.style.TextAlign
14+
import androidx.compose.ui.unit.dp
15+
import androidx.compose.ui.window.Dialog
16+
17+
/**
18+
* Dialog for API key input and management
19+
*/
20+
@Composable
21+
fun ApiKeyDialog(
22+
apiKeyManager: ApiKeyManager,
23+
isFirstLaunch: Boolean = false,
24+
onDismiss: () -> Unit
25+
) {
26+
var apiKeyInput by remember { mutableStateOf("") }
27+
var errorMessage by remember { mutableStateOf("") }
28+
val apiKeys = remember { mutableStateListOf<String>() }
29+
var selectedKeyIndex by remember { mutableStateOf(apiKeyManager.getCurrentKeyIndex()) }
30+
val context = LocalContext.current
31+
32+
// Load existing keys
33+
LaunchedEffect(Unit) {
34+
apiKeys.clear()
35+
apiKeys.addAll(apiKeyManager.getApiKeys())
36+
}
37+
38+
Dialog(onDismissRequest = {
39+
// Only allow dismissal if not first launch or if keys exist
40+
if (!isFirstLaunch || apiKeys.isNotEmpty()) {
41+
onDismiss()
42+
}
43+
}) {
44+
Surface(
45+
modifier = Modifier
46+
.fillMaxWidth()
47+
.wrapContentHeight(),
48+
shape = MaterialTheme.shapes.medium,
49+
color = MaterialTheme.colorScheme.surface
50+
) {
51+
Column(
52+
modifier = Modifier
53+
.padding(16.dp)
54+
.fillMaxWidth()
55+
) {
56+
Text(
57+
text = if (isFirstLaunch) "API Key Required" else "Manage API Keys",
58+
style = MaterialTheme.typography.headlineSmall,
59+
modifier = Modifier.padding(bottom = 16.dp)
60+
)
61+
62+
if (isFirstLaunch && apiKeys.isEmpty()) {
63+
Text(
64+
text = "Please enter a Gemini API key to use this application.",
65+
style = MaterialTheme.typography.bodyMedium,
66+
modifier = Modifier.padding(bottom = 16.dp)
67+
)
68+
}
69+
70+
// Get API Key button
71+
Button(
72+
onClick = {
73+
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("https://makersuite.google.com/app/apikey"))
74+
context.startActivity(intent)
75+
},
76+
modifier = Modifier
77+
.fillMaxWidth()
78+
.padding(bottom = 16.dp)
79+
) {
80+
Text("Get API Key")
81+
}
82+
83+
// Input field for new API key
84+
OutlinedTextField(
85+
value = apiKeyInput,
86+
onValueChange = {
87+
apiKeyInput = it
88+
errorMessage = ""
89+
},
90+
label = { Text("Enter API Key") },
91+
modifier = Modifier
92+
.fillMaxWidth()
93+
.padding(bottom = 8.dp),
94+
singleLine = true
95+
)
96+
97+
// Error message
98+
if (errorMessage.isNotEmpty()) {
99+
Text(
100+
text = errorMessage,
101+
color = MaterialTheme.colorScheme.error,
102+
style = MaterialTheme.typography.bodySmall,
103+
modifier = Modifier.padding(bottom = 8.dp)
104+
)
105+
}
106+
107+
// Add button
108+
Button(
109+
onClick = {
110+
if (apiKeyInput.isBlank()) {
111+
errorMessage = "API key cannot be empty"
112+
return@Button
113+
}
114+
115+
val added = apiKeyManager.addApiKey(apiKeyInput)
116+
if (added) {
117+
apiKeys.clear()
118+
apiKeys.addAll(apiKeyManager.getApiKeys())
119+
apiKeyInput = ""
120+
selectedKeyIndex = apiKeyManager.getCurrentKeyIndex()
121+
} else {
122+
errorMessage = "API key already exists"
123+
}
124+
},
125+
modifier = Modifier
126+
.align(Alignment.End)
127+
.padding(bottom = 16.dp)
128+
) {
129+
Text("Add Key")
130+
}
131+
132+
// List of existing keys
133+
if (apiKeys.isNotEmpty()) {
134+
Text(
135+
text = "Saved API Keys",
136+
style = MaterialTheme.typography.titleMedium,
137+
modifier = Modifier.padding(bottom = 8.dp)
138+
)
139+
140+
LazyColumn(
141+
modifier = Modifier
142+
.fillMaxWidth()
143+
.heightIn(max = 200.dp)
144+
) {
145+
itemsIndexed(apiKeys) { index, key ->
146+
Row(
147+
modifier = Modifier
148+
.fillMaxWidth()
149+
.padding(vertical = 4.dp),
150+
verticalAlignment = Alignment.CenterVertically
151+
) {
152+
RadioButton(
153+
selected = index == selectedKeyIndex,
154+
onClick = {
155+
apiKeyManager.setCurrentKeyIndex(index)
156+
selectedKeyIndex = index
157+
}
158+
)
159+
160+
Text(
161+
text = key.take(10) + "..." + key.takeLast(5),
162+
modifier = Modifier
163+
.weight(1f)
164+
.padding(start = 8.dp)
165+
)
166+
167+
IconButton(
168+
onClick = {
169+
apiKeyManager.removeApiKey(key)
170+
apiKeys.clear()
171+
apiKeys.addAll(apiKeyManager.getApiKeys())
172+
selectedKeyIndex = apiKeyManager.getCurrentKeyIndex()
173+
}
174+
) {
175+
Text("", textAlign = TextAlign.Center)
176+
}
177+
}
178+
}
179+
}
180+
}
181+
182+
// Buttons
183+
Row(
184+
modifier = Modifier
185+
.fillMaxWidth()
186+
.padding(top = 16.dp),
187+
horizontalArrangement = Arrangement.End
188+
) {
189+
if (!isFirstLaunch || apiKeys.isNotEmpty()) {
190+
TextButton(
191+
onClick = onDismiss
192+
) {
193+
Text("Close")
194+
}
195+
}
196+
}
197+
}
198+
}
199+
}
200+
}

0 commit comments

Comments
 (0)