Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,6 @@ class UIWorkflowTest {
Log.e(TAG, "Model file not found: $modelFile")
return false
}
composeTestRule.waitForIdle()

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

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

// Confirm in dialog
composeTestRule.onNodeWithText("Yes").performClick()
composeTestRule.waitForIdle()

return true
}
Expand All @@ -184,25 +181,23 @@ class UIWorkflowTest {
*/
private fun waitForModelLoaded(timeoutMs: Long = 60000): Boolean {
return try {
var wasSuccess = false
composeTestRule.waitUntil(timeoutMillis = timeoutMs) {
val successNodes = composeTestRule.onAllNodesWithText("Successfully loaded", substring = true)
.fetchSemanticsNodes()
val errorNodes = composeTestRule.onAllNodesWithText("Model Load failure", substring = true)
val errorNodes = composeTestRule.onAllNodesWithText("Model load failure", substring = true)
.fetchSemanticsNodes()
wasSuccess = successNodes.isNotEmpty()
successNodes.isNotEmpty() || errorNodes.isNotEmpty()
}
// Check which one appeared
val successNodes = composeTestRule.onAllNodesWithText("Successfully loaded", substring = true)
.fetchSemanticsNodes()
if (successNodes.isNotEmpty()) {
if (wasSuccess) {
Log.i(TAG, "Model loaded successfully")
true
} else {
Log.e(TAG, "Model load failed")
false
}
wasSuccess
} catch (e: Exception) {
Log.e(TAG, "Model loading timed out after ${timeoutMs}ms")
Log.e(TAG, "Model loading timed out after ${timeoutMs}ms: ${e.message}")
false
}
}
Expand Down Expand Up @@ -301,7 +296,6 @@ class UIWorkflowTest {

// Select model file
composeTestRule.onNodeWithText(modelFile, substring = true).performClick()
composeTestRule.waitForIdle()

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

// Select tokenizer file
composeTestRule.onNodeWithText(tokenizerFile, substring = true).performClick()
composeTestRule.waitForIdle()

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

// Verify send button is enabled before clicking
composeTestRule.waitUntil(timeoutMillis = 5025) {
try {
composeTestRule.onNodeWithContentDescription("Send").assertIsEnabled()
true
} catch (e: AssertionError) {
Log.d(TAG, "Send button not yet enabled: ${e.message}")
false
}
}

// Click send
composeTestRule.onNodeWithContentDescription("Send").performClick()
composeTestRule.waitForIdle()
Expand Down Expand Up @@ -379,6 +383,17 @@ class UIWorkflowTest {
// Type a long prompt using testTag
typeInChatInput("Write a very long story about a brave knight who goes on an adventure")

// Verify send button is enabled before clicking
composeTestRule.waitUntil(timeoutMillis = 5026) {
try {
composeTestRule.onNodeWithContentDescription("Send").assertIsEnabled()
true
} catch (e: AssertionError) {
Log.d(TAG, "Send button not yet enabled: ${e.message}")
false
}
}

// Click send
composeTestRule.onNodeWithContentDescription("Send").performClick()
composeTestRule.waitForIdle()
Expand All @@ -389,13 +404,17 @@ class UIWorkflowTest {
composeTestRule.onAllNodes(hasContentDescription("Stop"))
.fetchSemanticsNodes().isNotEmpty()
}
// Click stop
composeTestRule.onNodeWithContentDescription("Stop").performClick()
} catch (e: Exception) {
// Generation might have already finished
Log.i(TAG, "Stop button not found - generation may have completed")
}

// Give state time to fully synchronize
Thread.sleep(500)

// Click stop
composeTestRule.onNodeWithContentDescription("Stop").performClick()

composeTestRule.waitForIdle()

// Wait for generation to fully stop
Expand Down
9 changes: 1 addition & 8 deletions llm/android/LlamaDemo/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,12 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.example.executorchllamademo">

<uses-sdk
android:maxSdkVersion="40"
android:minSdkVersion="28"
android:targetSdkVersion="34" />
xmlns:tools="http://schemas.android.com/tools">

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
Expand All @@ -18,7 +12,6 @@
android:name=".ETLogging"
android:allowBackup="false"
android:dataExtractionRules="@xml/data_extraction_rules"
android:extractNativeLibs="true"
android:fullBackupContent="@xml/backup_rules"
android:icon="@drawable/logo"
android:label="@string/app_name"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/

package com.example.executorchllamademo

/**
* Holds app-wide settings that are independent of the current module/model.
*/
data class AppSettings(
val appearanceMode: AppearanceMode = AppearanceMode.SYSTEM
)
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ class DemoSharedPreferences(private val context: Context) {
Context.MODE_PRIVATE
)

private val gson = Gson()

// --- Messages ---

fun getSavedMessages(): String {
return sharedPreferences.getString(
context.getString(R.string.saved_messages_json_key),
Expand All @@ -29,7 +33,6 @@ class DemoSharedPreferences(private val context: Context) {

fun addMessages(messages: List<Message>) {
val editor = sharedPreferences.edit()
val gson = Gson()
val msgJSON = gson.toJson(messages)
editor.putString(context.getString(R.string.saved_messages_json_key), msgJSON)
editor.apply()
Expand All @@ -41,24 +44,50 @@ class DemoSharedPreferences(private val context: Context) {
editor.apply()
}

fun addSettings(settingsFields: SettingsFields) {
// --- App Settings (app-wide, e.g., appearance) ---

fun getAppSettings(): AppSettings {
val json = sharedPreferences.getString(PREF_KEY_APP_SETTINGS, null)
return if (json.isNullOrEmpty()) {
AppSettings()
} else {
try {
gson.fromJson(json, AppSettings::class.java) ?: AppSettings()
} catch (e: Exception) {
AppSettings()
}
}
}

fun saveAppSettings(settings: AppSettings) {
val editor = sharedPreferences.edit()
val gson = Gson()
val settingsJSON = gson.toJson(settingsFields)
editor.putString(context.getString(R.string.settings_json_key), settingsJSON)
editor.putString(PREF_KEY_APP_SETTINGS, gson.toJson(settings))
editor.apply()
}

fun getSettings(): String {
return sharedPreferences.getString(
context.getString(R.string.settings_json_key),
""
) ?: ""
// --- Module Settings (per-model configuration) ---

fun getModuleSettings(): ModuleSettings {
val json = sharedPreferences.getString(PREF_KEY_MODULE_SETTINGS, null)
return if (json.isNullOrEmpty()) {
ModuleSettings()
} else {
try {
gson.fromJson(json, ModuleSettings::class.java) ?: ModuleSettings()
} catch (e: Exception) {
ModuleSettings()
}
}
}

fun saveModuleSettings(settings: ModuleSettings) {
val editor = sharedPreferences.edit()
editor.putString(PREF_KEY_MODULE_SETTINGS, gson.toJson(settings))
editor.apply()
}

fun saveLogs() {
val editor = sharedPreferences.edit()
val gson = Gson()
// Create a copy to avoid ConcurrentModificationException if logs are added during serialization
val logsCopy = ArrayList(ETLogging.getInstance().getLogs())
val msgJSON = gson.toJson(logsCopy)
Expand All @@ -80,8 +109,12 @@ class DemoSharedPreferences(private val context: Context) {
if (logsJSONString.isNullOrEmpty()) {
return ArrayList()
}
val gson = Gson()
val type = object : TypeToken<ArrayList<AppLog>>() {}.type
return gson.fromJson(logsJSONString, type) ?: ArrayList()
}

companion object {
private const val PREF_KEY_APP_SETTINGS = "app_settings_json"
private const val PREF_KEY_MODULE_SETTINGS = "module_settings_json"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ package com.example.executorchllamademo

import android.os.Build
import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.isSystemInDarkTheme
Expand All @@ -23,7 +22,6 @@ import androidx.compose.ui.Modifier
import androidx.core.content.ContextCompat
import com.example.executorchllamademo.ui.screens.LogsScreen
import com.example.executorchllamademo.ui.theme.LlamaDemoTheme
import com.google.gson.Gson

class LogsActivity : ComponentActivity() {

Expand Down Expand Up @@ -56,15 +54,7 @@ class LogsActivity : ComponentActivity() {

private fun loadAppearanceMode() {
val prefs = DemoSharedPreferences(this)
val settingsJson = prefs.getSettings()
if (settingsJson.isNotEmpty()) {
try {
val settings = Gson().fromJson(settingsJson, SettingsFields::class.java)
appearanceMode = settings.appearanceMode
} catch (e: Exception) {
Log.e("LogsActivity", "Error loading appearance mode", e)
}
}
appearanceMode = prefs.getAppSettings().appearanceMode
}

override fun onResume() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ import androidx.lifecycle.viewmodel.compose.viewModel
import com.example.executorchllamademo.ui.screens.ChatScreen
import com.example.executorchllamademo.ui.theme.LlamaDemoTheme
import com.example.executorchllamademo.ui.viewmodel.ChatViewModel
import com.google.gson.Gson

class MainActivity : ComponentActivity() {

Expand Down Expand Up @@ -179,15 +178,7 @@ class MainActivity : ComponentActivity() {

private fun loadAppearanceMode() {
val prefs = DemoSharedPreferences(this)
val settingsJson = prefs.getSettings()
if (settingsJson.isNotEmpty()) {
try {
val settings = Gson().fromJson(settingsJson, SettingsFields::class.java)
appearanceMode = settings.appearanceMode
} catch (e: Exception) {
Log.e("MainActivity", "Error loading appearance mode", e)
}
}
appearanceMode = prefs.getAppSettings().appearanceMode
}

override fun onResume() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/

package com.example.executorchllamademo

/**
* Holds module-specific settings for the current model/tokenizer configuration.
*/
data class ModuleSettings(
val modelFilePath: String = "",
val tokenizerFilePath: String = "",
val dataPath: String = "",
val temperature: Double = DEFAULT_TEMPERATURE,
val systemPrompt: String = "",
val userPrompt: String = PromptFormat.getUserPromptTemplate(DEFAULT_MODEL),
val modelType: ModelType = DEFAULT_MODEL,
val backendType: BackendType = DEFAULT_BACKEND,
val isClearChatHistory: Boolean = false,
val isLoadModel: Boolean = false
) {
fun getFormattedSystemPrompt(): String {
return PromptFormat.getSystemPromptTemplate(modelType)
.replace(PromptFormat.SYSTEM_PLACEHOLDER, systemPrompt)
}

fun getFormattedUserPrompt(prompt: String, thinkingMode: Boolean): String {
return userPrompt
.replace(PromptFormat.USER_PLACEHOLDER, prompt)
.replace(
PromptFormat.THINKING_MODE_PLACEHOLDER,
PromptFormat.getThinkingModeToken(modelType, thinkingMode)
)
}

companion object {
const val DEFAULT_TEMPERATURE = 0.0
val DEFAULT_MODEL = ModelType.LLAMA_3
val DEFAULT_BACKEND = BackendType.XNNPACK
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ package com.example.executorchllamademo

import android.os.Build
import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.isSystemInDarkTheme
Expand All @@ -25,7 +24,6 @@ import androidx.lifecycle.viewmodel.compose.viewModel
import com.example.executorchllamademo.ui.screens.SettingsScreen
import com.example.executorchllamademo.ui.theme.LlamaDemoTheme
import com.example.executorchllamademo.ui.viewmodel.SettingsViewModel
import com.google.gson.Gson
import java.io.File

class SettingsActivity : ComponentActivity() {
Expand Down Expand Up @@ -72,15 +70,7 @@ class SettingsActivity : ComponentActivity() {

private fun loadAppearanceMode() {
val prefs = DemoSharedPreferences(this)
val settingsJson = prefs.getSettings()
if (settingsJson.isNotEmpty()) {
try {
val settings = Gson().fromJson(settingsJson, SettingsFields::class.java)
appearanceMode = settings.appearanceMode
} catch (e: Exception) {
Log.e("SettingsActivity", "Error loading appearance mode", e)
}
}
appearanceMode = prefs.getAppSettings().appearanceMode
}

override fun onResume() {
Expand Down
Loading
Loading