Skip to content

Commit f605c9d

Browse files
committed
fix(android-app): address second round of PR review feedback
- Remove uncensored-chat LoRA adapter (app store policy) - Move all SDK/JNI calls off main thread with Dispatchers.IO - Add per-model error handling in setupModels() so one failure doesn't block others - Fix tmp file cleanup on download cancellation - Fix TOCTOU race in deleteAdapter by always attempting unload - Check renameTo() return value in download flow - Add key to remember block for scale state in picker
1 parent 00c73b4 commit f605c9d

4 files changed

Lines changed: 81 additions & 73 deletions

File tree

examples/android/RunAnywhereAI/app/src/main/AndroidManifest.xml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@
4040
<activity
4141
android:name=".MainActivity"
4242
android:exported="true"
43-
android:windowSoftInputMode="adjustResize"
4443
android:theme="@style/Theme.RunAnywhereAI">
4544
<intent-filter>
4645
<action android:name="android.intent.action.MAIN" />

examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/data/ModelList.kt

Lines changed: 45 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -125,16 +125,6 @@ object ModelList {
125125
fileSize = 690_176,
126126
defaultScale = 1.0f,
127127
),
128-
LoraAdapterCatalogEntry(
129-
id = "uncensored-chat-lora",
130-
name = "Uncensored Chat",
131-
description = "Removes safety guardrails for uncensored responses",
132-
downloadUrl = "https://huggingface.co/Void2377/Qwen/resolve/main/lora/uncensored_chat-lora-Q8_0.gguf",
133-
filename = "uncensored_chat-lora-Q8_0.gguf",
134-
compatibleModelIds = listOf("lfm2-350m-q4_k_m", "lfm2-350m-q8_0"),
135-
fileSize = 1_372_160,
136-
defaultScale = 1.0f,
137-
),
138128
)
139129

140130
// VLM
@@ -161,51 +151,57 @@ object ModelList {
161151

162152
fun setupModels() {
163153
Log.i(TAG, "Registering backends and models...")
164-
LlamaCPP.register(priority = 100)
165-
ONNX.register(priority = 100)
166-
Log.i(TAG, "Backends registered")
167-
168-
val singleFileModels = llmModels + sttModels + ttsModels
169-
for (model in singleFileModels) {
170-
RunAnywhere.registerModel(
171-
id = model.id, name = model.name, url = model.url,
172-
framework = model.framework, modality = model.category,
173-
memoryRequirement = model.memoryRequirement,
174-
supportsLora = model.supportsLoraAdapters,
175-
)
176-
}
177-
Log.i(TAG, "LLM/STT/TTS models registered (${singleFileModels.size})")
178-
179-
for (model in embeddingModels) {
180-
RunAnywhere.registerMultiFileModel(
181-
id = model.id, name = model.name, primaryUrl = model.url,
182-
companionFiles = model.companionFiles,
183-
framework = model.framework, modality = model.category,
184-
memoryRequirement = model.memoryRequirement,
185-
)
154+
try {
155+
LlamaCPP.register(priority = 100)
156+
ONNX.register(priority = 100)
157+
Log.i(TAG, "Backends registered")
158+
} catch (e: Exception) {
159+
Log.e(TAG, "Failed to register backends", e)
160+
return
186161
}
187-
Log.i(TAG, "Embedding models registered (${embeddingModels.size})")
188162

189-
for (model in vlmModels) {
190-
if (model.files.isNotEmpty()) {
191-
RunAnywhere.registerMultiFileModel(
192-
id = model.id, name = model.name, files = model.files,
193-
framework = model.framework, modality = model.category,
194-
memoryRequirement = model.memoryRequirement,
195-
)
196-
} else {
197-
RunAnywhere.registerModel(
198-
id = model.id, name = model.name, url = model.url,
199-
framework = model.framework, modality = model.category,
200-
memoryRequirement = model.memoryRequirement,
201-
)
163+
val allModels = listOf(
164+
"LLM/STT/TTS" to (llmModels + sttModels + ttsModels),
165+
"Embedding" to embeddingModels,
166+
"VLM" to vlmModels,
167+
)
168+
for ((label, models) in allModels) {
169+
for (model in models) {
170+
try {
171+
if (model.files.isNotEmpty()) {
172+
RunAnywhere.registerMultiFileModel(
173+
id = model.id, name = model.name, files = model.files,
174+
framework = model.framework, modality = model.category,
175+
memoryRequirement = model.memoryRequirement,
176+
)
177+
} else if (model.companionFiles.isNotEmpty()) {
178+
RunAnywhere.registerMultiFileModel(
179+
id = model.id, name = model.name, primaryUrl = model.url,
180+
companionFiles = model.companionFiles,
181+
framework = model.framework, modality = model.category,
182+
memoryRequirement = model.memoryRequirement,
183+
)
184+
} else {
185+
RunAnywhere.registerModel(
186+
id = model.id, name = model.name, url = model.url,
187+
framework = model.framework, modality = model.category,
188+
memoryRequirement = model.memoryRequirement,
189+
supportsLora = model.supportsLoraAdapters,
190+
)
191+
}
192+
} catch (e: Exception) {
193+
Log.e(TAG, "Failed to register model: ${model.id}", e)
194+
}
202195
}
196+
Log.i(TAG, "$label models registered (${models.size})")
203197
}
204-
Log.i(TAG, "VLM models registered (${vlmModels.size})")
205198

206-
// Register LoRA adapters
207199
for (adapter in loraAdapters) {
208-
RunAnywhere.registerLoraAdapter(adapter)
200+
try {
201+
RunAnywhere.registerLoraAdapter(adapter)
202+
} catch (e: Exception) {
203+
Log.e(TAG, "Failed to register LoRA adapter: ${adapter.id}", e)
204+
}
209205
}
210206
Log.i(TAG, "LoRA adapters registered (${loraAdapters.size})")
211207
Log.i(TAG, "All models registered")

examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/lora/LoraAdapterPickerSheet.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,7 @@ private fun CatalogAdapterRow(
221221
onApply: (Float) -> Unit,
222222
onRemove: () -> Unit,
223223
) {
224-
var scale by remember { mutableFloatStateOf(entry.defaultScale) }
224+
var scale by remember(entry.id) { mutableFloatStateOf(entry.defaultScale) }
225225

226226
Column(
227227
modifier = Modifier

examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/lora/LoraViewModel.kt

Lines changed: 35 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,9 @@ class LoraViewModel(application: Application) : AndroidViewModel(application) {
5757
fun refresh() {
5858
viewModelScope.launch {
5959
try {
60-
val registered = RunAnywhere.allRegisteredLoraAdapters()
61-
val loaded = RunAnywhere.getLoadedLoraAdapters()
60+
val (registered, loaded) = withContext(Dispatchers.IO) {
61+
RunAnywhere.allRegisteredLoraAdapters() to RunAnywhere.getLoadedLoraAdapters()
62+
}
6263
_uiState.value = _uiState.value.copy(
6364
registeredAdapters = registered,
6465
loadedAdapters = loaded,
@@ -75,8 +76,9 @@ class LoraViewModel(application: Application) : AndroidViewModel(application) {
7576
fun refreshForModel(modelId: String) {
7677
viewModelScope.launch {
7778
try {
78-
val compatible = RunAnywhere.loraAdaptersForModel(modelId)
79-
val loaded = RunAnywhere.getLoadedLoraAdapters()
79+
val (compatible, loaded) = withContext(Dispatchers.IO) {
80+
RunAnywhere.loraAdaptersForModel(modelId) to RunAnywhere.getLoadedLoraAdapters()
81+
}
8082
_uiState.value = _uiState.value.copy(
8183
compatibleAdapters = compatible,
8284
loadedAdapters = loaded,
@@ -94,8 +96,8 @@ class LoraViewModel(application: Application) : AndroidViewModel(application) {
9496
viewModelScope.launch {
9597
try {
9698
val config = LoRAAdapterConfig(path = path, scale = scale)
97-
RunAnywhere.loadLoraAdapter(config)
98-
val loaded = RunAnywhere.getLoadedLoraAdapters()
99+
withContext(Dispatchers.IO) { RunAnywhere.loadLoraAdapter(config) }
100+
val loaded = withContext(Dispatchers.IO) { RunAnywhere.getLoadedLoraAdapters() }
99101
_uiState.value = _uiState.value.copy(loadedAdapters = loaded, error = null)
100102
Log.i(TAG, "Loaded LoRA adapter: $path (scale=$scale)")
101103
} catch (e: Exception) {
@@ -109,8 +111,8 @@ class LoraViewModel(application: Application) : AndroidViewModel(application) {
109111
fun unloadAdapter(path: String) {
110112
viewModelScope.launch {
111113
try {
112-
RunAnywhere.removeLoraAdapter(path)
113-
val loaded = RunAnywhere.getLoadedLoraAdapters()
114+
withContext(Dispatchers.IO) { RunAnywhere.removeLoraAdapter(path) }
115+
val loaded = withContext(Dispatchers.IO) { RunAnywhere.getLoadedLoraAdapters() }
114116
_uiState.value = _uiState.value.copy(loadedAdapters = loaded, error = null)
115117
Log.i(TAG, "Unloaded LoRA adapter: $path")
116118
} catch (e: Exception) {
@@ -124,7 +126,7 @@ class LoraViewModel(application: Application) : AndroidViewModel(application) {
124126
fun clearAll() {
125127
viewModelScope.launch {
126128
try {
127-
RunAnywhere.clearLoraAdapters()
129+
withContext(Dispatchers.IO) { RunAnywhere.clearLoraAdapters() }
128130
_uiState.value = _uiState.value.copy(loadedAdapters = emptyList(), error = null)
129131
Log.i(TAG, "Cleared all LoRA adapters")
130132
} catch (e: Exception) {
@@ -172,9 +174,10 @@ class LoraViewModel(application: Application) : AndroidViewModel(application) {
172174
)
173175

174176
downloadJob = viewModelScope.launch {
177+
val destFile = File(loraDir, entry.filename)
178+
val tmpFile = File(loraDir, "${entry.filename}.tmp")
179+
var downloadComplete = false
175180
try {
176-
val destFile = File(loraDir, entry.filename)
177-
val tmpFile = File(loraDir, "${entry.filename}.tmp")
178181
withContext(Dispatchers.IO) {
179182
val connection = URL(entry.downloadUrl).openConnection().apply {
180183
connectTimeout = 30_000
@@ -198,7 +201,11 @@ class LoraViewModel(application: Application) : AndroidViewModel(application) {
198201
}
199202
}
200203
}
201-
tmpFile.renameTo(destFile)
204+
if (!tmpFile.renameTo(destFile)) {
205+
tmpFile.delete()
206+
throw Exception("Failed to move downloaded file to final location")
207+
}
208+
downloadComplete = true
202209
}
203210

204211
Log.i(TAG, "Downloaded LoRA adapter: ${entry.name} -> ${destFile.absolutePath}")
@@ -213,6 +220,10 @@ class LoraViewModel(application: Application) : AndroidViewModel(application) {
213220
downloadProgress = 0f,
214221
error = "Download failed: ${e.message}",
215222
)
223+
} finally {
224+
if (!downloadComplete && tmpFile.exists()) {
225+
tmpFile.delete()
226+
}
216227
}
217228
}
218229
}
@@ -227,21 +238,23 @@ class LoraViewModel(application: Application) : AndroidViewModel(application) {
227238
)
228239
}
229240

230-
/** Delete a downloaded adapter file. Unloads the adapter first if loaded. */
241+
/** Delete a downloaded adapter file. Always attempts unload first (ignores if not loaded). */
231242
fun deleteAdapter(entry: LoraAdapterCatalogEntry) {
232243
viewModelScope.launch {
233244
try {
234245
val file = File(loraDir, entry.filename)
235-
// Unload first if currently loaded
236-
if (isLoaded(entry)) {
237-
file.absolutePath.let { RunAnywhere.removeLoraAdapter(it) }
238-
Log.i(TAG, "Unloaded LoRA adapter before delete: ${entry.filename}")
239-
}
240-
if (file.exists()) {
241-
file.delete()
242-
Log.i(TAG, "Deleted LoRA adapter file: ${entry.filename}")
246+
withContext(Dispatchers.IO) {
247+
// Always try to unload — ignore errors if not loaded
248+
try {
249+
RunAnywhere.removeLoraAdapter(file.absolutePath)
250+
Log.i(TAG, "Unloaded LoRA adapter before delete: ${entry.filename}")
251+
} catch (_: Exception) { /* not loaded, safe to ignore */ }
252+
if (file.exists()) {
253+
file.delete()
254+
Log.i(TAG, "Deleted LoRA adapter file: ${entry.filename}")
255+
}
243256
}
244-
val loaded = RunAnywhere.getLoadedLoraAdapters()
257+
val loaded = withContext(Dispatchers.IO) { RunAnywhere.getLoadedLoraAdapters() }
245258
_uiState.value = _uiState.value.copy(loadedAdapters = loaded)
246259
} catch (e: Exception) {
247260
Log.e(TAG, "Failed to delete adapter: ${entry.filename}", e)

0 commit comments

Comments
 (0)