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.app.AlertDialog; 12 import android.content.DialogInterface; 13 import android.os.Build; 14 import android.os.Bundle; 15 import android.text.Editable; 16 import android.text.TextWatcher; 17 import android.view.View; 18 import android.widget.Button; 19 import android.widget.EditText; 20 import android.widget.ImageButton; 21 import android.widget.TextView; 22 import androidx.appcompat.app.AppCompatActivity; 23 import androidx.core.content.ContextCompat; 24 import androidx.core.graphics.Insets; 25 import androidx.core.view.ViewCompat; 26 import androidx.core.view.WindowInsetsCompat; 27 import com.google.gson.Gson; 28 import java.io.File; 29 import java.util.ArrayList; 30 import java.util.List; 31 32 public class SettingsActivity extends AppCompatActivity { 33 34 private String mModelFilePath = ""; 35 private String mTokenizerFilePath = ""; 36 private TextView mBackendTextView; 37 private TextView mModelTextView; 38 private TextView mTokenizerTextView; 39 private TextView mModelTypeTextView; 40 private EditText mSystemPromptEditText; 41 private EditText mUserPromptEditText; 42 private Button mLoadModelButton; 43 private double mSetTemperature; 44 private String mSystemPrompt; 45 private String mUserPrompt; 46 private BackendType mBackendType; 47 private ModelType mModelType; 48 public SettingsFields mSettingsFields; 49 50 private DemoSharedPreferences mDemoSharedPreferences; 51 public static double TEMPERATURE_MIN_VALUE = 0.0; 52 53 @Override onCreate(Bundle savedInstanceState)54 protected void onCreate(Bundle savedInstanceState) { 55 super.onCreate(savedInstanceState); 56 setContentView(R.layout.activity_settings); 57 if (Build.VERSION.SDK_INT >= 21) { 58 getWindow().setStatusBarColor(ContextCompat.getColor(this, R.color.status_bar)); 59 getWindow().setNavigationBarColor(ContextCompat.getColor(this, R.color.nav_bar)); 60 } 61 ViewCompat.setOnApplyWindowInsetsListener( 62 requireViewById(R.id.main), 63 (v, insets) -> { 64 Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()); 65 v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom); 66 return insets; 67 }); 68 mDemoSharedPreferences = new DemoSharedPreferences(getBaseContext()); 69 mSettingsFields = new SettingsFields(); 70 setupSettings(); 71 } 72 setupSettings()73 private void setupSettings() { 74 mBackendTextView = requireViewById(R.id.backendTextView); 75 mModelTextView = requireViewById(R.id.modelTextView); 76 mTokenizerTextView = requireViewById(R.id.tokenizerTextView); 77 mModelTypeTextView = requireViewById(R.id.modelTypeTextView); 78 ImageButton backendImageButton = requireViewById(R.id.backendImageButton); 79 ImageButton modelImageButton = requireViewById(R.id.modelImageButton); 80 ImageButton tokenizerImageButton = requireViewById(R.id.tokenizerImageButton); 81 ImageButton modelTypeImageButton = requireViewById(R.id.modelTypeImageButton); 82 mSystemPromptEditText = requireViewById(R.id.systemPromptText); 83 mUserPromptEditText = requireViewById(R.id.userPromptText); 84 loadSettings(); 85 86 // TODO: The two setOnClickListeners will be removed after file path issue is resolved 87 backendImageButton.setOnClickListener( 88 view -> { 89 setupBackendSelectorDialog(); 90 }); 91 modelImageButton.setOnClickListener( 92 view -> { 93 setupModelSelectorDialog(); 94 }); 95 tokenizerImageButton.setOnClickListener( 96 view -> { 97 setupTokenizerSelectorDialog(); 98 }); 99 modelTypeImageButton.setOnClickListener( 100 view -> { 101 setupModelTypeSelectorDialog(); 102 }); 103 mModelFilePath = mSettingsFields.getModelFilePath(); 104 if (!mModelFilePath.isEmpty()) { 105 mModelTextView.setText(getFilenameFromPath(mModelFilePath)); 106 } 107 mTokenizerFilePath = mSettingsFields.getTokenizerFilePath(); 108 if (!mTokenizerFilePath.isEmpty()) { 109 mTokenizerTextView.setText(getFilenameFromPath(mTokenizerFilePath)); 110 } 111 mModelType = mSettingsFields.getModelType(); 112 ETLogging.getInstance().log("mModelType from settings " + mModelType); 113 if (mModelType != null) { 114 mModelTypeTextView.setText(mModelType.toString()); 115 } 116 mBackendType = mSettingsFields.getBackendType(); 117 ETLogging.getInstance().log("mBackendType from settings " + mBackendType); 118 if (mBackendType != null) { 119 mBackendTextView.setText(mBackendType.toString()); 120 setBackendSettingMode(); 121 } 122 123 setupParameterSettings(); 124 setupPromptSettings(); 125 setupClearChatHistoryButton(); 126 setupLoadModelButton(); 127 } 128 setupLoadModelButton()129 private void setupLoadModelButton() { 130 mLoadModelButton = requireViewById(R.id.loadModelButton); 131 mLoadModelButton.setEnabled(true); 132 mLoadModelButton.setOnClickListener( 133 view -> { 134 new AlertDialog.Builder(this) 135 .setTitle("Load Model") 136 .setMessage("Do you really want to load the new model?") 137 .setIcon(android.R.drawable.ic_dialog_alert) 138 .setPositiveButton( 139 android.R.string.yes, 140 new DialogInterface.OnClickListener() { 141 public void onClick(DialogInterface dialog, int whichButton) { 142 mSettingsFields.saveLoadModelAction(true); 143 mLoadModelButton.setEnabled(false); 144 onBackPressed(); 145 } 146 }) 147 .setNegativeButton(android.R.string.no, null) 148 .show(); 149 }); 150 } 151 setupClearChatHistoryButton()152 private void setupClearChatHistoryButton() { 153 Button clearChatButton = requireViewById(R.id.clearChatButton); 154 clearChatButton.setOnClickListener( 155 view -> { 156 new AlertDialog.Builder(this) 157 .setTitle("Delete Chat History") 158 .setMessage("Do you really want to delete chat history?") 159 .setIcon(android.R.drawable.ic_dialog_alert) 160 .setPositiveButton( 161 android.R.string.yes, 162 new DialogInterface.OnClickListener() { 163 public void onClick(DialogInterface dialog, int whichButton) { 164 mSettingsFields.saveIsClearChatHistory(true); 165 } 166 }) 167 .setNegativeButton(android.R.string.no, null) 168 .show(); 169 }); 170 } 171 setupParameterSettings()172 private void setupParameterSettings() { 173 setupTemperatureSettings(); 174 } 175 setupTemperatureSettings()176 private void setupTemperatureSettings() { 177 mSetTemperature = mSettingsFields.getTemperature(); 178 EditText temperatureEditText = requireViewById(R.id.temperatureEditText); 179 temperatureEditText.setText(String.valueOf(mSetTemperature)); 180 temperatureEditText.addTextChangedListener( 181 new TextWatcher() { 182 @Override 183 public void beforeTextChanged(CharSequence s, int start, int count, int after) {} 184 185 @Override 186 public void onTextChanged(CharSequence s, int start, int before, int count) {} 187 188 @Override 189 public void afterTextChanged(Editable s) { 190 mSetTemperature = Double.parseDouble(s.toString()); 191 // This is needed because temperature is changed together with model loading 192 // Once temperature is no longer in LlamaModule constructor, we can remove this 193 mSettingsFields.saveLoadModelAction(true); 194 saveSettings(); 195 } 196 }); 197 } 198 setupPromptSettings()199 private void setupPromptSettings() { 200 setupSystemPromptSettings(); 201 setupUserPromptSettings(); 202 } 203 setupSystemPromptSettings()204 private void setupSystemPromptSettings() { 205 mSystemPrompt = mSettingsFields.getSystemPrompt(); 206 mSystemPromptEditText.setText(mSystemPrompt); 207 mSystemPromptEditText.addTextChangedListener( 208 new TextWatcher() { 209 @Override 210 public void beforeTextChanged(CharSequence s, int start, int count, int after) {} 211 212 @Override 213 public void onTextChanged(CharSequence s, int start, int before, int count) {} 214 215 @Override 216 public void afterTextChanged(Editable s) { 217 mSystemPrompt = s.toString(); 218 } 219 }); 220 221 ImageButton resetSystemPrompt = requireViewById(R.id.resetSystemPrompt); 222 resetSystemPrompt.setOnClickListener( 223 view -> { 224 new AlertDialog.Builder(this) 225 .setTitle("Reset System Prompt") 226 .setMessage("Do you really want to reset system prompt?") 227 .setIcon(android.R.drawable.ic_dialog_alert) 228 .setPositiveButton( 229 android.R.string.yes, 230 new DialogInterface.OnClickListener() { 231 public void onClick(DialogInterface dialog, int whichButton) { 232 // Clear the messageAdapter and sharedPreference 233 mSystemPromptEditText.setText(PromptFormat.DEFAULT_SYSTEM_PROMPT); 234 } 235 }) 236 .setNegativeButton(android.R.string.no, null) 237 .show(); 238 }); 239 } 240 setupUserPromptSettings()241 private void setupUserPromptSettings() { 242 mUserPrompt = mSettingsFields.getUserPrompt(); 243 mUserPromptEditText.setText(mUserPrompt); 244 mUserPromptEditText.addTextChangedListener( 245 new TextWatcher() { 246 @Override 247 public void beforeTextChanged(CharSequence s, int start, int count, int after) {} 248 249 @Override 250 public void onTextChanged(CharSequence s, int start, int before, int count) {} 251 252 @Override 253 public void afterTextChanged(Editable s) { 254 if (isValidUserPrompt(s.toString())) { 255 mUserPrompt = s.toString(); 256 } else { 257 showInvalidPromptDialog(); 258 } 259 } 260 }); 261 262 ImageButton resetUserPrompt = requireViewById(R.id.resetUserPrompt); 263 resetUserPrompt.setOnClickListener( 264 view -> { 265 new AlertDialog.Builder(this) 266 .setTitle("Reset Prompt Template") 267 .setMessage("Do you really want to reset the prompt template?") 268 .setIcon(android.R.drawable.ic_dialog_alert) 269 .setPositiveButton( 270 android.R.string.yes, 271 new DialogInterface.OnClickListener() { 272 public void onClick(DialogInterface dialog, int whichButton) { 273 // Clear the messageAdapter and sharedPreference 274 mUserPromptEditText.setText(PromptFormat.getUserPromptTemplate(mModelType)); 275 } 276 }) 277 .setNegativeButton(android.R.string.no, null) 278 .show(); 279 }); 280 } 281 isValidUserPrompt(String userPrompt)282 private boolean isValidUserPrompt(String userPrompt) { 283 return userPrompt.contains(PromptFormat.USER_PLACEHOLDER); 284 } 285 showInvalidPromptDialog()286 private void showInvalidPromptDialog() { 287 new AlertDialog.Builder(this) 288 .setTitle("Invalid Prompt Format") 289 .setMessage( 290 "Prompt format must contain " 291 + PromptFormat.USER_PLACEHOLDER 292 + ". Do you want to reset prompt format?") 293 .setIcon(android.R.drawable.ic_dialog_alert) 294 .setPositiveButton( 295 android.R.string.yes, 296 (dialog, whichButton) -> { 297 mUserPromptEditText.setText(PromptFormat.getUserPromptTemplate(mModelType)); 298 }) 299 .setNegativeButton(android.R.string.no, null) 300 .show(); 301 } 302 setupBackendSelectorDialog()303 private void setupBackendSelectorDialog() { 304 // Convert enum to list 305 List<String> backendTypesList = new ArrayList<>(); 306 for (BackendType backendType : BackendType.values()) { 307 backendTypesList.add(backendType.toString()); 308 } 309 // Alert dialog builder takes in arr of string instead of list 310 String[] backendTypes = backendTypesList.toArray(new String[0]); 311 AlertDialog.Builder backendTypeBuilder = new AlertDialog.Builder(this); 312 backendTypeBuilder.setTitle("Select backend type"); 313 backendTypeBuilder.setSingleChoiceItems( 314 backendTypes, 315 -1, 316 (dialog, item) -> { 317 mBackendTextView.setText(backendTypes[item]); 318 mBackendType = BackendType.valueOf(backendTypes[item]); 319 setBackendSettingMode(); 320 dialog.dismiss(); 321 }); 322 323 backendTypeBuilder.create().show(); 324 } 325 setupModelSelectorDialog()326 private void setupModelSelectorDialog() { 327 String[] pteFiles = listLocalFile("/data/local/tmp/llama/", ".pte"); 328 AlertDialog.Builder modelPathBuilder = new AlertDialog.Builder(this); 329 modelPathBuilder.setTitle("Select model path"); 330 331 modelPathBuilder.setSingleChoiceItems( 332 pteFiles, 333 -1, 334 (dialog, item) -> { 335 mModelFilePath = pteFiles[item]; 336 mModelTextView.setText(getFilenameFromPath(mModelFilePath)); 337 mLoadModelButton.setEnabled(true); 338 dialog.dismiss(); 339 }); 340 341 modelPathBuilder.create().show(); 342 } 343 listLocalFile(String path, String suffix)344 private static String[] listLocalFile(String path, String suffix) { 345 File directory = new File(path); 346 if (directory.exists() && directory.isDirectory()) { 347 File[] files = directory.listFiles((dir, name) -> name.toLowerCase().endsWith(suffix)); 348 String[] result = new String[files.length]; 349 for (int i = 0; i < files.length; i++) { 350 if (files[i].isFile() && files[i].getName().endsWith(suffix)) { 351 result[i] = files[i].getAbsolutePath(); 352 } 353 } 354 return result; 355 } 356 return new String[] {}; 357 } 358 setupModelTypeSelectorDialog()359 private void setupModelTypeSelectorDialog() { 360 // Convert enum to list 361 List<String> modelTypesList = new ArrayList<>(); 362 for (ModelType modelType : ModelType.values()) { 363 modelTypesList.add(modelType.toString()); 364 } 365 // Alert dialog builder takes in arr of string instead of list 366 String[] modelTypes = modelTypesList.toArray(new String[0]); 367 AlertDialog.Builder modelTypeBuilder = new AlertDialog.Builder(this); 368 modelTypeBuilder.setTitle("Select model type"); 369 modelTypeBuilder.setSingleChoiceItems( 370 modelTypes, 371 -1, 372 (dialog, item) -> { 373 mModelTypeTextView.setText(modelTypes[item]); 374 mModelType = ModelType.valueOf(modelTypes[item]); 375 mUserPromptEditText.setText(PromptFormat.getUserPromptTemplate(mModelType)); 376 dialog.dismiss(); 377 }); 378 379 modelTypeBuilder.create().show(); 380 } 381 setupTokenizerSelectorDialog()382 private void setupTokenizerSelectorDialog() { 383 String[] binFiles = listLocalFile("/data/local/tmp/llama/", ".bin"); 384 String[] modelFiles = listLocalFile("/data/local/tmp/llama/", ".model"); 385 String[] tokenizerFiles = new String[binFiles.length + modelFiles.length]; 386 System.arraycopy(binFiles, 0, tokenizerFiles, 0, binFiles.length); 387 System.arraycopy(modelFiles, 0, tokenizerFiles, binFiles.length, modelFiles.length); 388 AlertDialog.Builder tokenizerPathBuilder = new AlertDialog.Builder(this); 389 tokenizerPathBuilder.setTitle("Select tokenizer path"); 390 tokenizerPathBuilder.setSingleChoiceItems( 391 tokenizerFiles, 392 -1, 393 (dialog, item) -> { 394 mTokenizerFilePath = tokenizerFiles[item]; 395 mTokenizerTextView.setText(getFilenameFromPath(mTokenizerFilePath)); 396 mLoadModelButton.setEnabled(true); 397 dialog.dismiss(); 398 }); 399 400 tokenizerPathBuilder.create().show(); 401 } 402 getFilenameFromPath(String uriFilePath)403 private String getFilenameFromPath(String uriFilePath) { 404 String[] segments = uriFilePath.split("/"); 405 if (segments.length > 0) { 406 return segments[segments.length - 1]; // get last element (aka filename) 407 } 408 return ""; 409 } 410 setBackendSettingMode()411 private void setBackendSettingMode() { 412 if (mBackendType.equals(BackendType.XNNPACK) || mBackendType.equals(BackendType.QUALCOMM)) { 413 setXNNPACKSettingMode(); 414 } else if (mBackendType.equals(BackendType.MEDIATEK)) { 415 setMediaTekSettingMode(); 416 } 417 } 418 setXNNPACKSettingMode()419 private void setXNNPACKSettingMode() { 420 requireViewById(R.id.modelLayout).setVisibility(View.VISIBLE); 421 requireViewById(R.id.tokenizerLayout).setVisibility(View.VISIBLE); 422 requireViewById(R.id.parametersView).setVisibility(View.VISIBLE); 423 requireViewById(R.id.temperatureLayout).setVisibility(View.VISIBLE); 424 mModelFilePath = ""; 425 mTokenizerFilePath = ""; 426 } 427 setMediaTekSettingMode()428 private void setMediaTekSettingMode() { 429 requireViewById(R.id.modelLayout).setVisibility(View.GONE); 430 requireViewById(R.id.tokenizerLayout).setVisibility(View.GONE); 431 requireViewById(R.id.parametersView).setVisibility(View.GONE); 432 requireViewById(R.id.temperatureLayout).setVisibility(View.GONE); 433 mModelFilePath = "/in/mtk/llama/runner"; 434 mTokenizerFilePath = "/in/mtk/llama/runner"; 435 } 436 loadSettings()437 private void loadSettings() { 438 Gson gson = new Gson(); 439 String settingsFieldsJSON = mDemoSharedPreferences.getSettings(); 440 if (!settingsFieldsJSON.isEmpty()) { 441 mSettingsFields = gson.fromJson(settingsFieldsJSON, SettingsFields.class); 442 } 443 } 444 saveSettings()445 private void saveSettings() { 446 mSettingsFields.saveModelPath(mModelFilePath); 447 mSettingsFields.saveTokenizerPath(mTokenizerFilePath); 448 mSettingsFields.saveParameters(mSetTemperature); 449 mSettingsFields.savePrompts(mSystemPrompt, mUserPrompt); 450 mSettingsFields.saveModelType(mModelType); 451 mSettingsFields.saveBackendType(mBackendType); 452 mDemoSharedPreferences.addSettings(mSettingsFields); 453 } 454 455 @Override onBackPressed()456 public void onBackPressed() { 457 super.onBackPressed(); 458 saveSettings(); 459 } 460 } 461