apkdev 2.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- apkdev/__init__.py +13 -0
- apkdev/__main__.py +5 -0
- apkdev/apkfile.py +168 -0
- apkdev/builder.py +671 -0
- apkdev/cli.py +925 -0
- apkdev/completion.py +14 -0
- apkdev/device.py +161 -0
- apkdev/inspector.py +291 -0
- apkdev/optimizer.py +58 -0
- apkdev/reverser.py +62 -0
- apkdev/sdk.py +526 -0
- apkdev/utils.py +74 -0
- apkdev-2.0.0.dist-info/METADATA +159 -0
- apkdev-2.0.0.dist-info/RECORD +17 -0
- apkdev-2.0.0.dist-info/WHEEL +5 -0
- apkdev-2.0.0.dist-info/entry_points.txt +2 -0
- apkdev-2.0.0.dist-info/top_level.txt +1 -0
apkdev/builder.py
ADDED
|
@@ -0,0 +1,671 @@
|
|
|
1
|
+
"""APK Builder — create projects, compile, sign."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import shutil
|
|
5
|
+
import subprocess
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from .utils import check_tool, run, ensure_android_home, is_termux
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def create_project(name: str) -> Path:
|
|
12
|
+
"""Scaffold a new Kotlin + Gradle APK project."""
|
|
13
|
+
proj_dir = Path.home() / name
|
|
14
|
+
src = proj_dir / "app" / "src" / "main"
|
|
15
|
+
java_dir = src / "java" / "com" / "example" / name.lower()
|
|
16
|
+
res_dir = src / "res"
|
|
17
|
+
|
|
18
|
+
java_dir.mkdir(parents=True, exist_ok=True)
|
|
19
|
+
(res_dir / "layout").mkdir(parents=True, exist_ok=True)
|
|
20
|
+
(res_dir / "values").mkdir(parents=True, exist_ok=True)
|
|
21
|
+
|
|
22
|
+
# settings.gradle.kts
|
|
23
|
+
(proj_dir / "settings.gradle.kts").write_text(
|
|
24
|
+
"""pluginManagement {
|
|
25
|
+
repositories { google(); mavenCentral(); gradlePluginPortal() }
|
|
26
|
+
}
|
|
27
|
+
dependencyResolutionManagement {
|
|
28
|
+
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
|
29
|
+
repositories { google(); mavenCentral() }
|
|
30
|
+
}
|
|
31
|
+
rootProject.name = "%s"
|
|
32
|
+
include(":app")
|
|
33
|
+
""" % name
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
# build.gradle.kts (root)
|
|
37
|
+
(proj_dir / "build.gradle.kts").write_text(
|
|
38
|
+
"""plugins {
|
|
39
|
+
id("com.android.application") version "8.7.3" apply false
|
|
40
|
+
kotlin("android") version "2.0.21" apply false
|
|
41
|
+
id("org.jetbrains.kotlin.plugin.compose") version "2.0.21" apply false
|
|
42
|
+
}
|
|
43
|
+
"""
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
# app/build.gradle.kts
|
|
47
|
+
(proj_dir / "app" / "build.gradle.kts").write_text(
|
|
48
|
+
"""plugins { id("com.android.application"); kotlin("android"); id("org.jetbrains.kotlin.plugin.compose") }
|
|
49
|
+
android {
|
|
50
|
+
namespace = "com.example.%s"
|
|
51
|
+
compileSdk = 34
|
|
52
|
+
defaultConfig {
|
|
53
|
+
applicationId = "com.example.%s"
|
|
54
|
+
minSdk = 26; targetSdk = 34; versionCode = 1; versionName = "1.0"
|
|
55
|
+
}
|
|
56
|
+
buildFeatures { compose = true }
|
|
57
|
+
compileOptions { sourceCompatibility = JavaVersion.VERSION_17; targetCompatibility = JavaVersion.VERSION_17 }
|
|
58
|
+
kotlinOptions { jvmTarget = "17" }
|
|
59
|
+
}
|
|
60
|
+
dependencies {
|
|
61
|
+
val composeBom = platform("androidx.compose:compose-bom:2024.10.00")
|
|
62
|
+
implementation(composeBom)
|
|
63
|
+
implementation("androidx.compose.ui:ui")
|
|
64
|
+
implementation("androidx.compose.ui:ui-graphics")
|
|
65
|
+
implementation("androidx.compose.ui:ui-tooling-preview")
|
|
66
|
+
implementation("androidx.compose.material3:material3")
|
|
67
|
+
implementation("androidx.compose.material:material-icons-core")
|
|
68
|
+
implementation("androidx.activity:activity-compose:1.9.3")
|
|
69
|
+
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.7")
|
|
70
|
+
implementation("androidx.core:core-ktx:1.13.1")
|
|
71
|
+
debugImplementation("androidx.compose.ui:ui-tooling")
|
|
72
|
+
}
|
|
73
|
+
""" % (name.lower(), name.lower())
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# AndroidManifest.xml
|
|
77
|
+
manifest = (
|
|
78
|
+
'<?xml version="1.0" encoding="utf-8"?>\n'
|
|
79
|
+
'<manifest xmlns:android="http://schemas.android.com/apk/res/android">\n'
|
|
80
|
+
' <application android:allowBackup="true" android:label="%s"\n'
|
|
81
|
+
' android:supportsRtl="true"\n'
|
|
82
|
+
' android:theme="@android:style/Theme.Material.Light.NoActionBar">\n'
|
|
83
|
+
' <activity android:name=".MainActivity" android:exported="true"\n'
|
|
84
|
+
' android:windowSoftInputMode="adjustResize">\n'
|
|
85
|
+
' <intent-filter>\n'
|
|
86
|
+
' <action android:name="android.intent.action.MAIN" />\n'
|
|
87
|
+
' <category android:name="android.intent.category.LAUNCHER" />\n'
|
|
88
|
+
' </intent-filter>\n'
|
|
89
|
+
' </activity>\n'
|
|
90
|
+
' </application>\n'
|
|
91
|
+
'</manifest>\n'
|
|
92
|
+
) % name
|
|
93
|
+
(src / "AndroidManifest.xml").write_text(manifest)
|
|
94
|
+
|
|
95
|
+
# MainActivity.kt
|
|
96
|
+
(java_dir / "MainActivity.kt").write_text(
|
|
97
|
+
"""package com.example.%s
|
|
98
|
+
|
|
99
|
+
import android.os.Bundle
|
|
100
|
+
import androidx.activity.ComponentActivity
|
|
101
|
+
import androidx.activity.compose.setContent
|
|
102
|
+
import androidx.compose.foundation.layout.fillMaxSize
|
|
103
|
+
import androidx.compose.material3.MaterialTheme
|
|
104
|
+
import androidx.compose.material3.Surface
|
|
105
|
+
import androidx.compose.ui.Modifier
|
|
106
|
+
import androidx.compose.ui.graphics.Color
|
|
107
|
+
import com.example.%s.ui.theme.NotesTheme
|
|
108
|
+
|
|
109
|
+
class MainActivity : ComponentActivity() {
|
|
110
|
+
override fun onCreate(savedInstanceState: Bundle?) {
|
|
111
|
+
super.onCreate(savedInstanceState)
|
|
112
|
+
setContent {
|
|
113
|
+
NotesTheme {
|
|
114
|
+
Surface(
|
|
115
|
+
modifier = Modifier.fillMaxSize(),
|
|
116
|
+
color = MaterialTheme.colorScheme.background
|
|
117
|
+
) {
|
|
118
|
+
NotesApp()
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
""" % (name.lower(), name.lower())
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
# Create directories for Compose UI files
|
|
128
|
+
ui_dir = java_dir / "ui" / "theme"
|
|
129
|
+
ui_dir.mkdir(parents=True, exist_ok=True)
|
|
130
|
+
|
|
131
|
+
# Theme.kt — Material 3 with dynamic colors
|
|
132
|
+
(ui_dir / "Theme.kt").write_text(
|
|
133
|
+
"""package com.example.%s.ui.theme
|
|
134
|
+
|
|
135
|
+
import android.app.Activity
|
|
136
|
+
import android.os.Build
|
|
137
|
+
import androidx.compose.foundation.isSystemInDarkTheme
|
|
138
|
+
import androidx.compose.material3.MaterialTheme
|
|
139
|
+
import androidx.compose.material3.darkColorScheme
|
|
140
|
+
import androidx.compose.material3.dynamicDarkColorScheme
|
|
141
|
+
import androidx.compose.material3.dynamicLightColorScheme
|
|
142
|
+
import androidx.compose.material3.lightColorScheme
|
|
143
|
+
import androidx.compose.runtime.Composable
|
|
144
|
+
import androidx.compose.runtime.SideEffect
|
|
145
|
+
import androidx.compose.ui.graphics.Color
|
|
146
|
+
import androidx.compose.ui.graphics.toArgb
|
|
147
|
+
import androidx.compose.ui.platform.LocalContext
|
|
148
|
+
import androidx.compose.ui.platform.LocalView
|
|
149
|
+
import androidx.core.view.WindowCompat
|
|
150
|
+
|
|
151
|
+
private val DarkColorScheme = darkColorScheme(
|
|
152
|
+
primary = Color(0xFF00D2FF),
|
|
153
|
+
onPrimary = Color(0xFF003544),
|
|
154
|
+
primaryContainer = Color(0xFF004D62),
|
|
155
|
+
onPrimaryContainer = Color(0xFFB3EBFF),
|
|
156
|
+
secondary = Color(0xFF4DD0E1),
|
|
157
|
+
tertiary = Color(0xFFB388FF),
|
|
158
|
+
background = Color(0xFF0D0D1A),
|
|
159
|
+
surface = Color(0xFF1A1A2E),
|
|
160
|
+
surfaceVariant = Color(0xFF16213E),
|
|
161
|
+
onBackground = Color(0xFFE6E1E5),
|
|
162
|
+
onSurface = Color(0xFFE6E1E5),
|
|
163
|
+
outline = Color(0xFF3A3A5C),
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
private val LightColorScheme = lightColorScheme(
|
|
167
|
+
primary = Color(0xFF006D82),
|
|
168
|
+
onPrimary = Color.White,
|
|
169
|
+
primaryContainer = Color(0xFFB3EBFF),
|
|
170
|
+
onPrimaryContainer = Color(0xFF001F28),
|
|
171
|
+
secondary = Color(0xFF008C9E),
|
|
172
|
+
tertiary = Color(0xFF7C52CC),
|
|
173
|
+
background = Color(0xFFFEFBFF),
|
|
174
|
+
surface = Color(0xFFFEFBFF),
|
|
175
|
+
surfaceVariant = Color(0xFFDCE5F0),
|
|
176
|
+
onBackground = Color(0xFF1B1B1F),
|
|
177
|
+
onSurface = Color(0xFF1B1B1F),
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
@Composable
|
|
181
|
+
fun NotesTheme(
|
|
182
|
+
darkTheme: Boolean = isSystemInDarkTheme(),
|
|
183
|
+
dynamicColor: Boolean = true,
|
|
184
|
+
content: @Composable () -> Unit
|
|
185
|
+
) {
|
|
186
|
+
val colorScheme = when {
|
|
187
|
+
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
|
|
188
|
+
val context = LocalContext.current
|
|
189
|
+
if (darkTheme) dynamicDarkColorScheme(context)
|
|
190
|
+
else dynamicLightColorScheme(context)
|
|
191
|
+
}
|
|
192
|
+
darkTheme -> DarkColorScheme
|
|
193
|
+
else -> LightColorScheme
|
|
194
|
+
}
|
|
195
|
+
val view = LocalView.current
|
|
196
|
+
if (!view.isInEditMode) {
|
|
197
|
+
SideEffect {
|
|
198
|
+
val window = (view.context as Activity).window
|
|
199
|
+
window.statusBarColor = colorScheme.background.toArgb()
|
|
200
|
+
WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = !darkTheme
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
MaterialTheme(
|
|
204
|
+
colorScheme = colorScheme,
|
|
205
|
+
content = content
|
|
206
|
+
)
|
|
207
|
+
}
|
|
208
|
+
""" % name.lower()
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
# Color.kt
|
|
212
|
+
(ui_dir / "Color.kt").write_text(
|
|
213
|
+
"""package com.example.%s.ui.theme
|
|
214
|
+
|
|
215
|
+
import androidx.compose.ui.graphics.Color
|
|
216
|
+
|
|
217
|
+
val Purple80 = Color(0xFFD0BCFF)
|
|
218
|
+
val PurpleGrey80 = Color(0xFFCCC2DC)
|
|
219
|
+
val Pink80 = Color(0xFFEFB8C8)
|
|
220
|
+
val Purple40 = Color(0xFF6650a4)
|
|
221
|
+
val PurpleGrey40 = Color(0xFF625b71)
|
|
222
|
+
val Pink40 = Color(0xFF7D5260)
|
|
223
|
+
""" % name.lower()
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
# Main composable: NotesApp.kt
|
|
227
|
+
(java_dir / "NotesApp.kt").write_text(
|
|
228
|
+
"""package com.example.%s
|
|
229
|
+
|
|
230
|
+
import android.content.Context
|
|
231
|
+
import androidx.compose.animation.*
|
|
232
|
+
import androidx.compose.foundation.background
|
|
233
|
+
import androidx.compose.foundation.layout.*
|
|
234
|
+
import androidx.compose.foundation.lazy.LazyColumn
|
|
235
|
+
import androidx.compose.foundation.lazy.items
|
|
236
|
+
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
237
|
+
import androidx.compose.material.icons.Icons
|
|
238
|
+
import androidx.compose.material.icons.filled.*
|
|
239
|
+
import androidx.compose.material.icons.outlined.*
|
|
240
|
+
import androidx.compose.material3.*
|
|
241
|
+
import androidx.compose.runtime.*
|
|
242
|
+
import androidx.compose.ui.Alignment
|
|
243
|
+
import androidx.compose.ui.Modifier
|
|
244
|
+
import androidx.compose.ui.draw.clip
|
|
245
|
+
import androidx.compose.ui.text.font.FontWeight
|
|
246
|
+
import androidx.compose.ui.text.style.TextDecoration
|
|
247
|
+
import androidx.compose.ui.text.style.TextOverflow
|
|
248
|
+
import androidx.compose.ui.unit.dp
|
|
249
|
+
import androidx.compose.ui.unit.sp
|
|
250
|
+
|
|
251
|
+
@OptIn(ExperimentalMaterial3Api::class)
|
|
252
|
+
@Composable
|
|
253
|
+
fun NotesApp() {
|
|
254
|
+
var notes by remember { mutableStateOf(listOf<Note>()) }
|
|
255
|
+
var showDialog by remember { mutableStateOf(false) }
|
|
256
|
+
var editingNote by remember { mutableStateOf<Note?>(null) }
|
|
257
|
+
var searchQuery by remember { mutableStateOf("") }
|
|
258
|
+
|
|
259
|
+
val filteredNotes = remember(notes, searchQuery) {
|
|
260
|
+
if (searchQuery.isBlank()) notes
|
|
261
|
+
else notes.filter { it.title.contains(searchQuery, ignoreCase = true) || it.content.contains(searchQuery, ignoreCase = true) }
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
Scaffold(
|
|
265
|
+
topBar = {
|
|
266
|
+
TopAppBar(
|
|
267
|
+
title = { Text("Notes", fontWeight = FontWeight.Bold) },
|
|
268
|
+
colors = TopAppBarDefaults.topAppBarColors(
|
|
269
|
+
containerColor = MaterialTheme.colorScheme.surface,
|
|
270
|
+
titleContentColor = MaterialTheme.colorScheme.onSurface
|
|
271
|
+
),
|
|
272
|
+
actions = {
|
|
273
|
+
IconButton(onClick = { /* sort */ }) {
|
|
274
|
+
Icon(Icons.Default.Menu, contentDescription = "Menu")
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
)
|
|
278
|
+
},
|
|
279
|
+
floatingActionButton = {
|
|
280
|
+
ExtendedFloatingActionButton(
|
|
281
|
+
onClick = {
|
|
282
|
+
editingNote = null
|
|
283
|
+
showDialog = true
|
|
284
|
+
},
|
|
285
|
+
icon = { Icon(Icons.Default.Add, contentDescription = null) },
|
|
286
|
+
text = { Text("New Note") },
|
|
287
|
+
containerColor = MaterialTheme.colorScheme.primary,
|
|
288
|
+
contentColor = MaterialTheme.colorScheme.onPrimary
|
|
289
|
+
)
|
|
290
|
+
}
|
|
291
|
+
) { padding ->
|
|
292
|
+
Column(modifier = Modifier.padding(padding)) {
|
|
293
|
+
// Search bar
|
|
294
|
+
OutlinedTextField(
|
|
295
|
+
value = searchQuery,
|
|
296
|
+
onValueChange = { searchQuery = it },
|
|
297
|
+
modifier = Modifier
|
|
298
|
+
.fillMaxWidth()
|
|
299
|
+
.padding(horizontal = 16.dp, vertical = 8.dp),
|
|
300
|
+
placeholder = { Text("Search notes...") },
|
|
301
|
+
leadingIcon = { Icon(Icons.Default.Search, contentDescription = null) },
|
|
302
|
+
trailingIcon = {
|
|
303
|
+
if (searchQuery.isNotEmpty()) {
|
|
304
|
+
IconButton(onClick = { searchQuery = "" }) {
|
|
305
|
+
Icon(Icons.Default.Close, contentDescription = "Clear")
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
},
|
|
309
|
+
singleLine = true,
|
|
310
|
+
shape = RoundedCornerShape(24.dp),
|
|
311
|
+
colors = OutlinedTextFieldDefaults.colors(
|
|
312
|
+
focusedBorderColor = MaterialTheme.colorScheme.primary,
|
|
313
|
+
unfocusedBorderColor = MaterialTheme.colorScheme.outline
|
|
314
|
+
)
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
if (filteredNotes.isEmpty()) {
|
|
318
|
+
// Empty state
|
|
319
|
+
Box(
|
|
320
|
+
modifier = Modifier.fillMaxSize(),
|
|
321
|
+
contentAlignment = Alignment.Center
|
|
322
|
+
) {
|
|
323
|
+
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
|
324
|
+
Icon(
|
|
325
|
+
Icons.Default.Create,
|
|
326
|
+
contentDescription = null,
|
|
327
|
+
modifier = Modifier.size(80.dp),
|
|
328
|
+
tint = MaterialTheme.colorScheme.outline
|
|
329
|
+
)
|
|
330
|
+
Spacer(Modifier.height(16.dp))
|
|
331
|
+
Text(
|
|
332
|
+
"No notes yet",
|
|
333
|
+
style = MaterialTheme.typography.titleLarge,
|
|
334
|
+
color = MaterialTheme.colorScheme.outline
|
|
335
|
+
)
|
|
336
|
+
Text(
|
|
337
|
+
"Tap + to create your first note",
|
|
338
|
+
style = MaterialTheme.typography.bodyMedium,
|
|
339
|
+
color = MaterialTheme.colorScheme.outline
|
|
340
|
+
)
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
} else {
|
|
344
|
+
LazyColumn(
|
|
345
|
+
contentPadding = PaddingValues(16.dp),
|
|
346
|
+
verticalArrangement = Arrangement.spacedBy(12.dp)
|
|
347
|
+
) {
|
|
348
|
+
items(filteredNotes, key = { it.id }) { note ->
|
|
349
|
+
NoteCard(
|
|
350
|
+
note = note,
|
|
351
|
+
onToggle = {
|
|
352
|
+
notes = notes.map { if (it.id == note.id) it.copy(done = !it.done) else it }
|
|
353
|
+
saveNotes(notes)
|
|
354
|
+
},
|
|
355
|
+
onEdit = {
|
|
356
|
+
editingNote = note
|
|
357
|
+
showDialog = true
|
|
358
|
+
},
|
|
359
|
+
onDelete = {
|
|
360
|
+
notes = notes.filter { it.id != note.id }
|
|
361
|
+
saveNotes(notes)
|
|
362
|
+
}
|
|
363
|
+
)
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
if (showDialog) {
|
|
371
|
+
NoteDialog(
|
|
372
|
+
note = editingNote,
|
|
373
|
+
onDismiss = { showDialog = false; editingNote = null },
|
|
374
|
+
onSave = { title, content ->
|
|
375
|
+
if (editingNote != null) {
|
|
376
|
+
notes = notes.map { if (it.id == editingNote!!.id) it.copy(title = title, content = content) else it }
|
|
377
|
+
} else {
|
|
378
|
+
notes = listOf(Note(title = title, content = content)) + notes
|
|
379
|
+
}
|
|
380
|
+
saveNotes(notes)
|
|
381
|
+
showDialog = false
|
|
382
|
+
editingNote = null
|
|
383
|
+
}
|
|
384
|
+
)
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
@Composable
|
|
389
|
+
fun NoteCard(note: Note, onToggle: () -> Unit, onEdit: () -> Unit, onDelete: () -> Unit) {
|
|
390
|
+
val surfaceColor = MaterialTheme.colorScheme.surfaceVariant
|
|
391
|
+
Card(
|
|
392
|
+
onClick = onEdit,
|
|
393
|
+
modifier = Modifier.fillMaxWidth(),
|
|
394
|
+
shape = RoundedCornerShape(16.dp),
|
|
395
|
+
colors = CardDefaults.cardColors(containerColor = surfaceColor),
|
|
396
|
+
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
|
|
397
|
+
) {
|
|
398
|
+
Row(
|
|
399
|
+
modifier = Modifier.padding(16.dp),
|
|
400
|
+
verticalAlignment = Alignment.Top
|
|
401
|
+
) {
|
|
402
|
+
Checkbox(
|
|
403
|
+
checked = note.done,
|
|
404
|
+
onCheckedChange = { onToggle() },
|
|
405
|
+
colors = CheckboxDefaults.colors(
|
|
406
|
+
checkedColor = MaterialTheme.colorScheme.primary
|
|
407
|
+
)
|
|
408
|
+
)
|
|
409
|
+
Spacer(Modifier.width(12.dp))
|
|
410
|
+
Column(modifier = Modifier.weight(1f)) {
|
|
411
|
+
Text(
|
|
412
|
+
text = note.title,
|
|
413
|
+
style = MaterialTheme.typography.titleMedium,
|
|
414
|
+
fontWeight = FontWeight.SemiBold,
|
|
415
|
+
textDecoration = if (note.done) TextDecoration.LineThrough else TextDecoration.None,
|
|
416
|
+
color = if (note.done) MaterialTheme.colorScheme.outline else MaterialTheme.colorScheme.onSurface,
|
|
417
|
+
maxLines = 2,
|
|
418
|
+
overflow = TextOverflow.Ellipsis
|
|
419
|
+
)
|
|
420
|
+
if (note.content.isNotBlank()) {
|
|
421
|
+
Spacer(Modifier.height(4.dp))
|
|
422
|
+
Text(
|
|
423
|
+
text = note.content,
|
|
424
|
+
style = MaterialTheme.typography.bodyMedium,
|
|
425
|
+
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
426
|
+
maxLines = 3,
|
|
427
|
+
overflow = TextOverflow.Ellipsis
|
|
428
|
+
)
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
Spacer(Modifier.width(8.dp))
|
|
432
|
+
Column {
|
|
433
|
+
IconButton(onClick = onEdit, modifier = Modifier.size(32.dp)) {
|
|
434
|
+
Icon(Icons.Default.Edit, contentDescription = "Edit", tint = MaterialTheme.colorScheme.primary, modifier = Modifier.size(18.dp))
|
|
435
|
+
}
|
|
436
|
+
IconButton(onClick = onDelete, modifier = Modifier.size(32.dp)) {
|
|
437
|
+
Icon(Icons.Default.Delete, contentDescription = "Delete", tint = MaterialTheme.colorScheme.error, modifier = Modifier.size(18.dp))
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
@Composable
|
|
445
|
+
fun NoteDialog(note: Note?, onDismiss: () -> Unit, onSave: (String, String) -> Unit) {
|
|
446
|
+
var title by remember { mutableStateOf(note?.title ?: "") }
|
|
447
|
+
var content by remember { mutableStateOf(note?.content ?: "") }
|
|
448
|
+
|
|
449
|
+
AlertDialog(
|
|
450
|
+
onDismissRequest = onDismiss,
|
|
451
|
+
title = { Text(if (note != null) "Edit Note" else "New Note", fontWeight = FontWeight.Bold) },
|
|
452
|
+
text = {
|
|
453
|
+
Column {
|
|
454
|
+
OutlinedTextField(
|
|
455
|
+
value = title,
|
|
456
|
+
onValueChange = { title = it },
|
|
457
|
+
label = { Text("Title") },
|
|
458
|
+
singleLine = true,
|
|
459
|
+
modifier = Modifier.fillMaxWidth(),
|
|
460
|
+
shape = RoundedCornerShape(12.dp)
|
|
461
|
+
)
|
|
462
|
+
Spacer(Modifier.height(12.dp))
|
|
463
|
+
OutlinedTextField(
|
|
464
|
+
value = content,
|
|
465
|
+
onValueChange = { content = it },
|
|
466
|
+
label = { Text("Content") },
|
|
467
|
+
modifier = Modifier
|
|
468
|
+
.fillMaxWidth()
|
|
469
|
+
.heightIn(min = 120.dp),
|
|
470
|
+
shape = RoundedCornerShape(12.dp),
|
|
471
|
+
maxLines = 5
|
|
472
|
+
)
|
|
473
|
+
}
|
|
474
|
+
},
|
|
475
|
+
confirmButton = {
|
|
476
|
+
Button(
|
|
477
|
+
onClick = { onSave(title, content) },
|
|
478
|
+
enabled = title.isNotBlank()
|
|
479
|
+
) {
|
|
480
|
+
Text("Save")
|
|
481
|
+
}
|
|
482
|
+
},
|
|
483
|
+
dismissButton = {
|
|
484
|
+
TextButton(onClick = onDismiss) {
|
|
485
|
+
Text("Cancel")
|
|
486
|
+
}
|
|
487
|
+
},
|
|
488
|
+
containerColor = MaterialTheme.colorScheme.surface,
|
|
489
|
+
titleContentColor = MaterialTheme.colorScheme.onSurface,
|
|
490
|
+
textContentColor = MaterialTheme.colorScheme.onSurfaceVariant
|
|
491
|
+
)
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
data class Note(
|
|
495
|
+
val id: Long = System.currentTimeMillis(),
|
|
496
|
+
val title: String,
|
|
497
|
+
val content: String = "",
|
|
498
|
+
val done: Boolean = false
|
|
499
|
+
)
|
|
500
|
+
|
|
501
|
+
private fun saveNotes(notes: List<Note>) {
|
|
502
|
+
// Notes are persisted via context or ViewModel in a real app
|
|
503
|
+
// For this demo, they persist during app session
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
private fun loadNotes(): MutableList<Note> {
|
|
507
|
+
return mutableListOf()
|
|
508
|
+
}
|
|
509
|
+
""" % name.lower()
|
|
510
|
+
)
|
|
511
|
+
|
|
512
|
+
# Remove XML layout (we use Compose)
|
|
513
|
+
layout_file = res_dir / "layout" / "activity_main.xml"
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
# strings.xml
|
|
517
|
+
app_name_str = "%s" % name
|
|
518
|
+
(res_dir / "values" / "strings.xml").write_text(
|
|
519
|
+
f"""<?xml version="1.0" encoding="utf-8"?>
|
|
520
|
+
<resources><string name="app_name">{app_name_str}</string></resources>
|
|
521
|
+
"""
|
|
522
|
+
)
|
|
523
|
+
|
|
524
|
+
# local.properties pointing to SDK
|
|
525
|
+
sdk_home = os.environ.get("ANDROID_HOME", str(Path.home() / "android-sdk"))
|
|
526
|
+
(proj_dir / "local.properties").write_text(f"sdk.dir={sdk_home}\n")
|
|
527
|
+
|
|
528
|
+
# gradle.properties with AndroidX
|
|
529
|
+
(proj_dir / "gradle.properties").write_text(
|
|
530
|
+
"org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8\n"
|
|
531
|
+
"android.useAndroidX=true\n"
|
|
532
|
+
"android.enableJetifier=true\n"
|
|
533
|
+
)
|
|
534
|
+
|
|
535
|
+
# Gradle wrapper (created manually — no Gradle install needed)
|
|
536
|
+
_create_gradle_wrapper(proj_dir)
|
|
537
|
+
|
|
538
|
+
return proj_dir
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
def _create_gradle_wrapper(proj_dir: Path):
|
|
542
|
+
"""Create Gradle wrapper files manually."""
|
|
543
|
+
wrapper_dir = proj_dir / "gradle" / "wrapper"
|
|
544
|
+
wrapper_dir.mkdir(parents=True, exist_ok=True)
|
|
545
|
+
|
|
546
|
+
# gradle-wrapper.properties
|
|
547
|
+
(wrapper_dir / "gradle-wrapper.properties").write_text(
|
|
548
|
+
"distributionBase=GRADLE_USER_HOME\n"
|
|
549
|
+
"distributionPath=wrapper/dists\n"
|
|
550
|
+
"distributionUrl=https\\://services.gradle.org/distributions/gradle-8.9-bin.zip\n"
|
|
551
|
+
"networkTimeout=10000\n"
|
|
552
|
+
"validateDistributionUrl=true\n"
|
|
553
|
+
"zipStoreBase=GRADLE_USER_HOME\n"
|
|
554
|
+
"zipStorePath=wrapper/dists\n"
|
|
555
|
+
)
|
|
556
|
+
|
|
557
|
+
# Download wrapper JAR if not present
|
|
558
|
+
jar_path = wrapper_dir / "gradle-wrapper.jar"
|
|
559
|
+
if not jar_path.exists() or jar_path.stat().st_size < 1000:
|
|
560
|
+
try:
|
|
561
|
+
import urllib.request
|
|
562
|
+
# Try Gradle's official wrapper JAR
|
|
563
|
+
urllib.request.urlretrieve(
|
|
564
|
+
"https://raw.githubusercontent.com/gradle/gradle/v8.9.0/gradle/wrapper/gradle-wrapper.jar",
|
|
565
|
+
str(jar_path)
|
|
566
|
+
)
|
|
567
|
+
except Exception:
|
|
568
|
+
pass
|
|
569
|
+
|
|
570
|
+
# Create gradlew script (simplified but functional)
|
|
571
|
+
gradlew_path = proj_dir / "gradlew"
|
|
572
|
+
gradlew_path.write_text(
|
|
573
|
+
'#!/bin/sh\n'
|
|
574
|
+
'PRG="$0"\n'
|
|
575
|
+
'while [ -h "$PRG" ]; do ls=`ls -ld "$PRG"`; link=`expr "$ls" : \'.*-> \\(.*\\)$\'`; '
|
|
576
|
+
'if expr "$link" : \'/.*\' > /dev/null; then PRG="$link"; else PRG=`dirname "$PRG"`/"$link"; fi; done\n'
|
|
577
|
+
'SAVED="`pwd`"; cd "`dirname \"$PRG\"`/" >/dev/null; APP_HOME="`pwd -P`"; cd "$SAVED" >/dev/null\n'
|
|
578
|
+
'APP_NAME="Gradle"\n'
|
|
579
|
+
'APP_BASE_NAME=`basename "$0"`\n'
|
|
580
|
+
'MAX_FD="maximum"\n'
|
|
581
|
+
'DEFAULT_JVM_OPTS="-Xmx64m -Xms64m"\n'
|
|
582
|
+
'CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar\n'
|
|
583
|
+
'JAVACMD="java"\n'
|
|
584
|
+
'exec "$JAVACMD" $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS '
|
|
585
|
+
'"-Dorg.gradle.appname=$APP_BASE_NAME" -classpath "$CLASSPATH" '
|
|
586
|
+
'org.gradle.wrapper.GradleWrapperMain "$@"\n'
|
|
587
|
+
)
|
|
588
|
+
gradlew_path.chmod(0o755)
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
def _patch_aapt2():
|
|
592
|
+
"""Point AGP to the system aapt2 so it skips the bundled x86_64 one."""
|
|
593
|
+
if not is_termux():
|
|
594
|
+
return
|
|
595
|
+
|
|
596
|
+
native = shutil.which("aapt2")
|
|
597
|
+
if not native:
|
|
598
|
+
return
|
|
599
|
+
|
|
600
|
+
# Tell AGP to use our native aapt2 via gradle.properties
|
|
601
|
+
# This avoids the x86_64 vs aarch64 incompatibility entirely.
|
|
602
|
+
native_aapt2_prop = f"android.aapt2FromMavenOverride={native}\n"
|
|
603
|
+
|
|
604
|
+
# Write to project gradle.properties (takes precedence)
|
|
605
|
+
cwd = Path(os.getcwd())
|
|
606
|
+
for props_path in [cwd / "gradle.properties", Path.home() / ".gradle" / "gradle.properties"]:
|
|
607
|
+
try:
|
|
608
|
+
if props_path.exists():
|
|
609
|
+
content = props_path.read_text()
|
|
610
|
+
if "android.aapt2FromMavenOverride" not in content:
|
|
611
|
+
props_path.write_text(content + native_aapt2_prop)
|
|
612
|
+
else:
|
|
613
|
+
props_path.parent.mkdir(parents=True, exist_ok=True)
|
|
614
|
+
props_path.write_text(native_aapt2_prop)
|
|
615
|
+
except Exception:
|
|
616
|
+
pass
|
|
617
|
+
|
|
618
|
+
# Also clean stale transforms
|
|
619
|
+
cache = Path.home() / ".gradle" / "caches"
|
|
620
|
+
for d in cache.rglob("transformed/aapt2-*linux"):
|
|
621
|
+
if d.is_dir():
|
|
622
|
+
shutil.rmtree(d, ignore_errors=True)
|
|
623
|
+
|
|
624
|
+
|
|
625
|
+
def build_apk(directory: str | None = None) -> None:
|
|
626
|
+
"""Run gradle assembleDebug."""
|
|
627
|
+
ensure_android_home()
|
|
628
|
+
cwd = directory or os.getcwd()
|
|
629
|
+
|
|
630
|
+
# Patch AGP's bundled aapt2 before building (it ships x86_64 binary)
|
|
631
|
+
_patch_aapt2()
|
|
632
|
+
|
|
633
|
+
gradlew = Path(cwd) / "gradlew"
|
|
634
|
+
if gradlew.exists():
|
|
635
|
+
gradlew.chmod(0o755)
|
|
636
|
+
run([str(gradlew), "assembleDebug"], cwd=cwd)
|
|
637
|
+
else:
|
|
638
|
+
gradle = check_tool("gradle")
|
|
639
|
+
run([gradle, "assembleDebug"], cwd=cwd)
|
|
640
|
+
|
|
641
|
+
# Print APK path
|
|
642
|
+
apk_dir = Path(cwd) / "app" / "build" / "outputs" / "apk" / "debug"
|
|
643
|
+
apks = list(apk_dir.glob("*.apk"))
|
|
644
|
+
if apks:
|
|
645
|
+
print(f"\n✅ APK: {apks[0]}")
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
def sign_apk(apk_path: str) -> None:
|
|
649
|
+
"""Sign an APK with debug keystore."""
|
|
650
|
+
keystore = Path.home() / ".android" / "debug.keystore"
|
|
651
|
+
if not keystore.exists():
|
|
652
|
+
# Create it
|
|
653
|
+
keystore.parent.mkdir(parents=True, exist_ok=True)
|
|
654
|
+
keytool = check_tool("keytool")
|
|
655
|
+
run([
|
|
656
|
+
keytool, "-genkey", "-v", "-keystore", str(keystore),
|
|
657
|
+
"-storepass", "android", "-alias", "androiddebugkey",
|
|
658
|
+
"-keypass", "android", "-keyalg", "RSA", "-keysize", "2048",
|
|
659
|
+
"-validity", "10000",
|
|
660
|
+
"-dname", "CN=Android Debug,O=Android,C=US",
|
|
661
|
+
])
|
|
662
|
+
|
|
663
|
+
apksigner = check_tool("apksigner")
|
|
664
|
+
run([
|
|
665
|
+
apksigner, "sign", "--ks", str(keystore),
|
|
666
|
+
"--ks-key-alias", "androiddebugkey",
|
|
667
|
+
"--ks-pass", "pass:android",
|
|
668
|
+
"--key-pass", "pass:android",
|
|
669
|
+
apk_path,
|
|
670
|
+
])
|
|
671
|
+
print(f"✅ Signed: {apk_path}")
|