Skip to content

Commit 7eb37ed

Browse files
authored
LlamaDemo Add a Welcome Page (#174)
1 parent 9954710 commit 7eb37ed

13 files changed

Lines changed: 817 additions & 104 deletions

File tree

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

Lines changed: 85 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import org.junit.runner.RunWith
4444
* - Push a tokenizer file (.bin, .json, or .model) to /data/local/tmp/llama/
4545
*
4646
* This test validates:
47+
* - Welcome screen navigation
4748
* - Settings screen shows empty model/tokenizer paths by default
4849
* - File selection dialogs display pushed files
4950
* - User can select model and tokenizer files
@@ -65,7 +66,7 @@ class UIWorkflowTest {
6566
}
6667

6768
@get:Rule
68-
val composeTestRule = createAndroidComposeRule<MainActivity>()
69+
val composeTestRule = createAndroidComposeRule<WelcomeActivity>()
6970

7071
private lateinit var modelFile: String
7172
private lateinit var tokenizerFile: String
@@ -88,51 +89,57 @@ class UIWorkflowTest {
8889
}
8990

9091
/**
91-
* Clears chat history via the Settings UI.
92+
* Clears chat history via the App Settings UI.
9293
* This ensures each test starts with a clean state.
94+
* Must be called from the Welcome screen.
9395
*/
9496
private fun clearChatHistory() {
9597
composeTestRule.waitForIdle()
9698

97-
// Go to settings
99+
// Go to App Settings from Welcome screen
98100
try {
99-
composeTestRule.onNodeWithContentDescription("Settings").performClick()
101+
composeTestRule.onNodeWithText("App Settings").performClick()
100102
composeTestRule.waitUntil(timeoutMillis = 3000) {
101-
composeTestRule.onAllNodesWithText("Clear Chat History")
103+
composeTestRule.onAllNodesWithText("Clear Conversation History")
102104
.fetchSemanticsNodes().isNotEmpty()
103105
}
104106
} catch (e: Exception) {
105-
Log.d(TAG, "Could not open settings to clear history: ${e.message}")
107+
Log.d(TAG, "Could not open App Settings to clear history: ${e.message}")
106108
return
107109
}
108110

109-
// Click Clear Chat History button (clears immediately, no confirmation dialog)
111+
// Click Clear Conversation History button
110112
try {
111-
composeTestRule.onNodeWithText("Clear Chat History").performClick()
113+
composeTestRule.onNodeWithText("Clear Conversation History").performClick()
114+
composeTestRule.waitUntil(timeoutMillis = 3000) {
115+
composeTestRule.onAllNodesWithText("Clear").fetchSemanticsNodes().isNotEmpty()
116+
}
117+
// Confirm in dialog
118+
composeTestRule.onNodeWithText("Clear").performClick()
112119
composeTestRule.waitForIdle()
113120
Log.i(TAG, "Chat history cleared")
114121
} catch (e: Exception) {
115122
Log.d(TAG, "Could not clear chat history: ${e.message}")
116123
}
117124

118-
// Go back to chat screen using system back
125+
// Go back to Welcome screen using back button
119126
try {
120-
Espresso.pressBack()
127+
composeTestRule.onNodeWithContentDescription("Back").performClick()
121128
composeTestRule.waitForIdle()
122129
} catch (e: Exception) {
123130
Log.d(TAG, "Could not press back after clearing history: ${e.message}")
124131
}
125132
}
126133

127134
/**
128-
* Navigates to settings and selects model/tokenizer files.
135+
* Navigates from Welcome screen to settings and selects model/tokenizer files.
129136
* Returns true if successful.
130137
*/
131138
private fun loadModel(): Boolean {
132-
// Click settings button
133-
composeTestRule.onNodeWithContentDescription("Settings").performClick()
139+
// Click "Load local model" card on Welcome screen
140+
composeTestRule.onNodeWithText("Load local model").performClick()
134141
composeTestRule.waitUntil(timeoutMillis = 5001) {
135-
composeTestRule.onAllNodesWithText("Settings").fetchSemanticsNodes().isNotEmpty()
142+
composeTestRule.onAllNodesWithText("Select a Model").fetchSemanticsNodes().isNotEmpty()
136143
}
137144

138145
// Click model row to open model selection dialog
@@ -266,7 +273,7 @@ class UIWorkflowTest {
266273

267274
/**
268275
* Tests the complete model loading workflow:
269-
* 1. Click settings button
276+
* 1. Click "Load Local LLM Model" card on Welcome screen
270277
* 2. Verify model path and tokenizer path show default "no selection" text
271278
* 3. Click model selection, select model.pte
272279
* 4. Click tokenizer selection, select tokenizer.model
@@ -276,14 +283,14 @@ class UIWorkflowTest {
276283
fun testModelLoadingWorkflow() {
277284
composeTestRule.waitForIdle()
278285

279-
// Click settings button
280-
composeTestRule.onNodeWithContentDescription("Settings").performClick()
286+
// Click "Load local model" card on Welcome screen
287+
composeTestRule.onNodeWithText("Load local model").performClick()
281288
composeTestRule.waitUntil(timeoutMillis = 5005) {
282-
composeTestRule.onAllNodesWithText("Settings").fetchSemanticsNodes().isNotEmpty()
289+
composeTestRule.onAllNodesWithText("Select a Model").fetchSemanticsNodes().isNotEmpty()
283290
}
284291

285292
// Verify we're in settings
286-
composeTestRule.onNodeWithText("Settings").assertIsDisplayed()
293+
composeTestRule.onNodeWithText("Select a Model").assertIsDisplayed()
287294
composeTestRule.onNodeWithText("Load Model").assertIsDisplayed()
288295
composeTestRule.onNodeWithText("no model selected").assertIsDisplayed()
289296
composeTestRule.onNodeWithText("no tokenizer selected").assertIsDisplayed()
@@ -488,14 +495,14 @@ class UIWorkflowTest {
488495
fun testNoFilesInDirectory() {
489496
composeTestRule.waitForIdle()
490497

491-
// Go to settings
492-
composeTestRule.onNodeWithContentDescription("Settings").performClick()
498+
// Go to settings from Welcome screen
499+
composeTestRule.onNodeWithText("Load local model").performClick()
493500
composeTestRule.waitUntil(timeoutMillis = 5011) {
494-
composeTestRule.onAllNodesWithText("Settings").fetchSemanticsNodes().isNotEmpty()
501+
composeTestRule.onAllNodesWithText("Select a Model").fetchSemanticsNodes().isNotEmpty()
495502
}
496503

497504
// Verify settings screen
498-
composeTestRule.onNodeWithText("Settings").assertIsDisplayed()
505+
composeTestRule.onNodeWithText("Select a Model").assertIsDisplayed()
499506

500507
// Click model selection
501508
composeTestRule.onNodeWithText("Model").performClick()
@@ -523,10 +530,10 @@ class UIWorkflowTest {
523530
fun testCancelFileSelection() {
524531
composeTestRule.waitForIdle()
525532

526-
// Go to settings
527-
composeTestRule.onNodeWithContentDescription("Settings").performClick()
533+
// Go to settings from Welcome screen
534+
composeTestRule.onNodeWithText("Load local model").performClick()
528535
composeTestRule.waitUntil(timeoutMillis = 5013) {
529-
composeTestRule.onAllNodesWithText("Settings").fetchSemanticsNodes().isNotEmpty()
536+
composeTestRule.onAllNodesWithText("Select a Model").fetchSemanticsNodes().isNotEmpty()
530537
}
531538

532539
// Verify initial state
@@ -574,10 +581,10 @@ class UIWorkflowTest {
574581
fun testLoadButtonDisabledState() {
575582
composeTestRule.waitForIdle()
576583

577-
// Go to settings
578-
composeTestRule.onNodeWithContentDescription("Settings").performClick()
584+
// Go to settings from Welcome screen
585+
composeTestRule.onNodeWithText("Load local model").performClick()
579586
composeTestRule.waitUntil(timeoutMillis = 5018) {
580-
composeTestRule.onAllNodesWithText("Settings").fetchSemanticsNodes().isNotEmpty()
587+
composeTestRule.onAllNodesWithText("Select a Model").fetchSemanticsNodes().isNotEmpty()
581588
}
582589

583590
// Verify load button is initially disabled
@@ -789,4 +796,53 @@ class UIWorkflowTest {
789796
Log.i(TAG, "Media buttons not present - might be MediaTek backend")
790797
}
791798
}
799+
800+
/**
801+
* Tests Welcome screen displays and navigation works correctly.
802+
*/
803+
@Test
804+
fun testWelcomeScreenNavigation() {
805+
composeTestRule.waitForIdle()
806+
807+
// Verify Welcome screen elements are displayed
808+
composeTestRule.onNodeWithText("ExecuTorch Llama Demo").assertIsDisplayed()
809+
composeTestRule.onNodeWithText("Welcome to ExecuTorch Llama Demo").assertIsDisplayed()
810+
composeTestRule.onNodeWithText("Load local model").assertIsDisplayed()
811+
composeTestRule.onNodeWithText("App Settings").assertIsDisplayed()
812+
813+
// Test navigation to App Settings
814+
composeTestRule.onNodeWithText("App Settings").performClick()
815+
composeTestRule.waitUntil(timeoutMillis = 3000) {
816+
composeTestRule.onAllNodesWithText("App Settings", useUnmergedTree = true)
817+
.fetchSemanticsNodes().size >= 1
818+
}
819+
820+
// Verify App Settings screen
821+
composeTestRule.onNodeWithText("Appearance").assertIsDisplayed()
822+
composeTestRule.onNodeWithText("Theme").assertIsDisplayed()
823+
composeTestRule.onNodeWithText("Clear Conversation History").assertIsDisplayed()
824+
825+
// Go back to Welcome screen
826+
composeTestRule.onNodeWithContentDescription("Back").performClick()
827+
composeTestRule.waitUntil(timeoutMillis = 3000) {
828+
composeTestRule.onAllNodesWithText("ExecuTorch Llama Demo").fetchSemanticsNodes().isNotEmpty()
829+
}
830+
831+
// Verify we're back on Welcome screen
832+
composeTestRule.onNodeWithText("ExecuTorch Llama Demo").assertIsDisplayed()
833+
composeTestRule.onNodeWithText("Load local model").assertIsDisplayed()
834+
835+
// Test navigation to Model Settings
836+
composeTestRule.onNodeWithText("Load local model").performClick()
837+
composeTestRule.waitUntil(timeoutMillis = 3000) {
838+
composeTestRule.onAllNodesWithText("Select a Model").fetchSemanticsNodes().isNotEmpty()
839+
}
840+
841+
// Verify Model Settings screen
842+
composeTestRule.onNodeWithText("Select a Model").assertIsDisplayed()
843+
composeTestRule.onNodeWithText("Backend").assertIsDisplayed()
844+
composeTestRule.onNodeWithText("Load Model").assertIsDisplayed()
845+
846+
Log.i(TAG, "Welcome screen navigation test completed successfully")
847+
}
792848
}

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

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,21 @@
2222
android:name=".LogsActivity"
2323
android:exported="false" />
2424
<activity
25-
android:name=".SettingsActivity"
25+
android:name=".ModelSettingsActivity"
2626
android:exported="false" />
27+
<activity
28+
android:name=".AppSettingsActivity"
29+
android:exported="false" />
30+
<activity
31+
android:name=".WelcomeActivity"
32+
android:exported="true"
33+
android:theme="@style/Theme.AppCompat.Light.NoActionBar">
34+
<intent-filter>
35+
<action android:name="android.intent.action.MAIN" />
36+
37+
<category android:name="android.intent.category.LAUNCHER" />
38+
</intent-filter>
39+
</activity>
2740

2841
<uses-native-library
2942
android:name="libcdsprpc.so"
@@ -55,15 +68,10 @@
5568

5669
<activity
5770
android:name=".MainActivity"
58-
android:exported="true"
71+
android:exported="false"
5972
android:label="@string/app_name"
6073
android:windowSoftInputMode="adjustResize"
6174
android:theme="@style/Theme.AppCompat.Light.NoActionBar">
62-
<intent-filter>
63-
<action android:name="android.intent.action.MAIN" />
64-
65-
<category android:name="android.intent.category.LAUNCHER" />
66-
</intent-filter>
6775
</activity>
6876
</application>
6977

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
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+
import android.os.Build
12+
import android.os.Bundle
13+
import androidx.activity.ComponentActivity
14+
import androidx.activity.compose.setContent
15+
import androidx.compose.foundation.isSystemInDarkTheme
16+
import androidx.compose.foundation.layout.fillMaxSize
17+
import androidx.compose.material3.Surface
18+
import androidx.compose.runtime.getValue
19+
import androidx.compose.runtime.mutableStateOf
20+
import androidx.compose.runtime.setValue
21+
import androidx.compose.ui.Modifier
22+
import androidx.core.content.ContextCompat
23+
import com.example.executorchllamademo.ui.screens.AppSettingsScreen
24+
import com.example.executorchllamademo.ui.theme.LlamaDemoTheme
25+
26+
class AppSettingsActivity : ComponentActivity() {
27+
28+
private var appearanceMode by mutableStateOf(AppearanceMode.SYSTEM)
29+
30+
override fun onCreate(savedInstanceState: Bundle?) {
31+
super.onCreate(savedInstanceState)
32+
33+
if (Build.VERSION.SDK_INT >= 21) {
34+
window.statusBarColor = ContextCompat.getColor(this, R.color.status_bar)
35+
window.navigationBarColor = ContextCompat.getColor(this, R.color.nav_bar)
36+
}
37+
38+
loadAppearanceMode()
39+
40+
setContent {
41+
val isDarkTheme = when (appearanceMode) {
42+
AppearanceMode.LIGHT -> false
43+
AppearanceMode.DARK -> true
44+
AppearanceMode.SYSTEM -> isSystemInDarkTheme()
45+
}
46+
47+
LlamaDemoTheme(darkTheme = isDarkTheme) {
48+
Surface(modifier = Modifier.fillMaxSize()) {
49+
AppSettingsScreen(
50+
onBackPressed = { finish() },
51+
onAppearanceChanged = { mode ->
52+
appearanceMode = mode
53+
}
54+
)
55+
}
56+
}
57+
}
58+
}
59+
60+
private fun loadAppearanceMode() {
61+
val prefs = DemoSharedPreferences(this)
62+
appearanceMode = prefs.getAppSettings().appearanceMode
63+
}
64+
65+
override fun onResume() {
66+
super.onResume()
67+
loadAppearanceMode()
68+
}
69+
}

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,9 @@ class LogsActivity : ComponentActivity() {
4646

4747
LlamaDemoTheme(darkTheme = isDarkTheme) {
4848
Surface(modifier = Modifier.fillMaxSize()) {
49-
LogsScreen()
49+
LogsScreen(
50+
onBackClick = { finish() }
51+
)
5052
}
5153
}
5254
}

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

Lines changed: 8 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
package com.example.executorchllamademo
1010

1111
import android.Manifest
12-
import android.app.AlertDialog
1312
import android.content.ContentValues
1413
import android.content.Intent
1514
import android.content.pm.PackageManager
@@ -81,8 +80,12 @@ class MainActivity : ComponentActivity() {
8180

8281
ChatScreen(
8382
viewModel = viewModel,
83+
onBackClick = {
84+
startActivity(Intent(this@MainActivity, WelcomeActivity::class.java))
85+
finish()
86+
},
8487
onSettingsClick = {
85-
startActivity(Intent(this@MainActivity, SettingsActivity::class.java))
88+
startActivity(Intent(this@MainActivity, ModelSettingsActivity::class.java))
8689
},
8790
onLogsClick = {
8891
startActivity(Intent(this@MainActivity, LogsActivity::class.java))
@@ -103,8 +106,9 @@ class MainActivity : ComponentActivity() {
103106
launchCamera()
104107
}
105108
},
106-
onAudioClick = { _ ->
107-
showAudioFileSelector()
109+
audioFiles = ModelSettingsActivity.listLocalFile("/data/local/tmp/audio/", arrayOf(".bin")).toList(),
110+
onAudioFileSelected = { audioFile ->
111+
chatViewModel?.setAudioFile(audioFile)
108112
}
109113
)
110114
}
@@ -164,18 +168,6 @@ class MainActivity : ComponentActivity() {
164168
cameraImageUri?.let { cameraRoll.launch(it) }
165169
}
166170

167-
private fun showAudioFileSelector() {
168-
val audioFiles = SettingsActivity.listLocalFile("/data/local/tmp/audio/", arrayOf(".bin"))
169-
AlertDialog.Builder(this)
170-
.setTitle("Select audio feature path")
171-
.setSingleChoiceItems(audioFiles, -1) { dialog, item ->
172-
chatViewModel?.setAudioFile(audioFiles[item])
173-
dialog.dismiss()
174-
}
175-
.create()
176-
.show()
177-
}
178-
179171
private fun loadAppearanceMode() {
180172
val prefs = DemoSharedPreferences(this)
181173
appearanceMode = prefs.getAppSettings().appearanceMode

0 commit comments

Comments
 (0)