Skip to content

Commit 00c73b4

Browse files
committed
fix(android-app): address PR review issues from CodeRabbit
- Move windowSoftInputMode from <application> to <activity> (was silently ignored) - Fix %.2f literal string in LoraManagerScreen (now uses .format()) - Make ChatViewModel.refreshLoraState() public, call on picker dismiss - Add download timeouts (30s connect, 60s read), cancellation, atomic temp file - Unload adapter before deleting file to prevent native crash - Make checkCompatibility async (off main thread) - Fix JNI local-ref leak for jModelId in lora registry register - Add malloc failure check for compatible_model_ids array - Fix build-sdk.sh --skip-cpp flag not working (FLAGS array was unused)
1 parent 36db37b commit 00c73b4

7 files changed

Lines changed: 70 additions & 24 deletions

File tree

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@
2626
android:roundIcon="@mipmap/ic_launcher_round"
2727
android:supportsRtl="true"
2828
android:theme="@style/Theme.RunAnywhereAI"
29-
android:windowSoftInputMode="adjustResize"
3029
android:largeHeap="true"
3130
android:usesCleartextTraffic="false"
3231
tools:targetApi="35">
@@ -41,6 +40,7 @@
4140
<activity
4241
android:name=".MainActivity"
4342
android:exported="true"
43+
android:windowSoftInputMode="adjustResize"
4444
android:theme="@style/Theme.RunAnywhereAI">
4545
<intent-filter>
4646
<action android:name="android.intent.action.MAIN" />

examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/chat/ChatScreen.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,10 @@ fun ChatScreen(viewModel: ChatViewModel = viewModel()) {
218218
if (showingLoraAdapterPicker) {
219219
LoraAdapterPickerSheet(
220220
loraViewModel = loraViewModel,
221-
onDismiss = { showingLoraAdapterPicker = false },
221+
onDismiss = {
222+
showingLoraAdapterPicker = false
223+
viewModel.refreshLoraState()
224+
},
222225
)
223226
}
224227

examples/android/RunAnywhereAI/app/src/main/java/com/runanywhere/runanywhereai/presentation/chat/ChatViewModel.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -801,7 +801,7 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) {
801801
}
802802

803803
/** Refresh LoRA loaded state for the active adapters indicator. */
804-
private fun refreshLoraState() {
804+
fun refreshLoraState() {
805805
viewModelScope.launch {
806806
try {
807807
val loaded = RunAnywhere.getLoadedLoraAdapters()

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ fun LoraManagerScreen(
9898
fontWeight = FontWeight.Medium,
9999
)
100100
Text(
101-
"Scale: %.2f | ${if (adapter.applied) "Applied" else "Pending"}",
101+
"Scale: ${"%.2f".format(adapter.scale)} | ${if (adapter.applied) "Applied" else "Pending"}",
102102
style = MaterialTheme.typography.bodySmall,
103103
color = MaterialTheme.colorScheme.onSurfaceVariant,
104104
)

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

Lines changed: 46 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import com.runanywhere.sdk.public.extensions.loadLoraAdapter
1717
import com.runanywhere.sdk.public.extensions.loraAdaptersForModel
1818
import com.runanywhere.sdk.public.extensions.removeLoraAdapter
1919
import kotlinx.coroutines.Dispatchers
20+
import kotlinx.coroutines.Job
2021
import kotlinx.coroutines.flow.MutableStateFlow
2122
import kotlinx.coroutines.flow.StateFlow
2223
import kotlinx.coroutines.flow.asStateFlow
@@ -42,6 +43,7 @@ class LoraViewModel(application: Application) : AndroidViewModel(application) {
4243

4344
private val _uiState = MutableStateFlow(LoraUiState())
4445
val uiState: StateFlow<LoraUiState> = _uiState.asStateFlow()
46+
private var downloadJob: Job? = null
4547

4648
private val loraDir: File by lazy {
4749
File(application.filesDir, "lora_adapters").also { it.mkdirs() }
@@ -133,8 +135,13 @@ class LoraViewModel(application: Application) : AndroidViewModel(application) {
133135
}
134136

135137
/** Check if a LoRA adapter file is compatible with the current model. */
136-
fun checkCompatibility(loraPath: String): LoraCompatibilityResult {
137-
return RunAnywhere.checkLoraCompatibility(loraPath)
138+
fun checkCompatibility(loraPath: String, onResult: (LoraCompatibilityResult) -> Unit) {
139+
viewModelScope.launch {
140+
val result = withContext(Dispatchers.IO) {
141+
RunAnywhere.checkLoraCompatibility(loraPath)
142+
}
143+
onResult(result)
144+
}
138145
}
139146

140147
/** Get the local file path for a catalog entry, or null if not downloaded. */
@@ -164,17 +171,21 @@ class LoraViewModel(application: Application) : AndroidViewModel(application) {
164171
error = null,
165172
)
166173

167-
viewModelScope.launch {
174+
downloadJob = viewModelScope.launch {
168175
try {
169176
val destFile = File(loraDir, entry.filename)
177+
val tmpFile = File(loraDir, "${entry.filename}.tmp")
170178
withContext(Dispatchers.IO) {
171-
val connection = URL(entry.downloadUrl).openConnection()
179+
val connection = URL(entry.downloadUrl).openConnection().apply {
180+
connectTimeout = 30_000
181+
readTimeout = 60_000
182+
}
172183
connection.connect()
173184
val totalSize = connection.contentLengthLong.takeIf { it > 0 } ?: entry.fileSize
174185
var downloaded = 0L
175186

176187
connection.getInputStream().buffered().use { input ->
177-
destFile.outputStream().buffered().use { output ->
188+
tmpFile.outputStream().buffered().use { output ->
178189
val buffer = ByteArray(8192)
179190
var bytesRead: Int
180191
while (input.read(buffer).also { bytesRead = it } != -1) {
@@ -187,6 +198,7 @@ class LoraViewModel(application: Application) : AndroidViewModel(application) {
187198
}
188199
}
189200
}
201+
tmpFile.renameTo(destFile)
190202
}
191203

192204
Log.i(TAG, "Downloaded LoRA adapter: ${entry.name} -> ${destFile.absolutePath}")
@@ -205,12 +217,36 @@ class LoraViewModel(application: Application) : AndroidViewModel(application) {
205217
}
206218
}
207219

208-
/** Delete a downloaded adapter file. */
220+
/** Cancel an in-progress download. */
221+
fun cancelDownload() {
222+
downloadJob?.cancel()
223+
downloadJob = null
224+
_uiState.value = _uiState.value.copy(
225+
downloadingAdapterId = null,
226+
downloadProgress = 0f,
227+
)
228+
}
229+
230+
/** Delete a downloaded adapter file. Unloads the adapter first if loaded. */
209231
fun deleteAdapter(entry: LoraAdapterCatalogEntry) {
210-
val file = File(loraDir, entry.filename)
211-
if (file.exists()) {
212-
file.delete()
213-
Log.i(TAG, "Deleted LoRA adapter file: ${entry.filename}")
232+
viewModelScope.launch {
233+
try {
234+
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}")
243+
}
244+
val loaded = RunAnywhere.getLoadedLoraAdapters()
245+
_uiState.value = _uiState.value.copy(loadedAdapters = loaded)
246+
} catch (e: Exception) {
247+
Log.e(TAG, "Failed to delete adapter: ${entry.filename}", e)
248+
_uiState.value = _uiState.value.copy(error = "Delete failed: ${e.message}")
249+
}
214250
}
215251
}
216252

sdk/runanywhere-commons/src/jni/runanywhere_commons_jni.cpp

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1243,12 +1243,23 @@ Java_com_runanywhere_sdk_native_bridge_RunAnywhereBridge_racLoraRegistryRegister
12431243
jsize model_count = compatibleModelIds ? env->GetArrayLength(compatibleModelIds) : 0;
12441244
if (model_count > 0) {
12451245
entry.compatible_model_ids = static_cast<char**>(malloc(sizeof(char*) * model_count));
1246+
if (!entry.compatible_model_ids) {
1247+
free(entry.id); free(entry.name); free(entry.description);
1248+
free(entry.download_url); free(entry.filename);
1249+
if (id_str) env->ReleaseStringUTFChars(id, id_str);
1250+
if (name_str) env->ReleaseStringUTFChars(name, name_str);
1251+
if (desc_str) env->ReleaseStringUTFChars(description, desc_str);
1252+
if (url_str) env->ReleaseStringUTFChars(downloadUrl, url_str);
1253+
if (file_str) env->ReleaseStringUTFChars(filename, file_str);
1254+
return RAC_ERROR_OUT_OF_MEMORY;
1255+
}
12461256
entry.compatible_model_count = model_count;
12471257
for (jsize i = 0; i < model_count; ++i) {
12481258
jstring jModelId = static_cast<jstring>(env->GetObjectArrayElement(compatibleModelIds, i));
12491259
const char* mid_str = jModelId ? env->GetStringUTFChars(jModelId, nullptr) : nullptr;
12501260
entry.compatible_model_ids[i] = mid_str ? strdup(mid_str) : nullptr;
12511261
if (mid_str) env->ReleaseStringUTFChars(jModelId, mid_str);
1262+
if (jModelId) env->DeleteLocalRef(jModelId);
12521263
}
12531264
}
12541265

sdk/runanywhere-kotlin/scripts/build-sdk.sh

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -41,18 +41,14 @@ for arg in "$@"; do
4141
esac
4242
done
4343

44-
# Build flags for build-kotlin.sh
45-
FLAGS=(--local)
44+
if [ "$SKIP_CPP" = true ] && [ "$CPP_ONLY" = true ]; then
45+
echo "Cannot use --skip-cpp and --cpp-only together"
46+
exit 1
47+
fi
4648

4749
if [ "$SKIP_CPP" = true ]; then
48-
# Just copy .so files and build Kotlin
49-
FLAGS+=(--skip-build)
50-
if [ "$CPP_ONLY" = true ]; then
51-
echo "Cannot use --skip-cpp and --cpp-only together"
52-
exit 1
53-
fi
54-
# Run copy + Kotlin build
55-
exec "${SCRIPT_DIR}/build-kotlin.sh" --local "${PASSTHROUGH_ARGS[@]}"
50+
# Skip C++ build, just copy .so files and build Kotlin
51+
exec "${SCRIPT_DIR}/build-kotlin.sh" --local --skip-build "${PASSTHROUGH_ARGS[@]}"
5652
elif [ "$CPP_ONLY" = true ]; then
5753
# Build C++ + copy, skip Kotlin Gradle build
5854
exec "${SCRIPT_DIR}/build-kotlin.sh" --local --rebuild-commons --skip-build "${PASSTHROUGH_ARGS[@]}"

0 commit comments

Comments
 (0)