11package com.llamakotlin.sample
22
3+ import android.content.Context
4+ import android.content.Intent
5+ import android.content.SharedPreferences
6+ import android.net.Uri
37import android.os.Bundle
4- import android.os.Environment
8+ import android.provider.OpenableColumns
59import android.widget.Toast
610import androidx.activity.result.contract.ActivityResultContracts
711import androidx.appcompat.app.AppCompatActivity
812import androidx.lifecycle.lifecycleScope
9- import com.llamakotlin.android.LlamaConfig
1013import com.llamakotlin.android.LlamaModel
1114import com.llamakotlin.android.exception.LlamaException
1215import com.llamakotlin.sample.databinding.ActivityMainBinding
@@ -16,20 +19,52 @@ import kotlinx.coroutines.flow.catch
1619import kotlinx.coroutines.launch
1720import kotlinx.coroutines.withContext
1821import java.io.File
22+ import java.io.FileOutputStream
1923
2024class MainActivity : AppCompatActivity () {
2125
2226 private lateinit var binding: ActivityMainBinding
27+ private lateinit var prefs: SharedPreferences
2328 private var model: LlamaModel ? = null
2429 private var generationJob: Job ? = null
2530
31+ companion object {
32+ private const val PREFS_NAME = " llama_prefs"
33+ private const val KEY_MODEL_PATH = " model_path"
34+ }
35+
2636 override fun onCreate (savedInstanceState : Bundle ? ) {
2737 super .onCreate(savedInstanceState)
28- binding = ActivityMainBinding .inflate(layoutInflater)
29- setContentView(binding.root)
38+ try {
39+ binding = ActivityMainBinding .inflate(layoutInflater)
40+ setContentView(binding.root)
41+
42+ prefs = getSharedPreferences(PREFS_NAME , Context .MODE_PRIVATE )
3043
31- setupUI()
32- showVersion()
44+ setupUI()
45+ showVersion()
46+
47+ // Auto-load last used model
48+ autoLoadModel()
49+ } catch (e: Exception ) {
50+ e.printStackTrace()
51+ Toast .makeText(this , " Error: ${e.message} " , Toast .LENGTH_LONG ).show()
52+ }
53+ }
54+
55+ private fun autoLoadModel () {
56+ val savedPath = prefs.getString(KEY_MODEL_PATH , null )
57+ if (savedPath != null ) {
58+ val modelFile = File (savedPath)
59+ if (modelFile.exists()) {
60+ binding.tvStatus.text = " Auto-loading last model..."
61+ loadModel(savedPath)
62+ } else {
63+ // Clear saved path if file no longer exists
64+ prefs.edit().remove(KEY_MODEL_PATH ).apply ()
65+ binding.tvStatus.text = " Previous model not found. Please select a new model."
66+ }
67+ }
3368 }
3469
3570 private fun setupUI () {
@@ -63,39 +98,94 @@ class MainActivity : AppCompatActivity() {
6398 }
6499 }
65100
66- private val pickFileLauncher = registerForActivityResult(
101+ // File picker for GGUF models
102+ private val filePickerLauncher = registerForActivityResult(
67103 ActivityResultContracts .OpenDocument ()
68- ) { uri ->
69- uri?.let {
70- // Copy to local storage or use content resolver
71- val path = getModelPath(uri.path ? : " " )
72- if (path.isNotEmpty()) {
73- loadModel(path)
74- } else {
75- Toast .makeText(this , " Invalid model file" , Toast .LENGTH_SHORT ).show()
76- }
104+ ) { uri: Uri ? ->
105+ uri?.let { selectedUri ->
106+ // Take persistent permission
107+ contentResolver.takePersistableUriPermission(
108+ selectedUri,
109+ Intent .FLAG_GRANT_READ_URI_PERMISSION
110+ )
111+ copyAndLoadModel(selectedUri)
77112 }
78113 }
79114
80115 private fun pickModelFile () {
81- // For simplicity, look for model in Downloads folder
82- val downloadsDir = Environment .getExternalStoragePublicDirectory(Environment .DIRECTORY_DOWNLOADS )
83- val ggufFiles = downloadsDir.listFiles { file -> file.extension == " gguf" }
116+ binding.tvStatus.text = " Opening file picker...\n Select a .gguf model file"
84117
85- if (ggufFiles.isNullOrEmpty()) {
86- Toast .makeText(this , " No .gguf files in Downloads. Please download a model first." , Toast .LENGTH_LONG ).show()
87- binding.tvStatus.text = " No model found. Download a .gguf model to Downloads folder."
88- return
89- }
118+ // Use Storage Access Framework - works on all Android versions
119+ filePickerLauncher.launch(arrayOf(" */*" ))
120+ }
121+
122+ private fun copyAndLoadModel (uri : Uri ) {
123+ binding.tvStatus.text = " Copying model to app storage..."
124+ binding.btnLoadModel.isEnabled = false
125+
126+ lifecycleScope.launch(Dispatchers .IO ) {
127+ try {
128+ // Get file name
129+ val fileName = getFileName(uri) ? : " model.gguf"
130+
131+ if (! fileName.endsWith(" .gguf" , ignoreCase = true )) {
132+ withContext(Dispatchers .Main ) {
133+ binding.tvStatus.text = " Please select a .gguf file"
134+ binding.btnLoadModel.isEnabled = true
135+ }
136+ return @launch
137+ }
138+
139+ // Copy to app's internal storage
140+ val modelFile = File (filesDir, fileName)
141+
142+ withContext(Dispatchers .Main ) {
143+ binding.tvStatus.text = " Copying $fileName ...\n This may take a few minutes for large models."
144+ }
145+
146+ contentResolver.openInputStream(uri)?.use { input ->
147+ FileOutputStream (modelFile).use { output ->
148+ val buffer = ByteArray (8192 )
149+ var bytesRead: Int
150+ var totalBytes = 0L
151+ while (input.read(buffer).also { bytesRead = it } != - 1 ) {
152+ output.write(buffer, 0 , bytesRead)
153+ totalBytes + = bytesRead
154+ // Update progress every 10MB
155+ if (totalBytes % (10 * 1024 * 1024 ) < 8192 ) {
156+ withContext(Dispatchers .Main ) {
157+ binding.tvStatus.text = " Copying $fileName ...\n ${totalBytes / (1024 * 1024 )} MB copied"
158+ }
159+ }
160+ }
161+ }
162+ }
90163
91- // Use the first found model
92- val modelFile = ggufFiles.first()
93- loadModel(modelFile.absolutePath)
164+ withContext(Dispatchers .Main ) {
165+ binding.tvStatus.text = " Model copied. Loading..."
166+ binding.btnLoadModel.isEnabled = true
167+ loadModel(modelFile.absolutePath)
168+ }
169+
170+ } catch (e: Exception ) {
171+ e.printStackTrace()
172+ withContext(Dispatchers .Main ) {
173+ binding.tvStatus.text = " Error copying model: ${e.message} "
174+ binding.btnLoadModel.isEnabled = true
175+ }
176+ }
177+ }
94178 }
95179
96- private fun getModelPath (uriPath : String ): String {
97- // Simple path extraction - in production use ContentResolver
98- return uriPath
180+ private fun getFileName (uri : Uri ): String? {
181+ var name: String? = null
182+ contentResolver.query(uri, null , null , null , null )?.use { cursor ->
183+ val nameIndex = cursor.getColumnIndex(OpenableColumns .DISPLAY_NAME )
184+ if (cursor.moveToFirst() && nameIndex >= 0 ) {
185+ name = cursor.getString(nameIndex)
186+ }
187+ }
188+ return name ? : uri.lastPathSegment
99189 }
100190
101191 private fun loadModel (modelPath : String ) {
@@ -114,6 +204,9 @@ class MainActivity : AppCompatActivity() {
114204 topK = 40
115205 }
116206
207+ // Save model path for auto-loading
208+ prefs.edit().putString(KEY_MODEL_PATH , modelPath).apply ()
209+
117210 binding.tvStatus.text = " Model loaded: ${File (modelPath).name} "
118211 binding.btnGenerate.isEnabled = true
119212 Toast .makeText(this @MainActivity, " Model loaded successfully!" , Toast .LENGTH_SHORT ).show()
0 commit comments