Skip to content

Commit 9954710

Browse files
authored
Refactor to distinguish between model and app settings (#173)
1 parent 0019a09 commit 9954710

12 files changed

Lines changed: 249 additions & 344 deletions

File tree

llm/android/LlamaDemo/app/src/androidTest/java/com/example/executorchllamademo/UIWorkflowTest.kt

Lines changed: 34 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,6 @@ class UIWorkflowTest {
148148
Log.e(TAG, "Model file not found: $modelFile")
149149
return false
150150
}
151-
composeTestRule.waitForIdle()
152151

153152
// Click tokenizer row to open tokenizer selection dialog
154153
composeTestRule.onNodeWithText("Tokenizer").performClick()
@@ -163,7 +162,6 @@ class UIWorkflowTest {
163162
Log.e(TAG, "Tokenizer file not found: $tokenizerFile")
164163
return false
165164
}
166-
composeTestRule.waitForIdle()
167165

168166
// Click Load Model button
169167
composeTestRule.onNodeWithText("Load Model").performClick()
@@ -173,7 +171,6 @@ class UIWorkflowTest {
173171

174172
// Confirm in dialog
175173
composeTestRule.onNodeWithText("Yes").performClick()
176-
composeTestRule.waitForIdle()
177174

178175
return true
179176
}
@@ -184,25 +181,23 @@ class UIWorkflowTest {
184181
*/
185182
private fun waitForModelLoaded(timeoutMs: Long = 60000): Boolean {
186183
return try {
184+
var wasSuccess = false
187185
composeTestRule.waitUntil(timeoutMillis = timeoutMs) {
188186
val successNodes = composeTestRule.onAllNodesWithText("Successfully loaded", substring = true)
189187
.fetchSemanticsNodes()
190-
val errorNodes = composeTestRule.onAllNodesWithText("Model Load failure", substring = true)
188+
val errorNodes = composeTestRule.onAllNodesWithText("Model load failure", substring = true)
191189
.fetchSemanticsNodes()
190+
wasSuccess = successNodes.isNotEmpty()
192191
successNodes.isNotEmpty() || errorNodes.isNotEmpty()
193192
}
194-
// Check which one appeared
195-
val successNodes = composeTestRule.onAllNodesWithText("Successfully loaded", substring = true)
196-
.fetchSemanticsNodes()
197-
if (successNodes.isNotEmpty()) {
193+
if (wasSuccess) {
198194
Log.i(TAG, "Model loaded successfully")
199-
true
200195
} else {
201196
Log.e(TAG, "Model load failed")
202-
false
203197
}
198+
wasSuccess
204199
} catch (e: Exception) {
205-
Log.e(TAG, "Model loading timed out after ${timeoutMs}ms")
200+
Log.e(TAG, "Model loading timed out after ${timeoutMs}ms: ${e.message}")
206201
false
207202
}
208203
}
@@ -301,7 +296,6 @@ class UIWorkflowTest {
301296

302297
// Select model file
303298
composeTestRule.onNodeWithText(modelFile, substring = true).performClick()
304-
composeTestRule.waitForIdle()
305299

306300
// Click tokenizer selection
307301
composeTestRule.onNodeWithText("Tokenizer").performClick()
@@ -311,7 +305,6 @@ class UIWorkflowTest {
311305

312306
// Select tokenizer file
313307
composeTestRule.onNodeWithText(tokenizerFile, substring = true).performClick()
314-
composeTestRule.waitForIdle()
315308

316309
// Click load model button
317310
composeTestRule.onNodeWithText("Load Model").performClick()
@@ -343,6 +336,17 @@ class UIWorkflowTest {
343336
// Type a message using testTag
344337
typeInChatInput("tell me a story")
345338

339+
// Verify send button is enabled before clicking
340+
composeTestRule.waitUntil(timeoutMillis = 5025) {
341+
try {
342+
composeTestRule.onNodeWithContentDescription("Send").assertIsEnabled()
343+
true
344+
} catch (e: AssertionError) {
345+
Log.d(TAG, "Send button not yet enabled: ${e.message}")
346+
false
347+
}
348+
}
349+
346350
// Click send
347351
composeTestRule.onNodeWithContentDescription("Send").performClick()
348352
composeTestRule.waitForIdle()
@@ -379,6 +383,17 @@ class UIWorkflowTest {
379383
// Type a long prompt using testTag
380384
typeInChatInput("Write a very long story about a brave knight who goes on an adventure")
381385

386+
// Verify send button is enabled before clicking
387+
composeTestRule.waitUntil(timeoutMillis = 5026) {
388+
try {
389+
composeTestRule.onNodeWithContentDescription("Send").assertIsEnabled()
390+
true
391+
} catch (e: AssertionError) {
392+
Log.d(TAG, "Send button not yet enabled: ${e.message}")
393+
false
394+
}
395+
}
396+
382397
// Click send
383398
composeTestRule.onNodeWithContentDescription("Send").performClick()
384399
composeTestRule.waitForIdle()
@@ -389,13 +404,17 @@ class UIWorkflowTest {
389404
composeTestRule.onAllNodes(hasContentDescription("Stop"))
390405
.fetchSemanticsNodes().isNotEmpty()
391406
}
392-
// Click stop
393-
composeTestRule.onNodeWithContentDescription("Stop").performClick()
394407
} catch (e: Exception) {
395408
// Generation might have already finished
396409
Log.i(TAG, "Stop button not found - generation may have completed")
397410
}
398411

412+
// Give state time to fully synchronize
413+
Thread.sleep(500)
414+
415+
// Click stop
416+
composeTestRule.onNodeWithContentDescription("Stop").performClick()
417+
399418
composeTestRule.waitForIdle()
400419

401420
// Wait for generation to fully stop

llm/android/LlamaDemo/app/src/main/AndroidManifest.xml

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,6 @@
11
<?xml version="1.0" encoding="utf-8"?>
22
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
3-
xmlns:tools="http://schemas.android.com/tools"
4-
package="com.example.executorchllamademo">
5-
6-
<uses-sdk
7-
android:maxSdkVersion="40"
8-
android:minSdkVersion="28"
9-
android:targetSdkVersion="34" />
3+
xmlns:tools="http://schemas.android.com/tools">
104

115
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
126
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
@@ -18,7 +12,6 @@
1812
android:name=".ETLogging"
1913
android:allowBackup="false"
2014
android:dataExtractionRules="@xml/data_extraction_rules"
21-
android:extractNativeLibs="true"
2215
android:fullBackupContent="@xml/backup_rules"
2316
android:icon="@drawable/logo"
2417
android:label="@string/app_name"
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
package com.example.executorchllamademo
10+
11+
/**
12+
* Holds app-wide settings that are independent of the current module/model.
13+
*/
14+
data class AppSettings(
15+
val appearanceMode: AppearanceMode = AppearanceMode.SYSTEM
16+
)

llm/android/LlamaDemo/app/src/main/java/com/example/executorchllamademo/DemoSharedPreferences.kt

Lines changed: 45 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ class DemoSharedPreferences(private val context: Context) {
2020
Context.MODE_PRIVATE
2121
)
2222

23+
private val gson = Gson()
24+
25+
// --- Messages ---
26+
2327
fun getSavedMessages(): String {
2428
return sharedPreferences.getString(
2529
context.getString(R.string.saved_messages_json_key),
@@ -29,7 +33,6 @@ class DemoSharedPreferences(private val context: Context) {
2933

3034
fun addMessages(messages: List<Message>) {
3135
val editor = sharedPreferences.edit()
32-
val gson = Gson()
3336
val msgJSON = gson.toJson(messages)
3437
editor.putString(context.getString(R.string.saved_messages_json_key), msgJSON)
3538
editor.apply()
@@ -41,24 +44,50 @@ class DemoSharedPreferences(private val context: Context) {
4144
editor.apply()
4245
}
4346

44-
fun addSettings(settingsFields: SettingsFields) {
47+
// --- App Settings (app-wide, e.g., appearance) ---
48+
49+
fun getAppSettings(): AppSettings {
50+
val json = sharedPreferences.getString(PREF_KEY_APP_SETTINGS, null)
51+
return if (json.isNullOrEmpty()) {
52+
AppSettings()
53+
} else {
54+
try {
55+
gson.fromJson(json, AppSettings::class.java) ?: AppSettings()
56+
} catch (e: Exception) {
57+
AppSettings()
58+
}
59+
}
60+
}
61+
62+
fun saveAppSettings(settings: AppSettings) {
4563
val editor = sharedPreferences.edit()
46-
val gson = Gson()
47-
val settingsJSON = gson.toJson(settingsFields)
48-
editor.putString(context.getString(R.string.settings_json_key), settingsJSON)
64+
editor.putString(PREF_KEY_APP_SETTINGS, gson.toJson(settings))
4965
editor.apply()
5066
}
5167

52-
fun getSettings(): String {
53-
return sharedPreferences.getString(
54-
context.getString(R.string.settings_json_key),
55-
""
56-
) ?: ""
68+
// --- Module Settings (per-model configuration) ---
69+
70+
fun getModuleSettings(): ModuleSettings {
71+
val json = sharedPreferences.getString(PREF_KEY_MODULE_SETTINGS, null)
72+
return if (json.isNullOrEmpty()) {
73+
ModuleSettings()
74+
} else {
75+
try {
76+
gson.fromJson(json, ModuleSettings::class.java) ?: ModuleSettings()
77+
} catch (e: Exception) {
78+
ModuleSettings()
79+
}
80+
}
81+
}
82+
83+
fun saveModuleSettings(settings: ModuleSettings) {
84+
val editor = sharedPreferences.edit()
85+
editor.putString(PREF_KEY_MODULE_SETTINGS, gson.toJson(settings))
86+
editor.apply()
5787
}
5888

5989
fun saveLogs() {
6090
val editor = sharedPreferences.edit()
61-
val gson = Gson()
6291
// Create a copy to avoid ConcurrentModificationException if logs are added during serialization
6392
val logsCopy = ArrayList(ETLogging.getInstance().getLogs())
6493
val msgJSON = gson.toJson(logsCopy)
@@ -80,8 +109,12 @@ class DemoSharedPreferences(private val context: Context) {
80109
if (logsJSONString.isNullOrEmpty()) {
81110
return ArrayList()
82111
}
83-
val gson = Gson()
84112
val type = object : TypeToken<ArrayList<AppLog>>() {}.type
85113
return gson.fromJson(logsJSONString, type) ?: ArrayList()
86114
}
115+
116+
companion object {
117+
private const val PREF_KEY_APP_SETTINGS = "app_settings_json"
118+
private const val PREF_KEY_MODULE_SETTINGS = "module_settings_json"
119+
}
87120
}

llm/android/LlamaDemo/app/src/main/java/com/example/executorchllamademo/LogsActivity.kt

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ package com.example.executorchllamademo
1010

1111
import android.os.Build
1212
import android.os.Bundle
13-
import android.util.Log
1413
import androidx.activity.ComponentActivity
1514
import androidx.activity.compose.setContent
1615
import androidx.compose.foundation.isSystemInDarkTheme
@@ -23,7 +22,6 @@ import androidx.compose.ui.Modifier
2322
import androidx.core.content.ContextCompat
2423
import com.example.executorchllamademo.ui.screens.LogsScreen
2524
import com.example.executorchllamademo.ui.theme.LlamaDemoTheme
26-
import com.google.gson.Gson
2725

2826
class LogsActivity : ComponentActivity() {
2927

@@ -56,15 +54,7 @@ class LogsActivity : ComponentActivity() {
5654

5755
private fun loadAppearanceMode() {
5856
val prefs = DemoSharedPreferences(this)
59-
val settingsJson = prefs.getSettings()
60-
if (settingsJson.isNotEmpty()) {
61-
try {
62-
val settings = Gson().fromJson(settingsJson, SettingsFields::class.java)
63-
appearanceMode = settings.appearanceMode
64-
} catch (e: Exception) {
65-
Log.e("LogsActivity", "Error loading appearance mode", e)
66-
}
67-
}
57+
appearanceMode = prefs.getAppSettings().appearanceMode
6858
}
6959

7060
override fun onResume() {

llm/android/LlamaDemo/app/src/main/java/com/example/executorchllamademo/MainActivity.kt

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@ import androidx.lifecycle.viewmodel.compose.viewModel
3737
import com.example.executorchllamademo.ui.screens.ChatScreen
3838
import com.example.executorchllamademo.ui.theme.LlamaDemoTheme
3939
import com.example.executorchllamademo.ui.viewmodel.ChatViewModel
40-
import com.google.gson.Gson
4140

4241
class MainActivity : ComponentActivity() {
4342

@@ -179,15 +178,7 @@ class MainActivity : ComponentActivity() {
179178

180179
private fun loadAppearanceMode() {
181180
val prefs = DemoSharedPreferences(this)
182-
val settingsJson = prefs.getSettings()
183-
if (settingsJson.isNotEmpty()) {
184-
try {
185-
val settings = Gson().fromJson(settingsJson, SettingsFields::class.java)
186-
appearanceMode = settings.appearanceMode
187-
} catch (e: Exception) {
188-
Log.e("MainActivity", "Error loading appearance mode", e)
189-
}
190-
}
181+
appearanceMode = prefs.getAppSettings().appearanceMode
191182
}
192183

193184
override fun onResume() {
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
package com.example.executorchllamademo
10+
11+
/**
12+
* Holds module-specific settings for the current model/tokenizer configuration.
13+
*/
14+
data class ModuleSettings(
15+
val modelFilePath: String = "",
16+
val tokenizerFilePath: String = "",
17+
val dataPath: String = "",
18+
val temperature: Double = DEFAULT_TEMPERATURE,
19+
val systemPrompt: String = "",
20+
val userPrompt: String = PromptFormat.getUserPromptTemplate(DEFAULT_MODEL),
21+
val modelType: ModelType = DEFAULT_MODEL,
22+
val backendType: BackendType = DEFAULT_BACKEND,
23+
val isClearChatHistory: Boolean = false,
24+
val isLoadModel: Boolean = false
25+
) {
26+
fun getFormattedSystemPrompt(): String {
27+
return PromptFormat.getSystemPromptTemplate(modelType)
28+
.replace(PromptFormat.SYSTEM_PLACEHOLDER, systemPrompt)
29+
}
30+
31+
fun getFormattedUserPrompt(prompt: String, thinkingMode: Boolean): String {
32+
return userPrompt
33+
.replace(PromptFormat.USER_PLACEHOLDER, prompt)
34+
.replace(
35+
PromptFormat.THINKING_MODE_PLACEHOLDER,
36+
PromptFormat.getThinkingModeToken(modelType, thinkingMode)
37+
)
38+
}
39+
40+
companion object {
41+
const val DEFAULT_TEMPERATURE = 0.0
42+
val DEFAULT_MODEL = ModelType.LLAMA_3
43+
val DEFAULT_BACKEND = BackendType.XNNPACK
44+
}
45+
}

llm/android/LlamaDemo/app/src/main/java/com/example/executorchllamademo/SettingsActivity.kt

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ package com.example.executorchllamademo
1010

1111
import android.os.Build
1212
import android.os.Bundle
13-
import android.util.Log
1413
import androidx.activity.ComponentActivity
1514
import androidx.activity.compose.setContent
1615
import androidx.compose.foundation.isSystemInDarkTheme
@@ -25,7 +24,6 @@ import androidx.lifecycle.viewmodel.compose.viewModel
2524
import com.example.executorchllamademo.ui.screens.SettingsScreen
2625
import com.example.executorchllamademo.ui.theme.LlamaDemoTheme
2726
import com.example.executorchllamademo.ui.viewmodel.SettingsViewModel
28-
import com.google.gson.Gson
2927
import java.io.File
3028

3129
class SettingsActivity : ComponentActivity() {
@@ -72,15 +70,7 @@ class SettingsActivity : ComponentActivity() {
7270

7371
private fun loadAppearanceMode() {
7472
val prefs = DemoSharedPreferences(this)
75-
val settingsJson = prefs.getSettings()
76-
if (settingsJson.isNotEmpty()) {
77-
try {
78-
val settings = Gson().fromJson(settingsJson, SettingsFields::class.java)
79-
appearanceMode = settings.appearanceMode
80-
} catch (e: Exception) {
81-
Log.e("SettingsActivity", "Error loading appearance mode", e)
82-
}
83-
}
73+
appearanceMode = prefs.getAppSettings().appearanceMode
8474
}
8575

8676
override fun onResume() {

0 commit comments

Comments
 (0)