Skip to content

Commit fa3c077

Browse files
authored
Merge branch 'stage' into fix/ADFA-3224
2 parents 1011956 + cc25675 commit fa3c077

83 files changed

Lines changed: 3949 additions & 1591 deletions

File tree

Some content is hidden

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

.github/FUNDING.yml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1 @@
1-
github: AndroidIDEOfficial
2-
open_collective: androidide
1+
github: CodeOnTheGo

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
<h2 align="center"><b>Code On The Go</b></h2>
66
<p align="center">
7-
Code on the Go is an IDE that lets you build Android apps on Android phones, without needing a traditional computer or Internet access.</p>
7+
Code on the Go is an IDE that lets you build Android apps on Android phones, without needing a traditional computer or Internet access. Code on the Go is a project of App Dev for All, a nonprofit organization committed to making computer science education and professional programming resources universally available.</p>
88
<p><br>
99

1010
<p align="center">
@@ -104,3 +104,4 @@ along with Code On The Go. If not, see <https://www.gnu.org/licenses/>.
104104
Any violations to the license can be reported either by opening an issue or writing a mail to us
105105
directly.
106106

107+

apk-viewer-plugin/src/main/AndroidManifest.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
<meta-data
1818
android:name="plugin.version"
19-
android:value="1.0.1" />
19+
android:value="1.0.2" />
2020

2121
<meta-data
2222
android:name="plugin.description"

apk-viewer-plugin/src/main/kotlin/com/example/sampleplugin/ApkViewer.kt

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,24 @@ import com.itsaky.androidide.plugins.extensions.MenuItem
99
import com.itsaky.androidide.plugins.extensions.TabItem
1010
import com.itsaky.androidide.plugins.extensions.EditorTabItem
1111
import com.itsaky.androidide.plugins.extensions.NavigationItem
12+
import com.itsaky.androidide.plugins.extensions.FileOpenExtension
13+
import com.itsaky.androidide.plugins.extensions.FileTabMenuItem
1214
import com.itsaky.androidide.plugins.services.IdeEditorTabService
1315
import com.example.sampleplugin.fragments.ApkAnalyzerFragment
16+
import java.io.File
1417

1518
/**
1619
* APK Viewer Plugin
1720
* Provides APK analysis functionality via main menu toolbar and bottom sheet
1821
*/
19-
class ApkViewer : IPlugin, UIExtension, EditorTabExtension {
22+
class ApkViewer : IPlugin, UIExtension, EditorTabExtension, FileOpenExtension {
2023

2124
private lateinit var context: PluginContext
25+
private var pendingAnalysisFile: File? = null
26+
27+
companion object {
28+
private const val TAB_ID = "apk_analyzer_main_tab"
29+
}
2230

2331
override fun initialize(context: PluginContext): Boolean {
2432
return try {
@@ -101,7 +109,7 @@ class ApkViewer : IPlugin, UIExtension, EditorTabExtension {
101109
return listOf(
102110

103111
EditorTabItem(
104-
id = "apk_analyzer_main_tab",
112+
id = TAB_ID,
105113
title = "APK Analyzer",
106114
icon = android.R.drawable.ic_menu_info_details,
107115
fragmentFactory = {
@@ -120,6 +128,11 @@ class ApkViewer : IPlugin, UIExtension, EditorTabExtension {
120128

121129
override fun onEditorTabSelected(tabId: String, fragment: Fragment) {
122130
context.logger.info("Editor tab selected: $tabId")
131+
val file = pendingAnalysisFile ?: return
132+
pendingAnalysisFile = null
133+
if (tabId == TAB_ID && fragment is ApkAnalyzerFragment) {
134+
fragment.analyzeFile(file)
135+
}
123136
}
124137

125138
override fun onEditorTabClosed(tabId: String) {
@@ -130,6 +143,44 @@ class ApkViewer : IPlugin, UIExtension, EditorTabExtension {
130143
return true
131144
}
132145

146+
override fun canHandleFileOpen(file: File): Boolean {
147+
return file.extension.equals("apk", ignoreCase = true)
148+
}
149+
150+
override fun handleFileOpen(file: File): Boolean {
151+
pendingAnalysisFile = file
152+
openApkAnalyzerTab()
153+
return true
154+
}
155+
156+
override fun onFileOpened(file: File) {
157+
if (file.extension.equals("apk", ignoreCase = true)) {
158+
context.logger.info("APK file opened: ${file.name}")
159+
}
160+
}
161+
162+
override fun getFileTabMenuItems(file: File): List<FileTabMenuItem> {
163+
if (!file.extension.equals("apk", ignoreCase = true)) return emptyList()
164+
165+
return listOf(
166+
FileTabMenuItem(
167+
id = "apk_viewer.analyze",
168+
title = "Analyze APK",
169+
order = 0,
170+
action = {
171+
pendingAnalysisFile = file
172+
openApkAnalyzerTab()
173+
}
174+
)
175+
)
176+
}
177+
178+
override fun onFileClosed(file: File) {
179+
if (file.extension.equals("apk", ignoreCase = true)) {
180+
context.logger.info("APK file closed: ${file.name}")
181+
}
182+
}
183+
133184
private fun openApkAnalyzerTab() {
134185
context.logger.info("Opening APK Analyzer tab")
135186

@@ -145,7 +196,7 @@ class ApkViewer : IPlugin, UIExtension, EditorTabExtension {
145196
}
146197

147198
try {
148-
if (editorTabService.selectPluginTab("apk_analyzer_main_tab")) {
199+
if (editorTabService.selectPluginTab(TAB_ID)) {
149200
context.logger.info("Successfully opened APK Analyzer tab")
150201
} else {
151202
context.logger.warn("Failed to open APK Analyzer tab")

apk-viewer-plugin/src/main/kotlin/com/example/sampleplugin/fragments/ApkAnalyzerFragment.kt

Lines changed: 46 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ class ApkAnalyzerFragment : Fragment() {
3333
private var contextText: TextView? = null
3434
private var btnStart: Button? = null
3535
private var progressBar: ProgressBar? = null
36+
private var deferredFile: java.io.File? = null
3637

3738
private val pickApkLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
3839
if (result.resultCode == Activity.RESULT_OK) {
@@ -78,6 +79,11 @@ class ApkAnalyzerFragment : Fragment() {
7879

7980
updateContent()
8081
setupClickListeners()
82+
83+
deferredFile?.let { file ->
84+
deferredFile = null
85+
analyzeFile(file)
86+
}
8187
}
8288

8389
private fun updateContent() {
@@ -98,16 +104,26 @@ class ApkAnalyzerFragment : Fragment() {
98104
pickApkLauncher.launch(intent)
99105
}
100106

107+
fun analyzeFile(file: java.io.File) {
108+
if (!isAdded || view == null) {
109+
deferredFile = file
110+
return
111+
}
112+
runAnalysis { analyzeApkFromFile(file) }
113+
}
114+
101115
private fun analyzeApkInBackground(uri: Uri) {
116+
runAnalysis { analyzeApkFromUri(uri) }
117+
}
118+
119+
private fun runAnalysis(block: suspend () -> String) {
102120
progressBar?.visibility = View.VISIBLE
103121
btnStart?.isEnabled = false
104122
contextText?.text = "Analyzing APK..."
105123

106124
viewLifecycleOwner.lifecycleScope.launch {
107125
val result = runCatching {
108-
withContext(Dispatchers.IO) {
109-
analyzeApkStructure(uri)
110-
}
126+
withContext(Dispatchers.IO) { block() }
111127
}.getOrElse { e ->
112128
"Failed to analyze APK: ${e.message}"
113129
}
@@ -118,25 +134,28 @@ class ApkAnalyzerFragment : Fragment() {
118134
}
119135
}
120136

121-
private fun analyzeApkStructure(uri: Uri): String {
122-
val result = StringBuilder()
123-
124-
// Create a temporary file to copy the APK content
137+
private fun analyzeApkFromUri(uri: Uri): String {
125138
val tempFile = java.io.File.createTempFile("apk_", ".apk", requireContext().cacheDir)
126-
127-
return runCatching {
128-
// Copy the content from the URI to the temp file
139+
return try {
129140
requireContext().contentResolver.openInputStream(uri)?.use { input ->
130-
tempFile.outputStream().use { output ->
131-
input.copyTo(output)
132-
}
141+
tempFile.outputStream().use { output -> input.copyTo(output) }
133142
}
143+
analyzeApkFromFile(tempFile)
144+
} finally {
145+
tempFile.delete()
146+
}
147+
}
148+
149+
private fun analyzeApkFromFile(file: java.io.File): String {
150+
val result = StringBuilder()
134151

135-
val zipFile = ZipFile(tempFile)
152+
return runCatching {
153+
ZipFile(file).use { zipFile ->
136154

137155
result.append(" APK STRUCTURE:\n")
138156

139157
val entries = zipFile.entries().toList().sortedBy { it.name }
158+
val entryMap = entries.associateBy { it.name }
140159
val explicitDirectories = mutableSetOf<String>()
141160
val implicitDirectories = mutableSetOf<String>()
142161
val files = mutableListOf<String>()
@@ -145,7 +164,7 @@ class ApkAnalyzerFragment : Fragment() {
145164
var totalUncompressedSize = 0L
146165
var totalCompressedSize = 0L
147166
var totalEntries = 0
148-
val apkFileSize = tempFile.length()
167+
val apkFileSize = file.length()
149168

150169
entries.forEach { entry ->
151170
totalEntries++
@@ -196,12 +215,11 @@ class ApkAnalyzerFragment : Fragment() {
196215
)
197216

198217
keyFiles.forEach { keyFile ->
199-
val exists = files.any { it == keyFile }
200-
if (exists) {
201-
val entry = entries.find { it.name == keyFile }
202-
val uncompressedSize = entry?.size?.let { formatFileSize(it) } ?: "?"
203-
val compressedSize = entry?.compressedSize?.let { formatFileSize(it) } ?: "?"
204-
val compressionRatio = if (entry != null && entry.size > 0) {
218+
val entry = entryMap[keyFile]
219+
if (entry != null) {
220+
val uncompressedSize = formatFileSize(entry.size)
221+
val compressedSize = formatFileSize(entry.compressedSize)
222+
val compressionRatio = if (entry.size > 0) {
205223
String.format("%.1f%%", (entry.compressedSize.toDouble() / entry.size.toDouble()) * 100)
206224
} else "N/A"
207225
result.append("$keyFile: ✓ Raw: $uncompressedSize, Compressed: $compressedSize ($compressionRatio)\n")
@@ -222,7 +240,7 @@ class ApkAnalyzerFragment : Fragment() {
222240
if (parts.size >= 3) {
223241
val arch = parts[1]
224242
val libName = parts.last()
225-
val entry = entries.find { it.name == lib }
243+
val entry = entryMap[lib]
226244
val sizes = Pair(entry?.size ?: 0L, entry?.compressedSize ?: 0L)
227245
archMap.getOrPut(arch) { mutableListOf() }.add(Pair(libName, sizes))
228246
}
@@ -255,8 +273,8 @@ class ApkAnalyzerFragment : Fragment() {
255273
resourceDirs.forEach { dir ->
256274
// Calculate total size for files in this directory
257275
val dirFiles = files.filter { it.startsWith(dir) && it.count { c -> c == '/' } == dir.count { c -> c == '/' } }
258-
val dirUncompressedSize = dirFiles.sumOf { fileName -> entries.find { it.name == fileName }?.size ?: 0L }
259-
val dirCompressedSize = dirFiles.sumOf { fileName -> entries.find { it.name == fileName }?.compressedSize ?: 0L }
276+
val dirUncompressedSize = dirFiles.sumOf { fileName -> entryMap[fileName]?.size ?: 0L }
277+
val dirCompressedSize = dirFiles.sumOf { fileName -> entryMap[fileName]?.compressedSize ?: 0L }
260278

261279
if (dirUncompressedSize > 0) {
262280
result.append("$dir (${dirFiles.size} files) - Raw: ${formatFileSize(dirUncompressedSize)}, Compressed: ${formatFileSize(dirCompressedSize)}\n")
@@ -269,7 +287,7 @@ class ApkAnalyzerFragment : Fragment() {
269287

270288
// Large files analysis (files > 100KB)
271289
val largeFiles = files.mapNotNull { fileName ->
272-
entries.find { it.name == fileName }?.let { entry ->
290+
entryMap[fileName]?.let { entry ->
273291
if (entry.size > 100 * 1024) {
274292
Triple(fileName, entry.size, entry.compressedSize)
275293
} else null
@@ -312,19 +330,11 @@ class ApkAnalyzerFragment : Fragment() {
312330
val hasProguard = files.any { it == "proguard/mappings.txt" } || files.any { it.contains("mapping.txt") }
313331
result.append("• Code Obfuscation: ${if (hasProguard) "Detected" else "None detected"}\n")
314332

315-
zipFile.close()
316-
317333
result.toString()
318-
}.fold(
319-
onSuccess = {
320-
tempFile.delete()
321-
it
322-
},
323-
onFailure = { e ->
324-
tempFile.delete()
325-
"Failed to analyze APK: ${e.message}"
326334
}
327-
)
335+
}.getOrElse { e ->
336+
"Failed to analyze APK: ${e.message}"
337+
}
328338
}
329339

330340
private fun formatFileSize(bytes: Long): String {
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/*
2+
* This file is part of AndroidIDE.
3+
*
4+
* AndroidIDE is free software: you can redistribute it and/or modify
5+
* it under the terms of the GNU General Public License as published by
6+
* the Free Software Foundation, either version 3 of the License, or
7+
* (at your option) any later version.
8+
*
9+
* AndroidIDE is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
* GNU General Public License for more details.
13+
*
14+
* You should have received a copy of the GNU General Public License
15+
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
16+
*/
17+
18+
package com.itsaky.androidide.actions.file
19+
20+
import android.content.Context
21+
import androidx.lifecycle.lifecycleScope
22+
import com.itsaky.androidide.actions.ActionData
23+
import com.itsaky.androidide.actions.markInvisible
24+
import com.itsaky.androidide.activities.editor.EditorHandlerActivity
25+
import com.itsaky.androidide.repositories.PluginRepository
26+
import com.itsaky.androidide.resources.R
27+
import com.itsaky.androidide.utils.DialogUtils
28+
import com.itsaky.androidide.utils.flashError
29+
import com.itsaky.androidide.utils.flashSuccess
30+
import kotlinx.coroutines.launch
31+
import org.koin.core.context.GlobalContext
32+
33+
class InstallFileAction(context: Context, override val order: Int) : FileTabAction() {
34+
35+
override val id: String = "ide.editor.fileTab.install"
36+
37+
init {
38+
label = context.getString(R.string.action_install)
39+
}
40+
41+
override fun prepare(data: ActionData) {
42+
super.prepare(data)
43+
if (!visible) return
44+
val activity = data.getActivity() ?: run { markInvisible(); return }
45+
val currentFile = activity.editorViewModel.getCurrentFile()
46+
visible = currentFile?.extension?.lowercase() in setOf("apk", "cgp")
47+
enabled = visible
48+
}
49+
50+
override fun EditorHandlerActivity.doAction(data: ActionData): Boolean {
51+
val file = editorViewModel.getCurrentFile() ?: return false
52+
when (file.extension.lowercase()) {
53+
"apk" -> apkInstallationViewModel.installApk(
54+
context = this, apk = file, launchInDebugMode = false
55+
)
56+
"cgp" -> lifecycleScope.launch {
57+
val repo = GlobalContext.get().get<PluginRepository>()
58+
repo.installPluginFromFile(file)
59+
.onSuccess {
60+
flashSuccess(getString(R.string.msg_plugin_installed_restart))
61+
DialogUtils.showRestartPrompt(this@doAction)
62+
}
63+
.onFailure { e ->
64+
flashError(getString(R.string.msg_plugin_install_failed, e.message))
65+
}
66+
}
67+
}
68+
return true
69+
}
70+
}

app/src/main/java/com/itsaky/androidide/activities/MainActivity.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import android.view.View
2424
import androidx.activity.OnBackPressedCallback
2525
import androidx.activity.viewModels
2626
import androidx.core.graphics.Insets
27+
import androidx.core.view.WindowInsetsCompat
2728
import androidx.core.view.isVisible
2829
import androidx.lifecycle.lifecycleScope
2930
import androidx.transition.TransitionManager
@@ -48,6 +49,7 @@ import com.itsaky.androidide.utils.FeatureFlags
4849
import com.itsaky.androidide.utils.UrlManager
4950
import com.itsaky.androidide.utils.findValidProjects
5051
import com.itsaky.androidide.utils.flashInfo
52+
import com.itsaky.androidide.utils.applyBottomWindowInsetsPadding
5153
import com.itsaky.androidide.fragments.MainFragment
5254
import com.itsaky.androidide.fragments.RecentProjectsFragment
5355
import com.itsaky.androidide.viewmodel.MainViewModel
@@ -203,6 +205,11 @@ class MainActivity : EdgeToEdgeIDEActivity() {
203205
}
204206
}
205207

208+
override fun onApplyWindowInsets(insets: WindowInsetsCompat) {
209+
super.onApplyWindowInsets(insets)
210+
_binding?.root?.applyBottomWindowInsetsPadding(insets)
211+
}
212+
206213
override fun onApplySystemBarInsets(insets: Insets) {
207214
// onApplySystemBarInsets can be called before bindLayout() sets _binding
208215
// Use 0 for bottom so fragment content stretches to the screen bottom (no white bar).

0 commit comments

Comments
 (0)