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