Browse Source

Add Android Article Summarizer app

Riandy 1 tuần trước cách đây
mục cha
commit
a23915049e
87 tập tin đã thay đổi với 3755 bổ sung0 xóa
  1. 39 0
      getting-started/demo/ArticleSummarizer/.gitignore
  2. 22 0
      getting-started/demo/ArticleSummarizer/README.md
  3. 1 0
      getting-started/demo/ArticleSummarizer/app/.gitignore
  4. 79 0
      getting-started/demo/ArticleSummarizer/app/build.gradle.kts
  5. 21 0
      getting-started/demo/ArticleSummarizer/app/proguard-rules.pro
  6. 65 0
      getting-started/demo/ArticleSummarizer/app/src/main/AndroidManifest.xml
  7. 49 0
      getting-started/demo/ArticleSummarizer/app/src/main/java/com/example/llamaandroiddemo/AppLog.java
  8. 54 0
      getting-started/demo/ArticleSummarizer/app/src/main/java/com/example/llamaandroiddemo/AppLogging.java
  9. 13 0
      getting-started/demo/ArticleSummarizer/app/src/main/java/com/example/llamaandroiddemo/AppUtils.java
  10. 5 0
      getting-started/demo/ArticleSummarizer/app/src/main/java/com/example/llamaandroiddemo/BackendType.java
  11. 90 0
      getting-started/demo/ArticleSummarizer/app/src/main/java/com/example/llamaandroiddemo/DemoSharedPreferences.java
  12. 240 0
      getting-started/demo/ArticleSummarizer/app/src/main/java/com/example/llamaandroiddemo/ExampleLlamaRemoteInference.kt
  13. 28 0
      getting-started/demo/ArticleSummarizer/app/src/main/java/com/example/llamaandroiddemo/HomescreenActivity.kt
  14. 65 0
      getting-started/demo/ArticleSummarizer/app/src/main/java/com/example/llamaandroiddemo/LanguageSelector.java
  15. 92 0
      getting-started/demo/ArticleSummarizer/app/src/main/java/com/example/llamaandroiddemo/LogsActivity.java
  16. 45 0
      getting-started/demo/ArticleSummarizer/app/src/main/java/com/example/llamaandroiddemo/LogsAdapter.java
  17. 571 0
      getting-started/demo/ArticleSummarizer/app/src/main/java/com/example/llamaandroiddemo/MainActivity.java
  18. 98 0
      getting-started/demo/ArticleSummarizer/app/src/main/java/com/example/llamaandroiddemo/Message.java
  19. 147 0
      getting-started/demo/ArticleSummarizer/app/src/main/java/com/example/llamaandroiddemo/MessageAdapter.java
  20. 15 0
      getting-started/demo/ArticleSummarizer/app/src/main/java/com/example/llamaandroiddemo/MessageType.java
  21. 16 0
      getting-started/demo/ArticleSummarizer/app/src/main/java/com/example/llamaandroiddemo/ModelType.java
  22. 23 0
      getting-started/demo/ArticleSummarizer/app/src/main/java/com/example/llamaandroiddemo/ModelUtils.java
  23. 14 0
      getting-started/demo/ArticleSummarizer/app/src/main/java/com/example/llamaandroiddemo/PromptFormat.java
  24. 246 0
      getting-started/demo/ArticleSummarizer/app/src/main/java/com/example/llamaandroiddemo/SettingsActivity.java
  25. 146 0
      getting-started/demo/ArticleSummarizer/app/src/main/java/com/example/llamaandroiddemo/SettingsFields.java
  26. 5 0
      getting-started/demo/ArticleSummarizer/app/src/main/res/drawable/banner_shape.xml
  27. 5 0
      getting-started/demo/ArticleSummarizer/app/src/main/res/drawable/baseline_add_24.xml
  28. 5 0
      getting-started/demo/ArticleSummarizer/app/src/main/res/drawable/baseline_add_photo_alternate_24.xml
  29. 6 0
      getting-started/demo/ArticleSummarizer/app/src/main/res/drawable/baseline_article_24.xml
  30. 6 0
      getting-started/demo/ArticleSummarizer/app/src/main/res/drawable/baseline_close_24.xml
  31. 5 0
      getting-started/demo/ArticleSummarizer/app/src/main/res/drawable/baseline_delete_forever_24.xml
  32. 5 0
      getting-started/demo/ArticleSummarizer/app/src/main/res/drawable/baseline_language_24.xml
  33. 6 0
      getting-started/demo/ArticleSummarizer/app/src/main/res/drawable/baseline_restart_alt_24.xml
  34. 6 0
      getting-started/demo/ArticleSummarizer/app/src/main/res/drawable/baseline_send_24.xml
  35. 11 0
      getting-started/demo/ArticleSummarizer/app/src/main/res/drawable/baseline_settings_24.xml
  36. 6 0
      getting-started/demo/ArticleSummarizer/app/src/main/res/drawable/baseline_stop_24.xml
  37. 8 0
      getting-started/demo/ArticleSummarizer/app/src/main/res/drawable/btn.xml
  38. 21 0
      getting-started/demo/ArticleSummarizer/app/src/main/res/drawable/chat_background.xml
  39. 7 0
      getting-started/demo/ArticleSummarizer/app/src/main/res/drawable/custom_button_round.xml
  40. 9 0
      getting-started/demo/ArticleSummarizer/app/src/main/res/drawable/expand_circle_down.xml
  41. 170 0
      getting-started/demo/ArticleSummarizer/app/src/main/res/drawable/ic_launcher_background.xml
  42. 30 0
      getting-started/demo/ArticleSummarizer/app/src/main/res/drawable/ic_launcher_foreground.xml
  43. 7 0
      getting-started/demo/ArticleSummarizer/app/src/main/res/drawable/input_text_shape.xml
  44. BIN
      getting-started/demo/ArticleSummarizer/app/src/main/res/drawable/logo.png
  45. 6 0
      getting-started/demo/ArticleSummarizer/app/src/main/res/drawable/outline_add_box_48.xml
  46. 5 0
      getting-started/demo/ArticleSummarizer/app/src/main/res/drawable/outline_camera_alt_48.xml
  47. 5 0
      getting-started/demo/ArticleSummarizer/app/src/main/res/drawable/outline_image_48.xml
  48. 6 0
      getting-started/demo/ArticleSummarizer/app/src/main/res/drawable/prompt_shape.xml
  49. 6 0
      getting-started/demo/ArticleSummarizer/app/src/main/res/drawable/received_message.xml
  50. 6 0
      getting-started/demo/ArticleSummarizer/app/src/main/res/drawable/sent_message.xml
  51. BIN
      getting-started/demo/ArticleSummarizer/app/src/main/res/drawable/summarizer.png
  52. 5 0
      getting-started/demo/ArticleSummarizer/app/src/main/res/drawable/three_dots.xml
  53. 16 0
      getting-started/demo/ArticleSummarizer/app/src/main/res/layout/activity_benchmarking.xml
  54. 74 0
      getting-started/demo/ArticleSummarizer/app/src/main/res/layout/activity_homescreen.xml
  55. 55 0
      getting-started/demo/ArticleSummarizer/app/src/main/res/layout/activity_logs.xml
  56. 255 0
      getting-started/demo/ArticleSummarizer/app/src/main/res/layout/activity_main.xml
  57. 209 0
      getting-started/demo/ArticleSummarizer/app/src/main/res/layout/activity_settings.xml
  58. 16 0
      getting-started/demo/ArticleSummarizer/app/src/main/res/layout/logs_message.xml
  59. 70 0
      getting-started/demo/ArticleSummarizer/app/src/main/res/layout/received_message.xml
  60. 63 0
      getting-started/demo/ArticleSummarizer/app/src/main/res/layout/sent_message.xml
  61. 23 0
      getting-started/demo/ArticleSummarizer/app/src/main/res/layout/system_message.xml
  62. 6 0
      getting-started/demo/ArticleSummarizer/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
  63. 6 0
      getting-started/demo/ArticleSummarizer/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
  64. BIN
      getting-started/demo/ArticleSummarizer/app/src/main/res/mipmap-hdpi/ic_launcher.webp
  65. BIN
      getting-started/demo/ArticleSummarizer/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
  66. BIN
      getting-started/demo/ArticleSummarizer/app/src/main/res/mipmap-mdpi/ic_launcher.webp
  67. BIN
      getting-started/demo/ArticleSummarizer/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
  68. BIN
      getting-started/demo/ArticleSummarizer/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
  69. BIN
      getting-started/demo/ArticleSummarizer/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
  70. BIN
      getting-started/demo/ArticleSummarizer/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
  71. BIN
      getting-started/demo/ArticleSummarizer/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
  72. BIN
      getting-started/demo/ArticleSummarizer/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
  73. BIN
      getting-started/demo/ArticleSummarizer/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
  74. 10 0
      getting-started/demo/ArticleSummarizer/app/src/main/res/values/colors.xml
  75. 8 0
      getting-started/demo/ArticleSummarizer/app/src/main/res/values/strings.xml
  76. 14 0
      getting-started/demo/ArticleSummarizer/app/src/main/res/values/styles.xml
  77. 4 0
      getting-started/demo/ArticleSummarizer/app/src/main/res/values/themes.xml
  78. 13 0
      getting-started/demo/ArticleSummarizer/app/src/main/res/xml/backup_rules.xml
  79. 19 0
      getting-started/demo/ArticleSummarizer/app/src/main/res/xml/data_extraction_rules.xml
  80. 4 0
      getting-started/demo/ArticleSummarizer/app/src/main/res/xml/file_paths.xml
  81. 13 0
      getting-started/demo/ArticleSummarizer/build.gradle.kts
  82. 23 0
      getting-started/demo/ArticleSummarizer/gradle.properties
  83. 6 0
      getting-started/demo/ArticleSummarizer/gradle/wrapper/gradle-wrapper.properties
  84. 185 0
      getting-started/demo/ArticleSummarizer/gradlew
  85. 95 0
      getting-started/demo/ArticleSummarizer/gradlew.bat
  86. BIN
      getting-started/demo/ArticleSummarizer/screenshot.png
  87. 27 0
      getting-started/demo/ArticleSummarizer/settings.gradle.kts

+ 39 - 0
getting-started/demo/ArticleSummarizer/.gitignore

@@ -0,0 +1,39 @@
+# Gradle files
+.gradle/
+build/
+
+# Local configuration file (sdk path, etc)
+local.properties
+
+# Log/OS Files
+*.log
+
+# Android Studio generated files and folders
+captures/
+.externalNativeBuild/
+.cxx/
+*.apk
+output.json
+
+# IntelliJ
+*.iml
+.idea/
+misc.xml
+deploymentTargetDropDown.xml
+render.experimental.xml
+
+# Keystore files
+*.jks
+*.keystore
+
+# Google Services (e.g. APIs or Firebase)
+google-services.json
+
+# Android Profiling
+*.hprof
+
+.DS_Store
+
+*.jar
+*.aar
+*.so

+ 22 - 0
getting-started/demo/ArticleSummarizer/README.md

@@ -0,0 +1,22 @@
+# Android Article Summarizer App
+
+
+<img src="./screenshot.png">
+
+This is a sample Android app to demonstrate Llama 4 multimodal and multilingual capabilities. This app allows user to take a picture/screenshot of an article, and then summarize and translate it into any of the supported languages
+
+## Quick Start
+
+1. Open the ArticleSummarizer folder in Android Studio
+2. Update the `API_KEY` in `AppUtils.java`
+3. Build the Android Project
+4. Inside the app, tap on settings icon on top right
+5. Configure the Remote URL endpoint (any supported providers that serve Llama 4 models. For example: https://api.together.xyz)
+6. Select the desired model from the drop down list. If you need to add more models, modify `ModelUtils.java`
+
+```
+Note: This is an example project to demonstrate E2E flow. You should NOT use/store API key directly on client. Exposing your API key in client-side environments allows malicious users to take that key and make requests on your behalf. Requests should always be routed through your own backend server where you can keep your API key secure.
+```
+
+## Reporting Issues
+If you encountered any bugs or issues following this tutorial please file a bug/issue here on [Github](https://github.com/meta-llama/llama-cookbook/issues)).

+ 1 - 0
getting-started/demo/ArticleSummarizer/app/.gitignore

@@ -0,0 +1 @@
+/build

+ 79 - 0
getting-started/demo/ArticleSummarizer/app/build.gradle.kts

@@ -0,0 +1,79 @@
+/*
+ * 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.
+ */
+
+plugins {
+  id("com.android.application")
+  id("org.jetbrains.kotlin.android")
+}
+
+android {
+  namespace = "com.example.llamaandroiddemo"
+  compileSdk = 34
+
+  defaultConfig {
+    applicationId = "com.example.llamaandroiddemo"
+    minSdk = 28
+    targetSdk = 33
+    versionCode = 1
+    versionName = "1.0"
+
+    testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+    vectorDrawables { useSupportLibrary = true }
+    externalNativeBuild { cmake { cppFlags += "" } }
+    packaging {
+      resources.excludes.add("META-INF/DEPENDENCIES")
+    }
+  }
+
+  buildTypes {
+    release {
+      isMinifyEnabled = false
+      proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
+    }
+  }
+  compileOptions {
+    sourceCompatibility = JavaVersion.VERSION_1_8
+    targetCompatibility = JavaVersion.VERSION_1_8
+  }
+  kotlinOptions { jvmTarget = "1.8" }
+  buildFeatures { compose = true }
+  composeOptions { kotlinCompilerExtensionVersion = "1.4.3" }
+  packaging { resources { excludes += "/META-INF/{AL2.0,LGPL2.1}" } }
+}
+
+dependencies {
+  implementation("androidx.core:core-ktx:1.9.0")
+  implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.1")
+  implementation("androidx.activity:activity-compose:1.7.0")
+  implementation(platform("androidx.compose:compose-bom:2023.03.00"))
+  implementation("androidx.compose.ui:ui")
+  implementation("androidx.compose.ui:ui-graphics")
+  implementation("androidx.compose.ui:ui-tooling-preview")
+  implementation("androidx.compose.material3:material3")
+  implementation("androidx.appcompat:appcompat:1.6.1")
+  implementation("androidx.camera:camera-core:1.3.0-rc02")
+  implementation("androidx.constraintlayout:constraintlayout:2.2.0-alpha12")
+  implementation("com.facebook.fbjni:fbjni:0.5.1")
+  implementation("com.google.code.gson:gson:2.8.6")
+  implementation("com.google.android.material:material:1.12.0")
+  implementation("androidx.activity:activity:1.9.0")
+  testImplementation("junit:junit:4.13.2")
+  androidTestImplementation("androidx.test.ext:junit:1.1.5")
+  androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
+  androidTestImplementation(platform("androidx.compose:compose-bom:2023.03.00"))
+  androidTestImplementation("androidx.compose.ui:ui-test-junit4")
+  debugImplementation("androidx.compose.ui:ui-tooling")
+  debugImplementation("androidx.compose.ui:ui-test-manifest")
+  implementation("com.squareup.okhttp3:okhttp:4.10.0")
+  implementation("com.google.guava:guava:31.0-jre")
+  implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.18.1")
+  implementation("com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.18.1")
+  implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.18.1")
+  implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.2.1")
+  implementation("io.noties.markwon:core:4.6.2")
+}

+ 21 - 0
getting-started/demo/ArticleSummarizer/app/proguard-rules.pro

@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+#   http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+#   public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile

+ 65 - 0
getting-started/demo/ArticleSummarizer/app/src/main/AndroidManifest.xml

@@ -0,0 +1,65 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools">
+
+    <uses-sdk
+        android:maxSdkVersion="40"
+        android:minSdkVersion="28"
+        android:targetSdkVersion="34" />
+
+    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+    <uses-permission android:name="android.permission.CAMERA" />
+    <uses-permission android:name="android.permission.INTERNET" />
+    <uses-feature android:name="android.hardware.camera" />
+
+    <application
+        android:name="com.example.llamaandroiddemo.AppLogging"
+        android:allowBackup="false"
+        android:dataExtractionRules="@xml/data_extraction_rules"
+        android:extractNativeLibs="true"
+        android:fullBackupContent="@xml/backup_rules"
+        android:icon="@drawable/summarizer"
+        android:label="@string/app_name"
+        android:supportsRtl="true"
+        android:theme="@style/Theme.AppCompat.Light.NoActionBar"
+        tools:targetApi="34">
+        <profileable android:shell="true" />
+        <activity
+            android:name="com.example.llamaandroiddemo.LogsActivity"
+            android:exported="false" />
+        <activity
+            android:name="com.example.llamaandroiddemo.SettingsActivity"
+            android:exported="false" />
+        <activity
+            android:name="com.example.llamaandroiddemo.MainActivity"
+            android:exported="false" />
+
+        <activity
+            android:name="com.example.llamaandroiddemo.HomescreenActivity"
+            android:exported="true"
+            android:label="@string/app_name"
+            android:theme="@style/Theme.AppCompat.Light.NoActionBar">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+            <intent-filter>
+                <action android:name="android.intent.action.INSERT" />
+                <category android:name="android.intent.category.APP_CALENDAR" />
+            </intent-filter>
+        </activity>
+
+        <provider
+            android:name="androidx.core.content.FileProvider"
+            android:authorities="${applicationId}.fileprovider"
+            android:exported="false"
+            android:grantUriPermissions="true">
+            <meta-data
+                android:name="android.support.FILE_PROVIDER_PATHS"
+                android:resource="@xml/file_paths" />
+        </provider>
+
+    </application>
+
+</manifest>

+ 49 - 0
getting-started/demo/ArticleSummarizer/app/src/main/java/com/example/llamaandroiddemo/AppLog.java

@@ -0,0 +1,49 @@
+/*
+ * 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.llamaandroiddemo;
+
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+
+public class AppLog {
+  private final Long timestamp;
+  private final String message;
+
+  public AppLog(String message) {
+    this.timestamp = getCurrentTimeStamp();
+    this.message = message;
+  }
+
+  public Long getTimestamp() {
+    return timestamp;
+  }
+
+  public String getMessage() {
+    return message;
+  }
+
+  public String getFormattedLog() {
+    return "[" + getFormattedTimeStamp() + "] " + message;
+  }
+
+  private Long getCurrentTimeStamp() {
+    return System.currentTimeMillis();
+  }
+
+  private String getFormattedTimeStamp() {
+    return formatDate(timestamp);
+  }
+
+  private String formatDate(long milliseconds) {
+    SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd  HH:mm:ss", Locale.getDefault());
+    Date date = new Date(milliseconds);
+    return formatter.format(date);
+  }
+}

+ 54 - 0
getting-started/demo/ArticleSummarizer/app/src/main/java/com/example/llamaandroiddemo/AppLogging.java

@@ -0,0 +1,54 @@
+/*
+ * 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.llamaandroiddemo;
+
+import android.app.Application;
+import android.util.Log;
+import java.util.ArrayList;
+
+public class AppLogging extends Application {
+  private static AppLogging singleton;
+
+  private ArrayList<AppLog> logs;
+  private DemoSharedPreferences mDemoSharedPreferences;
+
+  @Override
+  public void onCreate() {
+    super.onCreate();
+    singleton = this;
+    mDemoSharedPreferences = new DemoSharedPreferences(this.getApplicationContext());
+    logs = mDemoSharedPreferences.getSavedLogs();
+    if (logs == null) { // We don't have existing sharedPreference stored
+      logs = new ArrayList<>();
+    }
+  }
+
+  public static AppLogging getInstance() {
+    return singleton;
+  }
+
+  public void log(String message) {
+    AppLog appLog = new AppLog(message);
+    logs.add(appLog);
+    Log.d("AppLogging", appLog.getMessage());
+  }
+
+  public ArrayList<AppLog> getLogs() {
+    return logs;
+  }
+
+  public void clearLogs() {
+    logs.clear();
+    mDemoSharedPreferences.removeExistingLogs();
+  }
+
+  public void saveLogs() {
+    mDemoSharedPreferences.saveLogs();
+  }
+}

+ 13 - 0
getting-started/demo/ArticleSummarizer/app/src/main/java/com/example/llamaandroiddemo/AppUtils.java

@@ -0,0 +1,13 @@
+package com.example.llamaandroiddemo;
+
+public class AppUtils {
+	// Generation Mode
+	public static final int CONVERSATION_HISTORY_MESSAGE_LOOKBACK = 1;
+
+	// Note: This is an example project to demonstrate E2E flow.
+	// You should NOT use/store API key directly on client
+	// Exposing your API key in client-side environments allows malicious users to take
+	// that key and make requests on your behalf. Requests should always be routed through
+	// your own backend server where you can keep your API key secure.
+	public static final String API_KEY = "";
+}

+ 5 - 0
getting-started/demo/ArticleSummarizer/app/src/main/java/com/example/llamaandroiddemo/BackendType.java

@@ -0,0 +1,5 @@
+package com.example.llamaandroiddemo;
+
+public enum BackendType {
+  XNNPACK
+}

+ 90 - 0
getting-started/demo/ArticleSummarizer/app/src/main/java/com/example/llamaandroiddemo/DemoSharedPreferences.java

@@ -0,0 +1,90 @@
+/*
+ * 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.llamaandroiddemo;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import com.google.gson.Gson;
+import com.google.gson.reflect.TypeToken;
+import java.lang.reflect.Type;
+import java.util.ArrayList;
+
+public class DemoSharedPreferences {
+  Context context;
+  SharedPreferences sharedPreferences;
+
+  public DemoSharedPreferences(Context context) {
+    this.context = context;
+    this.sharedPreferences = getSharedPrefs();
+  }
+
+  private SharedPreferences getSharedPrefs() {
+    return context.getSharedPreferences(
+        context.getString(R.string.demo_pref_file_key), Context.MODE_PRIVATE);
+  }
+
+  public String getSavedMessages() {
+    return sharedPreferences.getString(context.getString(R.string.saved_messages_json_key), "");
+  }
+
+  public void addMessages(MessageAdapter messageAdapter) {
+    SharedPreferences.Editor editor = sharedPreferences.edit();
+    Gson gson = new Gson();
+    String msgJSON = gson.toJson(messageAdapter.getSavedMessages());
+    editor.putString(context.getString(R.string.saved_messages_json_key), msgJSON);
+    editor.apply();
+  }
+
+  public void removeExistingMessages() {
+    SharedPreferences.Editor editor = sharedPreferences.edit();
+    editor.remove(context.getString(R.string.saved_messages_json_key));
+    editor.apply();
+  }
+
+  public void addSettings(SettingsFields settingsFields) {
+    SharedPreferences.Editor editor = sharedPreferences.edit();
+    Gson gson = new Gson();
+    String settingsJSON = gson.toJson(settingsFields);
+    editor.putString(context.getString(R.string.settings_json_key), settingsJSON);
+    editor.apply();
+  }
+
+  public String getSettings() {
+    return sharedPreferences.getString(context.getString(R.string.settings_json_key), "");
+  }
+
+  public void saveLogs() {
+    SharedPreferences.Editor editor = sharedPreferences.edit();
+    Gson gson = new Gson();
+    String msgJSON = gson.toJson(AppLogging.getInstance().getLogs());
+    editor.putString(context.getString(R.string.logs_json_key), msgJSON);
+    editor.apply();
+  }
+
+  public void removeExistingLogs() {
+    SharedPreferences.Editor editor = sharedPreferences.edit();
+    editor.remove(context.getString(R.string.logs_json_key));
+    editor.apply();
+  }
+
+  public ArrayList<AppLog> getSavedLogs() {
+    String logsJSONString =
+        sharedPreferences.getString(context.getString(R.string.logs_json_key), null);
+    if (logsJSONString == null || logsJSONString.isEmpty()) {
+      return new ArrayList<>();
+    }
+    Gson gson = new Gson();
+    Type type = new TypeToken<ArrayList<AppLog>>() {}.getType();
+    ArrayList<AppLog> appLogs = gson.fromJson(logsJSONString, type);
+    if (appLogs == null) {
+      return new ArrayList<>();
+    }
+    return appLogs;
+  }
+}

+ 240 - 0
getting-started/demo/ArticleSummarizer/app/src/main/java/com/example/llamaandroiddemo/ExampleLlamaRemoteInference.kt

@@ -0,0 +1,240 @@
+package com.example.llamaandroiddemo
+
+import android.content.ContentResolver
+import android.content.Context
+import android.database.Cursor
+import android.net.Uri
+import android.provider.MediaStore
+import android.util.Base64
+import android.util.Log
+import okhttp3.MediaType.Companion.toMediaType
+import okhttp3.OkHttpClient
+import okhttp3.Request
+import okhttp3.RequestBody
+import okhttp3.RequestBody.Companion.toRequestBody
+import okhttp3.Response
+import okio.IOException
+import org.json.JSONObject
+import java.io.File
+import java.net.URLConnection
+import java.util.concurrent.CompletableFuture
+import java.util.concurrent.TimeUnit
+import androidx.core.net.toUri
+
+interface InferenceStreamingCallback {
+    fun onStreamReceived(message: String)
+    fun onStatStreamReceived(tps: Float)
+}
+
+class ExampleLlamaRemoteInference(remoteURL: String) {
+
+    var remoteURL: String = ""
+
+    init {
+            this.remoteURL = remoteURL
+    }
+
+    fun inferenceStartWithoutAgent(modelName: String, temperature: Double, prompt: ArrayList<Message>, userProvidedSystemPrompt:String, ctx: Context): String {
+        val future = CompletableFuture<String>()
+        val thread = Thread {
+            try {
+                val response = inferenceCallWithoutAgent(modelName, temperature, prompt, userProvidedSystemPrompt, ctx, true);
+                future.complete(response)
+            } catch (e: Exception) {
+                e.printStackTrace()
+            }
+        }
+        thread.start();
+        return future.get();
+    }
+
+    fun makeStreamingPostRequest(url: String, requestBody: RequestBody): Response {
+
+        val client = OkHttpClient.Builder()
+            .readTimeout(0, TimeUnit.MILLISECONDS) // No timeout for streaming
+            .build()
+
+        val request = Request.Builder()
+            .url(url)
+            .addHeader("Authorization","Bearer " + AppUtils.API_KEY)
+            .addHeader("Accept", "text/event-stream")
+            .post(requestBody)
+            .build()
+
+        return client.newCall(request).execute()
+    }
+
+    private fun llamaChatCompletion(ctx: Context, modelName: String, conversationHistory: ArrayList<Message>, userProvidedSystemPrompt: String){
+        var msg = """
+                        {
+                          "role": "system",
+                          "content": "$userProvidedSystemPrompt"
+                        },
+        """.trimIndent()
+
+        msg += constructMessageForAPICall(conversationHistory,ctx)
+        val thread = Thread {
+            try {
+                // Create request body
+                val json = """
+                {
+                    "messages": [
+                         $msg
+                    ],
+                    "model": "$modelName",
+                    "repetition_penalty": 1,
+                    "temperature": 0.6,
+                    "top_p": 0.9,
+                    "max_completion_tokens": 2048,
+                    "stream": true
+                }""".trimIndent()
+                val requestBody = json.toRequestBody("application/json".toMediaType())
+                // Make request
+                val response = makeStreamingPostRequest("$remoteURL/v1/chat/completions", requestBody)
+                val callback = ctx as InferenceStreamingCallback
+
+                // Process streaming response
+                response.use { res ->
+                    if (!res.isSuccessful) throw IOException("Unexpected code $res")
+
+                    res.body?.source()?.let { source ->
+                        while (!source.exhausted()) {
+                            val streamDelta = source.readUtf8Line()
+                            if (streamDelta != null){
+                                val jsonString = streamDelta.substringAfter("data: ")
+                                if (jsonString != ""){
+                                    val obj = JSONObject(jsonString)
+                                    if (obj.has("choices")) {
+                                        val choices = obj.getJSONArray("choices")
+                                        if (choices.length() > 0) {
+                                            val choice = choices.getJSONObject(0)
+                                            if (choice.has("delta")) {
+                                                val delta = choice.getJSONObject("delta")
+                                                if (delta.has("content")) {
+                                                    val result = delta.getString("content")
+                                                    callback.onStreamReceived(result)
+                                                }
+                                            } else if (choice.has("text")) {
+                                                val result = choice.getString("text")
+                                                callback.onStreamReceived(result)
+                                            }
+                                        }
+                                    }
+                                }
+                            }
+                        }
+                    }
+                }
+            } catch (e: Exception) {
+                Log.d("error",e.message.toString())
+                e.printStackTrace()
+            }
+        }
+        thread.start();
+    }
+
+    //Example running simple inference + tool calls without using agent's workflow
+    private fun inferenceCallWithoutAgent(modelName: String, temperature: Double, conversationHistory: ArrayList<Message>, userProvidedSystemPrompt: String, ctx: Context, streaming: Boolean): String {
+
+        llamaChatCompletion(ctx,modelName,conversationHistory, userProvidedSystemPrompt)
+        return ""
+    }
+
+    private fun encodeImageToDataUrl(filePath: String): String {
+        val mimeType = URLConnection.guessContentTypeFromName(filePath)
+            ?: throw RuntimeException("Could not determine MIME type of the file")
+        val imageFile = File(filePath)
+        val encodedString = Base64.encodeToString(imageFile.readBytes(), Base64.NO_WRAP)
+        return "data:image/jpeg;base64,$encodedString"
+    }
+
+    private fun getFilePathFromUri(contentResolver: ContentResolver, uri: Uri): String? {
+        var filePath: String? = null
+        val projection = arrayOf(MediaStore.Images.Media.DATA)
+        val cursor: Cursor? = contentResolver.query(uri, projection, null, null, null)
+        cursor?.use {
+            if (it.moveToFirst()) {
+                val columnIndex = it.getColumnIndexOrThrow(MediaStore.Images.Media.DATA)
+                filePath = it.getString(columnIndex)
+            }
+        }
+        return filePath
+    }
+
+    private fun constructMessageForAPICall(
+        conversationHistory: ArrayList<Message>,
+        ctx: Context
+    ):String {
+        var prompt = ""
+        var isPreviousChatImage = false
+        val imagePromptList = ArrayList<String>()
+        for ((index, chat) in conversationHistory.withIndex()) {
+            if (chat.isSent) {
+                // First image in the chat. Image must pair with a prompt
+                if (chat.messageType == MessageType.IMAGE) {
+                    val imageUri = chat.imagePath.toUri()
+                    val contentResolver = ctx.contentResolver
+                    val imageFilePath = getFilePathFromUri(contentResolver, imageUri)
+                    val imageDataUrl = imageFilePath?.let { encodeImageToDataUrl(it) }
+                    Log.d("imageDataURL",imageDataUrl.toString())
+                    imagePromptList += """     
+                        {
+                              "type": "image_url",
+                              "image_url": {
+                                "url": "$imageDataUrl"
+                              }
+                        }
+                    """.trimIndent()
+                    isPreviousChatImage = true
+                    continue
+                }
+                // Prompt right after the image
+                else if (chat.messageType == MessageType.TEXT) {
+                    if (isPreviousChatImage) {
+                        var imagePrompts = ""
+                        for ((idx, image) in imagePromptList.withIndex()) {
+                            imagePrompts += image
+                            if (idx < imagePromptList.lastIndex) {
+                                imagePrompts += ","
+                            }
+                        }
+                        prompt += """
+                            {
+                                "role": "user",
+                                "content": [
+                                    $imagePrompts,
+                                    {
+                                        "type": "text",
+                                        "text": "${chat.text}"
+                                    }
+                                ]
+                            }
+                        """.trimIndent()
+                        isPreviousChatImage = false
+                    } else {
+                        prompt += """
+                            {
+                              "role": "user",
+                              "content": "${chat.text}"
+                            }                                            
+                        """.trimIndent()
+                    }
+                }
+            } else {
+                // assistant message/response
+                // only text response
+                prompt += """
+                    {  "role": "assistant", 
+                       "content": ${JSONObject.quote(chat.text)}
+                    }                   
+                """.trimIndent()
+            }
+            if (chat.messageType != MessageType.IMAGE && index != conversationHistory.lastIndex) {
+                // This is NOT the last chat and not image
+                prompt += ","
+            }
+        }
+        Log.d("inference", "this is prompt: $prompt")
+        return prompt
+    }
+}

+ 28 - 0
getting-started/demo/ArticleSummarizer/app/src/main/java/com/example/llamaandroiddemo/HomescreenActivity.kt

@@ -0,0 +1,28 @@
+package com.example.llamaandroiddemo
+
+import android.content.Intent
+import androidx.appcompat.app.AppCompatActivity
+import android.os.Bundle
+import android.view.View
+import android.widget.Button
+import android.widget.TextView
+
+class HomescreenActivity : AppCompatActivity() {
+
+    private lateinit var startChatButton: Button
+    private lateinit var introTextView: TextView
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        setContentView(R.layout.activity_homescreen)
+
+        // Initialize UI components
+        startChatButton = findViewById(R.id.btn_start_chat)
+
+        // Set up start chat button click listener
+        startChatButton.setOnClickListener {
+            val intent = Intent(this, MainActivity::class.java)
+            startActivity(intent)
+        }
+    }
+}

+ 65 - 0
getting-started/demo/ArticleSummarizer/app/src/main/java/com/example/llamaandroiddemo/LanguageSelector.java

@@ -0,0 +1,65 @@
+package com.example.llamaandroiddemo;
+
+import android.app.AlertDialog;
+import android.content.Context;
+import java.util.ArrayList;
+import java.util.List;
+
+public class LanguageSelector {
+
+    private final Context context;
+    private final String[] languages = {
+            "Arabic",
+            "English",
+            "French",
+            "German",
+            "Hindi",
+            "Indonesian",
+            "Italian",
+            "Portuguese",
+            "Spanish",
+            "Tagalog",
+            "Thai",
+            "Vietnamese"
+    };
+
+    private final boolean[] checkedLanguages = new boolean[languages.length];
+
+    public LanguageSelector(Context context) {
+        this.context = context;
+    }
+
+    public String getSelectedLanguage() {
+        List<String> selectedLanguages = new ArrayList<>();
+        for (int i = 0; i < checkedLanguages.length; i++) {
+            if (checkedLanguages[i]) {
+                selectedLanguages.add(languages[i]);
+            }
+        }
+        return String.join(", ", selectedLanguages);
+    }
+    public void showLanguageSelector() {
+        AlertDialog.Builder builder = new AlertDialog.Builder(context);
+        builder.setTitle("Select Languages");
+
+        builder.setMultiChoiceItems(languages, checkedLanguages, (dialog, which, isChecked) -> {
+            checkedLanguages[which] = isChecked;
+        });
+
+        builder.setPositiveButton("OK", (dialog, which) -> {
+            List<String> selectedLanguages = new ArrayList<>();
+            for (int i = 0; i < checkedLanguages.length; i++) {
+                if (checkedLanguages[i]) {
+                    selectedLanguages.add(languages[i]);
+                }
+            }
+
+            // Store the result into a string
+            String result = String.join(", ", selectedLanguages);
+            // Do something with the result
+            System.out.println(result);
+        });
+
+        builder.show();
+    }
+}

+ 92 - 0
getting-started/demo/ArticleSummarizer/app/src/main/java/com/example/llamaandroiddemo/LogsActivity.java

@@ -0,0 +1,92 @@
+/*
+ * 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.llamaandroiddemo;
+
+import android.app.AlertDialog;
+import android.content.DialogInterface;
+import android.os.Build;
+import android.os.Bundle;
+import android.widget.ImageButton;
+import android.widget.ListView;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.core.content.ContextCompat;
+import androidx.core.graphics.Insets;
+import androidx.core.view.ViewCompat;
+import androidx.core.view.WindowInsetsCompat;
+
+public class LogsActivity extends AppCompatActivity {
+
+  private LogsAdapter mLogsAdapter;
+
+  @Override
+  protected void onCreate(Bundle savedInstanceState) {
+    super.onCreate(savedInstanceState);
+    setContentView(R.layout.activity_logs);
+    if (Build.VERSION.SDK_INT >= 21) {
+      getWindow().setStatusBarColor(ContextCompat.getColor(this, R.color.status_bar));
+      getWindow().setNavigationBarColor(ContextCompat.getColor(this, R.color.nav_bar));
+    }
+    ViewCompat.setOnApplyWindowInsetsListener(
+        requireViewById(R.id.main),
+        (v, insets) -> {
+          Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
+          v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
+          return insets;
+        });
+
+    setupLogs();
+    setupClearLogsButton();
+  }
+
+  @Override
+  public void onResume() {
+    super.onResume();
+    mLogsAdapter.clear();
+    mLogsAdapter.addAll(AppLogging.getInstance().getLogs());
+    mLogsAdapter.notifyDataSetChanged();
+  }
+
+  private void setupLogs() {
+    ListView mLogsListView = requireViewById(R.id.logsListView);
+    mLogsAdapter = new LogsAdapter(this, R.layout.logs_message);
+
+    mLogsListView.setAdapter(mLogsAdapter);
+    mLogsAdapter.addAll(AppLogging.getInstance().getLogs());
+    mLogsAdapter.notifyDataSetChanged();
+  }
+
+  private void setupClearLogsButton() {
+    ImageButton clearLogsButton = requireViewById(R.id.clearLogsButton);
+    clearLogsButton.setOnClickListener(
+        view -> {
+          new AlertDialog.Builder(this)
+              .setTitle("Delete Logs History")
+              .setMessage("Do you really want to delete logs history?")
+              .setIcon(android.R.drawable.ic_dialog_alert)
+              .setPositiveButton(
+                  android.R.string.yes,
+                  new DialogInterface.OnClickListener() {
+                    public void onClick(DialogInterface dialog, int whichButton) {
+                      // Clear the messageAdapter and sharedPreference
+                      AppLogging.getInstance().clearLogs();
+                      mLogsAdapter.clear();
+                      mLogsAdapter.notifyDataSetChanged();
+                    }
+                  })
+              .setNegativeButton(android.R.string.no, null)
+              .show();
+        });
+  }
+
+  @Override
+  protected void onDestroy() {
+    super.onDestroy();
+    AppLogging.getInstance().saveLogs();
+  }
+}

+ 45 - 0
getting-started/demo/ArticleSummarizer/app/src/main/java/com/example/llamaandroiddemo/LogsAdapter.java

@@ -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.llamaandroiddemo;
+
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.TextView;
+import androidx.annotation.NonNull;
+import java.util.Objects;
+
+public class LogsAdapter extends ArrayAdapter<AppLog> {
+  public LogsAdapter(android.content.Context context, int resource) {
+    super(context, resource);
+  }
+
+  static class ViewHolder {
+    private TextView logTextView;
+  }
+
+  @NonNull
+  @Override
+  public View getView(int position, View convertView, @NonNull ViewGroup parent) {
+    ViewHolder mViewHolder = null;
+
+    String logMessage = Objects.requireNonNull(getItem(position)).getFormattedLog();
+
+    if (convertView == null || convertView.getTag() == null) {
+      mViewHolder = new ViewHolder();
+      convertView = LayoutInflater.from(getContext()).inflate(R.layout.logs_message, parent, false);
+      mViewHolder.logTextView = convertView.requireViewById(R.id.logsTextView);
+    } else {
+      mViewHolder = (ViewHolder) convertView.getTag();
+    }
+    mViewHolder.logTextView.setText(logMessage);
+    return convertView;
+  }
+}

+ 571 - 0
getting-started/demo/ArticleSummarizer/app/src/main/java/com/example/llamaandroiddemo/MainActivity.java

@@ -0,0 +1,571 @@
+/*
+ * 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.llamaandroiddemo;
+
+import android.Manifest;
+import android.app.ActivityManager;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Process;
+import android.provider.MediaStore;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.util.Log;
+import android.view.View;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.EditText;
+import android.widget.ImageButton;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.ListView;
+import android.widget.TextView;
+import android.widget.Toast;
+import androidx.activity.result.ActivityResultLauncher;
+import androidx.activity.result.PickVisualMediaRequest;
+import androidx.activity.result.contract.ActivityResultContracts;
+import androidx.annotation.NonNull;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.constraintlayout.widget.ConstraintLayout;
+import androidx.core.app.ActivityCompat;
+import androidx.core.content.ContextCompat;
+import com.google.gson.Gson;
+import com.google.gson.reflect.TypeToken;
+import java.lang.reflect.Type;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+
+public class MainActivity extends AppCompatActivity implements Runnable, InferenceStreamingCallback {
+  private EditText mEditTextMessage;
+  private ImageButton mSendButton;
+  private ImageButton mGalleryButton;
+  private ImageButton mCameraButton;
+  private ListView mMessagesView;
+  private MessageAdapter mMessageAdapter;
+  private Message mResultMessage = null;
+  private ImageButton mSettingsButton;
+  private TextView mMemoryView;
+  private ActivityResultLauncher<PickVisualMediaRequest> mPickGallery;
+  private ActivityResultLauncher<Uri> mCameraRoll;
+  private List<Uri> mSelectedImageUri;
+  private ConstraintLayout mMediaPreviewConstraintLayout;
+  private LinearLayout mAddMediaLayout;
+  private static final int MAX_NUM_OF_IMAGES = 5;
+  private static final int REQUEST_IMAGE_CAPTURE = 1;
+  private TextView mGenerationModeButton;
+  private Uri cameraImageUri;
+  private DemoSharedPreferences mDemoSharedPreferences;
+  private SettingsFields mCurrentSettingsFields;
+  private Handler mMemoryUpdateHandler;
+  private Runnable memoryUpdater;
+  private int promptID = 0;
+  private Executor executor;
+  private ExampleLlamaRemoteInference exampleLlamaRemoteInference;
+  private LanguageSelector languageSelector;
+  private void populateExistingMessages(String existingMsgJSON) {
+    Gson gson = new Gson();
+    Type type = new TypeToken<ArrayList<Message>>() {}.getType();
+    ArrayList<Message> savedMessages = gson.fromJson(existingMsgJSON, type);
+    for (Message msg : savedMessages) {
+      mMessageAdapter.add(msg);
+    }
+    mMessageAdapter.notifyDataSetChanged();
+  }
+
+  private int setPromptID() {
+    return mMessageAdapter.getMaxPromptID() + 1;
+  }
+
+  @Override
+  protected void onCreate(Bundle savedInstanceState) {
+    super.onCreate(savedInstanceState);
+    setContentView(R.layout.activity_main);
+	  getWindow().setStatusBarColor(ContextCompat.getColor(this, R.color.status_bar));
+	  getWindow().setNavigationBarColor(ContextCompat.getColor(this, R.color.nav_bar));
+
+	  try {
+      Os.setenv("ADSP_LIBRARY_PATH", getApplicationInfo().nativeLibraryDir, true);
+      Os.setenv("LD_LIBRARY_PATH", getApplicationInfo().nativeLibraryDir, true);
+    } catch (ErrnoException e) {
+      finish();
+    }
+
+    mEditTextMessage = requireViewById(R.id.editTextMessage);
+    mSendButton = requireViewById(R.id.sendButton);
+    mSendButton.setEnabled(true);
+    mMessagesView = requireViewById(R.id.messages_view);
+    mMessageAdapter = new MessageAdapter(this, R.layout.sent_message, new ArrayList<Message>());
+    mMessagesView.setAdapter(mMessageAdapter);
+    mDemoSharedPreferences = new DemoSharedPreferences(this.getApplicationContext());
+    String existingMsgJSON = mDemoSharedPreferences.getSavedMessages();
+    if (!existingMsgJSON.isEmpty()) {
+      populateExistingMessages(existingMsgJSON);
+      promptID = setPromptID();
+    }
+    mSettingsButton = requireViewById(R.id.settings);
+    mSettingsButton.setOnClickListener(
+            view -> {
+              Intent myIntent = new Intent(MainActivity.this, SettingsActivity.class);
+              MainActivity.this.startActivity(myIntent);
+            });
+
+    mCurrentSettingsFields = new SettingsFields();
+    mMemoryUpdateHandler = new Handler(Looper.getMainLooper());
+    setupMediaButton();
+    setupGalleryPicker();
+    setupCameraRoll();
+    startMemoryUpdate();
+    setupShowLogsButton();
+    setupLanguageSelector();
+    executor = Executors.newSingleThreadExecutor();
+    onModelRunStopped();
+  }
+
+  private void setupLanguageSelector() {
+    languageSelector = new LanguageSelector(this);
+    ImageButton button = findViewById(R.id.addLanguageButton);
+    button.setOnClickListener(
+            view -> {
+              Log.d("hello","you click language button");
+              languageSelector.showLanguageSelector();
+            }
+    );
+  }
+
+  @Override
+  protected void onPause() {
+    super.onPause();
+    mDemoSharedPreferences.addMessages(mMessageAdapter);
+  }
+
+  @Override
+  protected void onResume() {
+    super.onResume();
+    // Check for if settings parameters have changed
+    AppLogging.getInstance().log("onResume is called");
+    Gson gson = new Gson();
+    String settingsFieldsJSON = mDemoSharedPreferences.getSettings();
+    if (!settingsFieldsJSON.isEmpty()) {
+      SettingsFields updatedSettingsFields =
+              gson.fromJson(settingsFieldsJSON, SettingsFields.class);
+      if (updatedSettingsFields == null) {
+        // Added this check, because gson.fromJson can return null
+        askUserToSelectModel();
+        return;
+      }
+      AppLogging.getInstance().log("test "+ updatedSettingsFields.getRemoteURL());
+
+      boolean isUpdated = !mCurrentSettingsFields.equals(updatedSettingsFields);
+      boolean isLoadModel = updatedSettingsFields.getIsLoadModel();
+      if (isUpdated || isLoadModel) {
+        if (!mCurrentSettingsFields.getRemoteURL().equals(updatedSettingsFields.getRemoteURL())) {
+          // Remote URL changes
+          AppLogging.getInstance().log(mCurrentSettingsFields.getRemoteURL() + "remote URL is changing to " + updatedSettingsFields.getRemoteURL());
+          exampleLlamaRemoteInference = new ExampleLlamaRemoteInference(updatedSettingsFields.getRemoteURL());
+        }
+
+        AppLogging.getInstance().log("llamaRemoteInference " + (exampleLlamaRemoteInference == null));
+
+        if (exampleLlamaRemoteInference == null) {
+          askUserToSelectModel();
+        } else {
+          String message = "Remote inference with model: " + updatedSettingsFields.getRemoteModel();
+          addSystemMessage(message);
+        }
+
+        checkForClearChatHistory(updatedSettingsFields);
+        // Update current to point to the latest
+        mCurrentSettingsFields = new SettingsFields(updatedSettingsFields);
+        AppLogging.getInstance().log("onResume mCurrentSettingsFields " + mCurrentSettingsFields);
+      }
+    } else {
+      askUserToSelectModel();
+    }
+  }
+
+  private void checkForClearChatHistory(SettingsFields updatedSettingsFields) {
+    if (updatedSettingsFields.getIsClearChatHistory()) {
+      mMessageAdapter.clear();
+      mMessageAdapter.notifyDataSetChanged();
+      mDemoSharedPreferences.removeExistingMessages();
+      // changing to false since chat history has been cleared.
+      updatedSettingsFields.saveIsClearChatHistory(false);
+      mDemoSharedPreferences.addSettings(updatedSettingsFields);
+    }
+  }
+
+  private void addSystemMessage(String message) {
+    Message systemMessage = new Message(message, false, MessageType.SYSTEM, 0);
+    AppLogging.getInstance().log(message);
+    runOnUiThread(
+            () -> {
+              mMessageAdapter.add(systemMessage);
+              mMessageAdapter.notifyDataSetChanged();
+            });
+  }
+
+  private void askUserToSelectModel() {
+    String askLoadModel =
+            "To get started, configure remote URL" +
+            "from the top right corner";
+    addSystemMessage(askLoadModel);
+  }
+
+  private void setupShowLogsButton() {
+    ImageButton showLogsButton = requireViewById(R.id.showLogsButton);
+    showLogsButton.setOnClickListener(
+            view -> {
+              Intent myIntent = new Intent(MainActivity.this, LogsActivity.class);
+              MainActivity.this.startActivity(myIntent);
+            });
+  }
+
+  private void setupMediaButton() {
+    mAddMediaLayout = requireViewById(R.id.addMediaLayout);
+    mAddMediaLayout.setVisibility(View.GONE); // We hide this initially
+
+    ImageButton addMediaButton = requireViewById(R.id.addMediaButton);
+    addMediaButton.setOnClickListener(
+            view -> {
+              mAddMediaLayout.setVisibility(View.VISIBLE);
+            });
+
+    mGalleryButton = requireViewById(R.id.galleryButton);
+    mGalleryButton.setOnClickListener(
+            view -> {
+              // Launch the photo picker and let the user choose only images.
+              mPickGallery.launch(
+                      new PickVisualMediaRequest.Builder()
+                              .setMediaType(ActivityResultContracts.PickVisualMedia.ImageOnly.INSTANCE)
+                              .build());
+            });
+    mCameraButton = requireViewById(R.id.cameraButton);
+    mCameraButton.setOnClickListener(
+            view -> {
+              Log.d("CameraRoll", "Check permission");
+              if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.CAMERA)
+                          != PackageManager.PERMISSION_GRANTED) {
+                ActivityCompat.requestPermissions(
+                        MainActivity.this,
+                        new String[] {Manifest.permission.CAMERA},
+                        REQUEST_IMAGE_CAPTURE);
+              } else {
+                launchCamera();
+              }
+            });
+  }
+
+  private void setupCameraRoll() {
+    // Registers a camera roll activity launcher.
+    mCameraRoll =
+            registerForActivityResult(
+                    new ActivityResultContracts.TakePicture(),
+                    result -> {
+                      if (result && cameraImageUri != null) {
+                        Log.d("CameraRoll", "Photo saved to uri: " + cameraImageUri);
+                        mAddMediaLayout.setVisibility(View.GONE);
+                        List<Uri> uris = new ArrayList<>();
+                        uris.add(cameraImageUri);
+                        showMediaPreview(uris);
+                      } else {
+                        // Delete the temp image file based on the url since the photo is not successfully taken
+                        if (cameraImageUri != null) {
+                          ContentResolver contentResolver = MainActivity.this.getContentResolver();
+                          contentResolver.delete(cameraImageUri, null, null);
+                          Log.d("CameraRoll", "No photo taken. Delete temp uri");
+                        }
+                      }
+                    });
+    mMediaPreviewConstraintLayout = requireViewById(R.id.mediaPreviewConstraintLayout);
+    ImageButton mediaPreviewCloseButton = requireViewById(R.id.mediaPreviewCloseButton);
+    mediaPreviewCloseButton.setOnClickListener(
+            view -> {
+              mMediaPreviewConstraintLayout.setVisibility(View.GONE);
+              mSelectedImageUri = null;
+            });
+
+    ImageButton addMoreImageButton = requireViewById(R.id.addMoreImageButton);
+    addMoreImageButton.setOnClickListener(
+            view -> {
+              Log.d("addMore", "clicked");
+              mMediaPreviewConstraintLayout.setVisibility(View.GONE);
+              // Direct user to select type of input
+              mCameraButton.callOnClick();
+            });
+  }
+
+  private String updateMemoryUsage() {
+    ActivityManager.MemoryInfo memoryInfo = new ActivityManager.MemoryInfo();
+    ActivityManager activityManager = (ActivityManager) getSystemService(ACTIVITY_SERVICE);
+    if (activityManager == null) {
+      return "---";
+    }
+    activityManager.getMemoryInfo(memoryInfo);
+    long totalMem = memoryInfo.totalMem / (1024 * 1024);
+    long availableMem = memoryInfo.availMem / (1024 * 1024);
+    long usedMem = totalMem - availableMem;
+    return usedMem + "MB";
+  }
+
+  private void startMemoryUpdate() {
+    mMemoryView = requireViewById(R.id.ram_usage_live);
+    memoryUpdater =
+            new Runnable() {
+              @Override
+              public void run() {
+                mMemoryView.setText(updateMemoryUsage());
+                mMemoryUpdateHandler.postDelayed(this, 1000);
+              }
+            };
+    mMemoryUpdateHandler.post(memoryUpdater);
+  }
+
+  @Override
+  public void onRequestPermissionsResult(
+          int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
+    super.onRequestPermissionsResult(requestCode, permissions, grantResults);
+    if (requestCode == REQUEST_IMAGE_CAPTURE && grantResults.length != 0) {
+      if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+        launchCamera();
+      } else if (grantResults[0] == PackageManager.PERMISSION_DENIED) {
+        Log.d("CameraRoll", "Permission denied");
+      }
+    }
+  }
+
+  private void launchCamera() {
+    ContentValues values = new ContentValues();
+    values.put(MediaStore.Images.Media.TITLE, "New Picture");
+    values.put(MediaStore.Images.Media.DESCRIPTION, "From Camera");
+    values.put(MediaStore.Images.Media.RELATIVE_PATH, "DCIM/Camera/");
+    cameraImageUri =
+            MainActivity.this
+                    .getContentResolver()
+                    .insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
+    mCameraRoll.launch(cameraImageUri);
+  }
+
+  private void setupGalleryPicker() {
+    // Registers a photo picker activity launcher in single-select mode.
+    mPickGallery =
+            registerForActivityResult(
+                    new ActivityResultContracts.PickMultipleVisualMedia(MAX_NUM_OF_IMAGES),
+                    uris -> {
+                      if (!uris.isEmpty()) {
+                        Log.d("PhotoPicker", "Selected URIs: " + uris);
+                        mAddMediaLayout.setVisibility(View.GONE);
+                        for (Uri uri : uris) {
+                          MainActivity.this
+                                  .getContentResolver()
+                                  .takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
+                        }
+                        showMediaPreview(uris);
+                      } else {
+                        Log.d("PhotoPicker", "No media selected");
+                      }
+                    });
+
+    mMediaPreviewConstraintLayout = requireViewById(R.id.mediaPreviewConstraintLayout);
+    ImageButton mediaPreviewCloseButton = requireViewById(R.id.mediaPreviewCloseButton);
+    mediaPreviewCloseButton.setOnClickListener(
+            view -> {
+              mMediaPreviewConstraintLayout.setVisibility(View.GONE);
+              mSelectedImageUri = null;
+            });
+
+    ImageButton addMoreImageButton = requireViewById(R.id.addMoreImageButton);
+    addMoreImageButton.setOnClickListener(
+            view -> {
+              Log.d("addMore", "clicked");
+              mMediaPreviewConstraintLayout.setVisibility(View.GONE);
+              mGalleryButton.callOnClick();
+            });
+  }
+
+  private void showMediaPreview(List<Uri> uris) {
+    if (mSelectedImageUri == null) {
+      mSelectedImageUri = uris;
+    } else {
+      mSelectedImageUri.addAll(uris);
+    }
+
+    if (mSelectedImageUri.size() > MAX_NUM_OF_IMAGES) {
+      mSelectedImageUri = mSelectedImageUri.subList(0, MAX_NUM_OF_IMAGES);
+      Toast.makeText(
+                      this, "Only max " + MAX_NUM_OF_IMAGES + " images are allowed", Toast.LENGTH_SHORT)
+              .show();
+    }
+    Log.d("mSelectedImageUri", mSelectedImageUri.size() + " " + mSelectedImageUri);
+
+    mMediaPreviewConstraintLayout.setVisibility(View.VISIBLE);
+
+    List<ImageView> imageViews = new ArrayList<ImageView>();
+
+    // Pre-populate all the image views that are available from the layout (currently max 5)
+    imageViews.add(requireViewById(R.id.mediaPreviewImageView1));
+    imageViews.add(requireViewById(R.id.mediaPreviewImageView2));
+    imageViews.add(requireViewById(R.id.mediaPreviewImageView3));
+    imageViews.add(requireViewById(R.id.mediaPreviewImageView4));
+    imageViews.add(requireViewById(R.id.mediaPreviewImageView5));
+
+    // Hide all the image views (reset state)
+    for (int i = 0; i < imageViews.size(); i++) {
+      imageViews.get(i).setVisibility(View.GONE);
+    }
+
+    // Only show/render those that have proper Image URIs
+    for (int i = 0; i < mSelectedImageUri.size(); i++) {
+      imageViews.get(i).setVisibility(View.VISIBLE);
+      imageViews.get(i).setImageURI(mSelectedImageUri.get(i));
+    }
+  }
+
+  private void addSelectedImagesToChatThread(List<Uri> selectedImageUri) {
+    if (selectedImageUri == null) {
+      return;
+    }
+    mMediaPreviewConstraintLayout.setVisibility(View.GONE);
+    for (int i = 0; i < selectedImageUri.size(); i++) {
+      Uri imageURI = selectedImageUri.get(i);
+      Log.d("image uri ", "test " + imageURI.getPath());
+      mMessageAdapter.add(new Message(imageURI.toString(), true, MessageType.IMAGE, 0));
+    }
+    mMessageAdapter.notifyDataSetChanged();
+  }
+
+  private void onModelRunStopped() {
+    mSendButton.setClickable(true);
+    mSendButton.setImageResource(R.drawable.baseline_send_24);
+    mSendButton.setOnClickListener(
+            view -> {
+              try {
+                InputMethodManager imm = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE);
+                imm.hideSoftInputFromWindow(Objects.requireNonNull(getCurrentFocus()).getWindowToken(), 0);
+              } catch (Exception e) {
+                AppLogging.getInstance().log("Keyboard dismissal error: " + e.getMessage());
+              }
+              addSelectedImagesToChatThread(mSelectedImageUri);
+              String rawPrompt = mEditTextMessage.getText().toString();
+              mMessageAdapter.add(new Message(rawPrompt, true, MessageType.TEXT, promptID));
+              mMessageAdapter.notifyDataSetChanged();
+              mEditTextMessage.setText("");
+              mResultMessage = new Message("", false, MessageType.TEXT, promptID);
+              mMessageAdapter.add(mResultMessage);
+              // Scroll to bottom of the list
+              mMessagesView.smoothScrollToPosition(mMessageAdapter.getCount() - 1);
+              // After images are added to prompt and chat thread, we clear the imageURI list
+              // Note: This has to be done after imageURIs are no longer needed by LlamaModule
+              mSelectedImageUri = null;
+              promptID++;
+              Runnable runnable =
+                      new Runnable() {
+                        @Override
+                        public void run() {
+                          Process.setThreadPriority(Process.THREAD_PRIORITY_MORE_FAVORABLE);
+                          long generateStartTime = System.currentTimeMillis();
+                          remoteLlamaGeneration(rawPrompt);
+
+                          long generateDuration = System.currentTimeMillis() - generateStartTime;
+                          mResultMessage.setTotalGenerationTime(generateDuration);
+                          runOnUiThread(
+                                  new Runnable() {
+                                    @Override
+                                    public void run() {
+                                      onModelRunStopped();
+                                    }
+                                  });
+                          AppLogging.getInstance().log("Inference completed");
+                        }
+                      };
+              executor.execute(runnable);
+            });
+    mMessageAdapter.notifyDataSetChanged();
+  }
+
+  public void remoteLlamaGeneration(String rawPrompt) {
+    AppLogging.getInstance().log("Running inference remotely ("+ mCurrentSettingsFields.getRemoteModel() +").. raw prompt=" + rawPrompt);
+    String systemPrompt = mCurrentSettingsFields.getSystemPrompt();
+    String modelName = mCurrentSettingsFields.getRemoteModel();
+    double temperature = mCurrentSettingsFields.getTemperature();
+
+    String result = "";
+
+    // Get user selected language, and add that to system prompt
+    systemPrompt += " Summarize and translate it into the following language: " + languageSelector.getSelectedLanguage();
+    Log.d("updated system prompt with language selected",systemPrompt);
+
+    result = exampleLlamaRemoteInference.inferenceStartWithoutAgent(
+              modelName,
+              temperature,
+              mMessageAdapter.getRecentSavedTextMessages(AppUtils.CONVERSATION_HISTORY_MESSAGE_LOOKBACK),
+              systemPrompt,
+              this
+    );
+    mResultMessage.appendText(result);
+  }
+
+  @Override
+  public void run() {
+    runOnUiThread(
+            new Runnable() {
+              @Override
+              public void run() {
+                mMessageAdapter.notifyDataSetChanged();
+              }
+            });
+  }
+
+  @Override
+  public void onBackPressed() {
+    super.onBackPressed();
+    if (mAddMediaLayout != null && mAddMediaLayout.getVisibility() == View.VISIBLE) {
+      mAddMediaLayout.setVisibility(View.GONE);
+    } else {
+      // Default behavior of back button
+      finish();
+    }
+  }
+
+  @Override
+  protected void onDestroy() {
+    super.onDestroy();
+    mMemoryUpdateHandler.removeCallbacks(memoryUpdater);
+    // This is to cover the case where the app is shutdown when user is on MainActivity but
+    // never clicked on the logsActivity
+    AppLogging.getInstance().saveLogs();
+  }
+
+  @Override
+  public void onStreamReceived(@NonNull String message) {
+    AppLogging.getInstance().log("this is stream received: " + message);
+    runOnUiThread(
+            () -> {
+              mResultMessage.appendText(message);
+              mMessageAdapter.notifyDataSetChanged();
+            });
+  }
+
+  @Override
+  public void onStatStreamReceived(float tps) {
+    AppLogging.getInstance().log("this is stats received: " + tps);
+    runOnUiThread(
+            () -> {
+              mResultMessage.setTokensPerSecond(tps);
+              mMessageAdapter.notifyDataSetChanged();
+            });
+  }
+}

+ 98 - 0
getting-started/demo/ArticleSummarizer/app/src/main/java/com/example/llamaandroiddemo/Message.java

@@ -0,0 +1,98 @@
+/*
+ * 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.llamaandroiddemo;
+
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+
+public class Message {
+  private String text;
+  private final boolean isSent;
+  private float tokensPerSecond;
+  private long totalGenerationTime;
+  private final long timestamp;
+  private final MessageType messageType;
+  private String imagePath;
+  private final int promptID;
+
+  private static final String TIMESTAMP_FORMAT = "hh:mm a"; // example: 2:23 PM
+
+  public Message(String text, boolean isSent, MessageType messageType, int promptID) {
+    this.isSent = isSent;
+    this.messageType = messageType;
+    this.promptID = promptID;
+
+    if (messageType == MessageType.IMAGE) {
+      this.imagePath = text;
+    } else {
+      this.text = text;
+    }
+
+    if (messageType != MessageType.SYSTEM) {
+      this.timestamp = System.currentTimeMillis();
+    } else {
+      this.timestamp = (long) 0;
+    }
+  }
+
+  public int getPromptID() {
+    return promptID;
+  }
+
+  public MessageType getMessageType() {
+    return messageType;
+  }
+
+  public String getImagePath() {
+    return imagePath;
+  }
+
+  public String getText() {
+    return text;
+  }
+
+  public void appendText(String text) {
+    this.text += text;
+  }
+
+  public void setText(String text) {
+    this.text = text;
+  }
+
+  public boolean getIsSent() {
+    return isSent;
+  }
+
+  public void setTokensPerSecond(float tokensPerSecond) {
+    this.tokensPerSecond = tokensPerSecond;
+  }
+
+  public void setTotalGenerationTime(long totalGenerationTime) {
+    this.totalGenerationTime = totalGenerationTime;
+  }
+
+  public float getTokensPerSecond() {
+    return tokensPerSecond;
+  }
+
+  public long getTotalGenerationTime() {
+    return totalGenerationTime;
+  }
+
+  public long getTimestamp() {
+    return timestamp;
+  }
+
+  public String getFormattedTimestamp() {
+    SimpleDateFormat formatter = new SimpleDateFormat(TIMESTAMP_FORMAT, Locale.getDefault());
+    Date date = new Date(timestamp);
+    return formatter.format(date);
+  }
+}

+ 147 - 0
getting-started/demo/ArticleSummarizer/app/src/main/java/com/example/llamaandroiddemo/MessageAdapter.java

@@ -0,0 +1,147 @@
+/*
+ * 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.llamaandroiddemo;
+
+import android.net.Uri;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import java.util.ArrayList;
+import java.util.Collections;
+
+import io.noties.markwon.Markwon;
+
+public class MessageAdapter extends ArrayAdapter<Message> {
+
+  private final ArrayList<Message> savedMessages;
+
+  public MessageAdapter(
+      android.content.Context context, int resource, ArrayList<Message> savedMessages) {
+    super(context, resource);
+    this.savedMessages = savedMessages;
+  }
+
+  @Override
+  public View getView(int position, View convertView, ViewGroup parent) {
+    Message currentMessage = getItem(position);
+    int layoutIdForListItem;
+
+    if (currentMessage.getMessageType() == MessageType.SYSTEM) {
+      layoutIdForListItem = R.layout.system_message;
+    } else {
+      layoutIdForListItem =
+          currentMessage.getIsSent() ? R.layout.sent_message : R.layout.received_message;
+    }
+    View listItemView =
+        LayoutInflater.from(getContext()).inflate(layoutIdForListItem, parent, false);
+    if (currentMessage.getMessageType() == MessageType.IMAGE) {
+      ImageView messageImageView = listItemView.requireViewById(R.id.message_image);
+      messageImageView.setImageURI(Uri.parse(currentMessage.getImagePath()));
+      TextView messageTextView = listItemView.requireViewById(R.id.message_text);
+      messageTextView.setVisibility(View.GONE);
+    } else {
+      TextView messageTextView = listItemView.requireViewById(R.id.message_text);
+      String markdownString = currentMessage.getText();
+      Markwon markwon = Markwon.create(this.getContext());
+      markwon.setMarkdown(messageTextView, markdownString);
+    }
+
+    String metrics = "";
+    TextView tokensView;
+    if (currentMessage.getTokensPerSecond() > 0) {
+      metrics = String.format("%.2f", currentMessage.getTokensPerSecond()) + "t/s  ";
+    }
+
+    if (currentMessage.getTotalGenerationTime() > 0) {
+      metrics = metrics + (float) currentMessage.getTotalGenerationTime() / 1000 + "s  ";
+    }
+
+    if (currentMessage.getTokensPerSecond() > 0 || currentMessage.getTotalGenerationTime() > 0) {
+      tokensView = listItemView.requireViewById(R.id.generation_metrics);
+      tokensView.setText(metrics);
+      TextView separatorView = listItemView.requireViewById(R.id.bar);
+      separatorView.setVisibility(View.VISIBLE);
+    }
+
+    if (currentMessage.getTimestamp() > 0) {
+      TextView timestampView = listItemView.requireViewById(R.id.timestamp);
+      timestampView.setText(currentMessage.getFormattedTimestamp());
+    }
+
+    return listItemView;
+  }
+
+  @Override
+  public void add(Message msg) {
+    super.add(msg);
+    savedMessages.add(msg);
+  }
+
+  @Override
+  public void clear() {
+    super.clear();
+    savedMessages.clear();
+  }
+
+  public ArrayList<Message> getSavedMessages() {
+    return savedMessages;
+  }
+
+  public ArrayList<Message> getRecentSavedTextMessages(int numOfLatestPromptMessages) {
+    ArrayList<Message> recentMessages = new ArrayList<Message>();
+
+    // We don't want the "last" empty prompt response message that is yet to generate.
+    // -1 to get last index and another -1 to exclude the above. Therefore -2.
+    int lastIndex = savedMessages.size() - 2;
+
+    // In most cases lastIndex >=0 .
+    // A situation where the user clears chat history and enters prompt. Causes lastIndex=-1 .
+    if (lastIndex >= 0) {
+      Message messageToAdd = savedMessages.get(lastIndex);
+      int oldPromptID = messageToAdd.getPromptID();
+
+      for (int i = 0; i < savedMessages.size() - 1; i++) {
+        messageToAdd = savedMessages.get(lastIndex - i);
+        if (messageToAdd.getMessageType() != MessageType.SYSTEM) {
+          if (messageToAdd.getPromptID() != oldPromptID) {
+            numOfLatestPromptMessages--;
+            oldPromptID = messageToAdd.getPromptID();
+          }
+          if (numOfLatestPromptMessages >= 0) {
+            if (messageToAdd.getMessageType() == MessageType.TEXT && !messageToAdd.getText().isEmpty()) {
+              recentMessages.add(messageToAdd);
+            }
+            if (messageToAdd.getMessageType() == MessageType.IMAGE && !messageToAdd.getImagePath().isEmpty()) {
+              recentMessages.add(messageToAdd);
+            }
+          } else {
+            break;
+          }
+        }
+      }
+      // To place the order in [input1, output1, input2, output2...]
+      Collections.reverse(recentMessages);
+    }
+
+    return recentMessages;
+  }
+
+  public int getMaxPromptID() {
+    int maxPromptID = -1;
+    for (Message msg : savedMessages) {
+
+      maxPromptID = Math.max(msg.getPromptID(), maxPromptID);
+    }
+    return maxPromptID;
+  }
+}

+ 15 - 0
getting-started/demo/ArticleSummarizer/app/src/main/java/com/example/llamaandroiddemo/MessageType.java

@@ -0,0 +1,15 @@
+/*
+ * 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.llamaandroiddemo;
+
+public enum MessageType {
+  TEXT,
+  IMAGE,
+  SYSTEM
+}

+ 16 - 0
getting-started/demo/ArticleSummarizer/app/src/main/java/com/example/llamaandroiddemo/ModelType.java

@@ -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.llamaandroiddemo;
+
+public enum ModelType {
+  LLAMA_3,
+  LLAMA_3_1,
+  LLAMA_3_2,
+  LLAMA_GUARD_3,
+}

+ 23 - 0
getting-started/demo/ArticleSummarizer/app/src/main/java/com/example/llamaandroiddemo/ModelUtils.java

@@ -0,0 +1,23 @@
+/*
+ * 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.llamaandroiddemo;
+
+import java.util.Arrays;
+import java.util.List;
+
+public class ModelUtils {
+  public static List<String> getSupportedRemoteModels() {
+      // UPDATE THIS TO THE RELEVANT MODELS YOU WANT TO USE
+      // NOTE THAT SOME PROVIDERS MIGHT HAVE DIFFERENT MODEL NAMING FORMAT
+    return Arrays.asList(
+            "meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8",
+            "meta-llama/Llama-4-Scout-17B-16E-Instruct"
+            );
+  }
+}

+ 14 - 0
getting-started/demo/ArticleSummarizer/app/src/main/java/com/example/llamaandroiddemo/PromptFormat.java

@@ -0,0 +1,14 @@
+/*
+ * 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.llamaandroiddemo;
+
+public class PromptFormat {
+  public static final String DEFAULT_SYSTEM_PROMPT = "";
+
+}

+ 246 - 0
getting-started/demo/ArticleSummarizer/app/src/main/java/com/example/llamaandroiddemo/SettingsActivity.java

@@ -0,0 +1,246 @@
+/*
+ * 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.llamaandroiddemo;
+
+import android.app.AlertDialog;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.ImageButton;
+import android.widget.TextView;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.core.content.ContextCompat;
+import androidx.core.graphics.Insets;
+import androidx.core.view.ViewCompat;
+import androidx.core.view.WindowInsetsCompat;
+import com.google.gson.Gson;
+import java.io.File;
+
+public class SettingsActivity extends AppCompatActivity {
+
+  private EditText mSystemPromptEditText;
+  private double mSetTemperature;
+  private String mSystemPrompt;
+  private EditText mRemoteURLEditText;
+  private String mRemoteURL;
+  public SettingsFields mSettingsFields;
+  private String mRemoteModel;
+  private TextView mRemoteModelTextView;
+
+  private DemoSharedPreferences mDemoSharedPreferences;
+  public static double TEMPERATURE_MIN_VALUE = 0.0;
+
+  @Override
+  protected void onCreate(Bundle savedInstanceState) {
+    super.onCreate(savedInstanceState);
+    setContentView(R.layout.activity_settings);
+	  getWindow().setStatusBarColor(ContextCompat.getColor(this, R.color.status_bar));
+	  getWindow().setNavigationBarColor(ContextCompat.getColor(this, R.color.nav_bar));
+	  ViewCompat.setOnApplyWindowInsetsListener(
+        requireViewById(R.id.main),
+        (v, insets) -> {
+          Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
+          v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
+          return insets;
+        });
+    mDemoSharedPreferences = new DemoSharedPreferences(getBaseContext());
+    mSettingsFields = new SettingsFields();
+    setupSettings();
+  }
+
+  private void setupSettings() {
+    mSystemPromptEditText = requireViewById(R.id.systemPromptText);
+    mRemoteURLEditText = requireViewById(R.id.remoteURLEditText);
+
+    loadSettings();
+
+    setupParameterSettings();
+    setupPromptSettings();
+    setupClearChatHistoryButton();
+    setupRemoteInferenceSettings();
+  }
+  private void setupClearChatHistoryButton() {
+    Button clearChatButton = requireViewById(R.id.clearChatButton);
+    clearChatButton.setOnClickListener(
+        view -> {
+          new AlertDialog.Builder(this)
+              .setTitle("Delete Chat History")
+              .setMessage("Do you really want to delete chat history?")
+              .setIcon(android.R.drawable.ic_dialog_alert)
+              .setPositiveButton(
+                  android.R.string.yes,
+                  new DialogInterface.OnClickListener() {
+                    public void onClick(DialogInterface dialog, int whichButton) {
+                      mSettingsFields.saveIsClearChatHistory(true);
+                    }
+                  })
+              .setNegativeButton(android.R.string.no, null)
+              .show();
+        });
+  }
+
+  private void setupParameterSettings() {
+    setupTemperatureSettings();
+  }
+
+  private void setupTemperatureSettings() {
+    mSetTemperature = mSettingsFields.getTemperature();
+    EditText temperatureEditText = requireViewById(R.id.temperatureEditText);
+    temperatureEditText.setText(String.valueOf(mSetTemperature));
+    temperatureEditText.addTextChangedListener(
+        new TextWatcher() {
+          @Override
+          public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
+
+          @Override
+          public void onTextChanged(CharSequence s, int start, int before, int count) {}
+
+          @Override
+          public void afterTextChanged(Editable s) {
+            mSetTemperature = Double.parseDouble(s.toString());
+            // This is needed because temperature is changed together with model loading
+            // Once temperature is no longer in LlamaModule constructor, we can remove this
+            mSettingsFields.saveLoadModelAction(true);
+            saveSettings();
+          }
+        });
+  }
+
+  private void setupRemoteInferenceSettings() {
+    mRemoteURL = mSettingsFields.getRemoteURL();
+    AppLogging.getInstance().log("mRemoteURL from settings " + mRemoteURL);
+    if (mRemoteURL != null) {
+      mRemoteURLEditText.setText(mRemoteURL);
+    }
+    mRemoteURLEditText.addTextChangedListener(
+            new TextWatcher() {
+              @Override
+              public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+              }
+
+              @Override
+              public void onTextChanged(CharSequence s, int start, int before, int count) {
+              }
+
+              @Override
+              public void afterTextChanged(Editable s) {
+                mRemoteURL = s.toString();
+                AppLogging.getInstance().log("after text change remote url" + mRemoteURL);
+                mSettingsFields.saveRemoteURL(mRemoteURL);
+                saveSettings();
+              }
+            }
+    );
+
+    mRemoteModelTextView = requireViewById(R.id.remoteModelTextView);
+    ImageButton mRemoteModelImageButton = requireViewById(R.id.remoteModelImageButton);
+
+    mRemoteModel = mSettingsFields.getRemoteModel();
+    AppLogging.getInstance().log("mRemoteModel from settings " + mRemoteModel);
+    if (mRemoteModel != null) {
+      mRemoteModelTextView.setText(mRemoteModel);
+    }
+
+    mRemoteModelImageButton.setOnClickListener(
+            view -> {
+              String[] models = ModelUtils.getSupportedRemoteModels().toArray(new String[0]);
+              AlertDialog.Builder modelBuilder = new AlertDialog.Builder(this);
+              modelBuilder.setTitle("Select remote model");
+              modelBuilder.setSingleChoiceItems(
+                      models,
+                      -1,
+                      (dialog, item) -> {
+                        mRemoteModelTextView.setText(models[item]);
+                        mRemoteModel = models[item];
+                        dialog.dismiss();
+                      });
+              modelBuilder.create().show();
+            });
+  }
+
+  private void setupPromptSettings() {
+    setupSystemPromptSettings();
+  }
+
+  private void setupSystemPromptSettings() {
+    mSystemPrompt = mSettingsFields.getSystemPrompt();
+    mSystemPromptEditText.setText(mSystemPrompt);
+    mSystemPromptEditText.addTextChangedListener(
+        new TextWatcher() {
+          @Override
+          public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
+
+          @Override
+          public void onTextChanged(CharSequence s, int start, int before, int count) {}
+
+          @Override
+          public void afterTextChanged(Editable s) {
+            mSystemPrompt = s.toString();
+          }
+        });
+
+    ImageButton resetSystemPrompt = requireViewById(R.id.resetSystemPrompt);
+    resetSystemPrompt.setOnClickListener(
+        view -> {
+          new AlertDialog.Builder(this)
+              .setTitle("Reset System Prompt")
+              .setMessage("Do you really want to reset system prompt?")
+              .setIcon(android.R.drawable.ic_dialog_alert)
+              .setPositiveButton(
+                  android.R.string.yes,
+                  new DialogInterface.OnClickListener() {
+                    public void onClick(DialogInterface dialog, int whichButton) {
+                      // Clear the messageAdapter and sharedPreference
+                      mSystemPromptEditText.setText(PromptFormat.DEFAULT_SYSTEM_PROMPT);
+                    }
+                  })
+              .setNegativeButton(android.R.string.no, null)
+              .show();
+        });
+  }
+  private static String[] listLocalFile(String path, String suffix) {
+    File directory = new File(path);
+    if (directory.exists() && directory.isDirectory()) {
+      File[] files = directory.listFiles((dir, name) -> name.toLowerCase().endsWith(suffix));
+      String[] result = new String[files.length];
+      for (int i = 0; i < files.length; i++) {
+        if (files[i].isFile() && files[i].getName().endsWith(suffix)) {
+          result[i] = files[i].getAbsolutePath();
+        }
+      }
+      return result;
+    }
+    return new String[] {};
+  }
+  private void loadSettings() {
+    Gson gson = new Gson();
+    String settingsFieldsJSON = mDemoSharedPreferences.getSettings();
+    if (!settingsFieldsJSON.isEmpty()) {
+      AppLogging.getInstance().log("mSettingsFields " + settingsFieldsJSON);
+      mSettingsFields = gson.fromJson(settingsFieldsJSON, SettingsFields.class);
+    }
+  }
+
+  private void saveSettings() {
+    mSettingsFields.saveParameters(mSetTemperature);
+    mSettingsFields.savePrompts(mSystemPrompt);
+    mSettingsFields.saveRemoteURL(mRemoteURL);
+    mSettingsFields.saveRemoteModel(mRemoteModel);
+    mDemoSharedPreferences.addSettings(mSettingsFields);
+  }
+
+  @Override
+  public void onBackPressed() {
+    super.onBackPressed();
+    saveSettings();
+  }
+}

+ 146 - 0
getting-started/demo/ArticleSummarizer/app/src/main/java/com/example/llamaandroiddemo/SettingsFields.java

@@ -0,0 +1,146 @@
+/*
+ * 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.llamaandroiddemo;
+
+public class SettingsFields {
+
+  public String getModelFilePath() {
+    return modelFilePath;
+  }
+
+  public String getTokenizerFilePath() {
+    return tokenizerFilePath;
+  }
+
+  public double getTemperature() {
+    return temperature;
+  }
+
+  public String getSystemPrompt() {
+    return systemPrompt;
+  }
+
+  public ModelType getModelType() {
+    return modelType;
+  }
+
+  public BackendType getBackendType() {
+    return backendType;
+  }
+
+  public String getRemoteURL() {
+    return remoteURL;
+  }
+
+  public boolean getIsClearChatHistory() {
+    return isClearChatHistory;
+  }
+
+  public boolean getIsLoadModel() {
+    return isLoadModel;
+  }
+
+  public String getRemoteModel() {
+    return remoteModel;
+  }
+
+  private String modelFilePath;
+  private String tokenizerFilePath;
+  private double temperature;
+  private String systemPrompt;
+  private boolean isClearChatHistory;
+  private boolean isLoadModel;
+  private ModelType modelType;
+  private BackendType backendType;
+  private String remoteURL;
+  private String remoteModel;
+
+  public SettingsFields() {
+    ModelType DEFAULT_MODEL = ModelType.LLAMA_3;
+    BackendType DEFAULT_BACKEND = BackendType.XNNPACK;
+
+    modelFilePath = "";
+    tokenizerFilePath = "";
+    temperature = SettingsActivity.TEMPERATURE_MIN_VALUE;
+    systemPrompt = "";
+    isClearChatHistory = false;
+    isLoadModel = false;
+    modelType = DEFAULT_MODEL;
+    backendType = DEFAULT_BACKEND;
+    remoteURL = "";
+    remoteModel = "";
+  }
+
+  public SettingsFields(SettingsFields settingsFields) {
+    this.modelFilePath = settingsFields.modelFilePath;
+    this.tokenizerFilePath = settingsFields.tokenizerFilePath;
+    this.temperature = settingsFields.temperature;
+    this.systemPrompt = settingsFields.getSystemPrompt();
+    this.isClearChatHistory = settingsFields.getIsClearChatHistory();
+    this.isLoadModel = settingsFields.getIsLoadModel();
+    this.modelType = settingsFields.modelType;
+    this.backendType = settingsFields.backendType;
+    this.remoteURL = settingsFields.remoteURL;
+    this.remoteModel = settingsFields.remoteModel;
+  }
+
+  public void saveModelPath(String modelFilePath) {
+    this.modelFilePath = modelFilePath;
+  }
+
+  public void saveTokenizerPath(String tokenizerFilePath) {
+    this.tokenizerFilePath = tokenizerFilePath;
+  }
+
+  public void saveModelType(ModelType modelType) {
+    this.modelType = modelType;
+  }
+
+  public void saveBackendType(BackendType backendType) {
+    this.backendType = backendType;
+  }
+
+  public void saveParameters(Double temperature) {
+    this.temperature = temperature;
+  }
+
+  public void savePrompts(String systemPrompt) {
+    this.systemPrompt = systemPrompt;
+  }
+
+  public void saveIsClearChatHistory(boolean needToClear) {
+    this.isClearChatHistory = needToClear;
+  }
+
+  public void saveLoadModelAction(boolean shouldLoadModel) {
+    this.isLoadModel = shouldLoadModel;
+  }
+
+  public void saveRemoteURL(String url) {
+    this.remoteURL = url;
+  }
+
+  public void saveRemoteModel(String model) {
+    this.remoteModel = model;
+  }
+
+  public boolean equals(SettingsFields anotherSettingsFields) {
+    if (this == anotherSettingsFields) return true;
+    return modelFilePath.equals(anotherSettingsFields.modelFilePath)
+                   && tokenizerFilePath.equals(anotherSettingsFields.tokenizerFilePath)
+                   && temperature == anotherSettingsFields.temperature
+                   && systemPrompt.equals(anotherSettingsFields.systemPrompt)
+                   && isClearChatHistory == anotherSettingsFields.isClearChatHistory
+                   && isLoadModel == anotherSettingsFields.isLoadModel
+                   && modelType == anotherSettingsFields.modelType
+                   && backendType == anotherSettingsFields.backendType
+                   && remoteURL.equals(anotherSettingsFields.remoteURL)
+                   && remoteModel.equals(anotherSettingsFields.remoteModel);
+  }
+}

+ 5 - 0
getting-started/demo/ArticleSummarizer/app/src/main/res/drawable/banner_shape.xml

@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="rectangle">
+    <solid android:color="#16293D" />
+</shape>

+ 5 - 0
getting-started/demo/ArticleSummarizer/app/src/main/res/drawable/baseline_add_24.xml

@@ -0,0 +1,5 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
+      
+    <path android:fillColor="@android:color/white" android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
+    
+</vector>

+ 5 - 0
getting-started/demo/ArticleSummarizer/app/src/main/res/drawable/baseline_add_photo_alternate_24.xml

@@ -0,0 +1,5 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
+      
+    <path android:fillColor="@android:color/white" android:pathData="M19,7v2.99s-1.99,0.01 -2,0L17,7h-3s0.01,-1.99 0,-2h3L17,2h2v3h3v2h-3zM16,11L16,8h-3L13,5L5,5c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2v-8h-3zM5,19l3,-4 2,3 3,-4 4,5L5,19z"/>
+    
+</vector>

+ 6 - 0
getting-started/demo/ArticleSummarizer/app/src/main/res/drawable/baseline_article_24.xml

@@ -0,0 +1,6 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android" android:autoMirrored="true" android:height="24dp" android:tint="#FFFFFF
+" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
+      
+    <path android:fillColor="@android:color/white" android:pathData="M19,3L5,3c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2L21,5c0,-1.1 -0.9,-2 -2,-2zM14,17L7,17v-2h7v2zM17,13L7,13v-2h10v2zM17,9L7,9L7,7h10v2z"/>
+    
+</vector>

+ 6 - 0
getting-started/demo/ArticleSummarizer/app/src/main/res/drawable/baseline_close_24.xml

@@ -0,0 +1,6 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#FFFFFF
+" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
+      
+    <path android:fillColor="@android:color/white" android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z"/>
+    
+</vector>

+ 5 - 0
getting-started/demo/ArticleSummarizer/app/src/main/res/drawable/baseline_delete_forever_24.xml

@@ -0,0 +1,5 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#FFFFFF" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
+      
+    <path android:fillColor="@android:color/white" android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2L18,7L6,7v12zM8.46,11.88l1.41,-1.41L12,12.59l2.12,-2.12 1.41,1.41L13.41,14l2.12,2.12 -1.41,1.41L12,15.41l-2.12,2.12 -1.41,-1.41L10.59,14l-2.13,-2.12zM15.5,4l-1,-1h-5l-1,1L5,4v2h14L19,4z"/>
+    
+</vector>

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 5 - 0
getting-started/demo/ArticleSummarizer/app/src/main/res/drawable/baseline_language_24.xml


+ 6 - 0
getting-started/demo/ArticleSummarizer/app/src/main/res/drawable/baseline_restart_alt_24.xml

@@ -0,0 +1,6 @@
+<vector android:height="24dp" android:tint="#FFFFFF"
+    android:viewportHeight="24" android:viewportWidth="24"
+    android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
+    <path android:fillColor="@android:color/white" android:pathData="M12,5V2L8,6l4,4V7c3.31,0 6,2.69 6,6c0,2.97 -2.17,5.43 -5,5.91v2.02c3.95,-0.49 7,-3.85 7,-7.93C20,8.58 16.42,5 12,5z"/>
+    <path android:fillColor="@android:color/white" android:pathData="M6,13c0,-1.65 0.67,-3.15 1.76,-4.24L6.34,7.34C4.9,8.79 4,10.79 4,13c0,4.08 3.05,7.44 7,7.93v-2.02C8.17,18.43 6,15.97 6,13z"/>
+</vector>

+ 6 - 0
getting-started/demo/ArticleSummarizer/app/src/main/res/drawable/baseline_send_24.xml

@@ -0,0 +1,6 @@
+<vector android:autoMirrored="true" android:height="24dp"
+    android:tint="#FFFFFF
+" android:viewportHeight="24"
+    android:viewportWidth="24" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
+    <path android:fillColor="@android:color/white" android:pathData="M2.01,21L23,12 2.01,3 2,10l15,2 -15,2z"/>
+</vector>

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 11 - 0
getting-started/demo/ArticleSummarizer/app/src/main/res/drawable/baseline_settings_24.xml


+ 6 - 0
getting-started/demo/ArticleSummarizer/app/src/main/res/drawable/baseline_stop_24.xml

@@ -0,0 +1,6 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#FFFFFF
+" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
+      
+    <path android:fillColor="@android:color/white" android:pathData="M6,6h12v12H6z"/>
+    
+</vector>

+ 8 - 0
getting-started/demo/ArticleSummarizer/app/src/main/res/drawable/btn.xml

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <!-- Disable background -->
+    <item android:state_enabled="false"
+        android:color="@color/btn_disabled"/>
+    <!-- Enabled background -->
+    <item android:color="@color/btn_enabled"/>
+</selector>

+ 21 - 0
getting-started/demo/ArticleSummarizer/app/src/main/res/drawable/chat_background.xml

@@ -0,0 +1,21 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:aapt="http://schemas.android.com/aapt"
+    android:width="412dp"
+    android:height="893dp"
+    android:viewportWidth="412"
+    android:viewportHeight="893">
+  <path
+      android:pathData="M0,0h412v893h-412z">
+    <aapt:attr name="android:fillColor">
+      <gradient 
+          android:startX="206"
+          android:startY="0"
+          android:endX="206"
+          android:endY="893"
+          android:type="linear">
+        <item android:offset="0.05" android:color="#FF16293D"/>
+        <item android:offset="0.9" android:color="#FF192E4D"/>
+      </gradient>
+    </aapt:attr>
+  </path>
+</vector>

+ 7 - 0
getting-started/demo/ArticleSummarizer/app/src/main/res/drawable/custom_button_round.xml

@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
+    <solid android:color="#6080F0"/>
+    <corners android:radius="500dp"/>
+    <size android:width="100dp"
+        android:height="100dp"/>
+</shape>

+ 9 - 0
getting-started/demo/ArticleSummarizer/app/src/main/res/drawable/expand_circle_down.xml

@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="18dp"
+    android:viewportWidth="15"
+    android:viewportHeight="10">
+  <path
+      android:pathData="M15,2.373L7.5,10L0,2.373L2.375,0L7.5,5.212L12.625,0L15,2.373Z"
+      android:fillColor="#F4F4F4"/>
+</vector>

+ 170 - 0
getting-started/demo/ArticleSummarizer/app/src/main/res/drawable/ic_launcher_background.xml

@@ -0,0 +1,170 @@
+<?xml version="1.0" encoding="utf-8"?>
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="108dp"
+    android:height="108dp"
+    android:viewportWidth="108"
+    android:viewportHeight="108">
+    <path
+        android:fillColor="#3DDC84"
+        android:pathData="M0,0h108v108h-108z" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M9,0L9,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,0L19,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M29,0L29,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M39,0L39,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M49,0L49,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M59,0L59,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M69,0L69,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M79,0L79,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M89,0L89,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M99,0L99,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,9L108,9"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,19L108,19"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,29L108,29"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,39L108,39"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,49L108,49"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,59L108,59"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,69L108,69"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,79L108,79"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,89L108,89"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,99L108,99"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,29L89,29"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,39L89,39"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,49L89,49"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,59L89,59"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,69L89,69"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,79L89,79"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M29,19L29,89"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M39,19L39,89"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M49,19L49,89"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M59,19L59,89"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M69,19L69,89"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M79,19L79,89"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+</vector>

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 30 - 0
getting-started/demo/ArticleSummarizer/app/src/main/res/drawable/ic_launcher_foreground.xml


+ 7 - 0
getting-started/demo/ArticleSummarizer/app/src/main/res/drawable/input_text_shape.xml

@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="rectangle">
+    <solid android:color="#081D2C" />
+    <corners android:radius="20dp"/>
+    <padding android:layout_marginTop="5dp" android:layout_marginBottom="5dp" android:left="10dp" android:right="10dp"/>
+</shape>

BIN
getting-started/demo/ArticleSummarizer/app/src/main/res/drawable/logo.png


+ 6 - 0
getting-started/demo/ArticleSummarizer/app/src/main/res/drawable/outline_add_box_48.xml

@@ -0,0 +1,6 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="48dp" android:tint="#ffffff
+" android:viewportHeight="24" android:viewportWidth="24" android:width="48dp">
+      
+    <path android:fillColor="@android:color/white" android:pathData="M19,3L5,3c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2L21,5c0,-1.1 -0.9,-2 -2,-2zM19,19L5,19L5,5h14v14zM11,17h2v-4h4v-2h-4L13,7h-2v4L7,11v2h4z"/>
+    
+</vector>

+ 5 - 0
getting-started/demo/ArticleSummarizer/app/src/main/res/drawable/outline_camera_alt_48.xml

@@ -0,0 +1,5 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="48dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="48dp">
+      
+    <path android:fillColor="@android:color/white" android:pathData="M20,4h-3.17L15,2L9,2L7.17,4L4,4c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2L22,6c0,-1.1 -0.9,-2 -2,-2zM20,18L4,18L4,6h4.05l1.83,-2h4.24l1.83,2L20,6v12zM12,7c-2.76,0 -5,2.24 -5,5s2.24,5 5,5 5,-2.24 5,-5 -2.24,-5 -5,-5zM12,15c-1.65,0 -3,-1.35 -3,-3s1.35,-3 3,-3 3,1.35 3,3 -1.35,3 -3,3z"/>
+    
+</vector>

+ 5 - 0
getting-started/demo/ArticleSummarizer/app/src/main/res/drawable/outline_image_48.xml

@@ -0,0 +1,5 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="48dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="48dp">
+      
+    <path android:fillColor="@android:color/white" android:pathData="M19,5v14L5,19L5,5h14m0,-2L5,3c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2L21,5c0,-1.1 -0.9,-2 -2,-2zM14.14,11.86l-3,3.87L9,13.14 6,17h12l-3.86,-5.14z"/>
+    
+</vector>

+ 6 - 0
getting-started/demo/ArticleSummarizer/app/src/main/res/drawable/prompt_shape.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+android:shape="rectangle">
+<solid android:color="#081D2C" />
+<corners android:radius="4dp" />
+</shape>

+ 6 - 0
getting-started/demo/ArticleSummarizer/app/src/main/res/drawable/received_message.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="rectangle">
+    <solid android:color="#081D2C" />
+    <corners android:radius="10dp" />
+</shape>

+ 6 - 0
getting-started/demo/ArticleSummarizer/app/src/main/res/drawable/sent_message.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="rectangle">
+    <solid android:color="@color/colorPrimary" />
+    <corners android:radius="10dp" />
+</shape>

BIN
getting-started/demo/ArticleSummarizer/app/src/main/res/drawable/summarizer.png


+ 5 - 0
getting-started/demo/ArticleSummarizer/app/src/main/res/drawable/three_dots.xml

@@ -0,0 +1,5 @@
+<vector android:height="24dp" android:tint="#000000"
+    android:viewportHeight="24" android:viewportWidth="24"
+    android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
+    <path android:fillColor="@android:color/white" android:pathData="M6,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM18,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM12,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2z"/>
+</vector>

+ 16 - 0
getting-started/demo/ArticleSummarizer/app/src/main/res/layout/activity_benchmarking.xml

@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical"
+    android:clipToPadding="false"
+    android:focusableInTouchMode="true"
+    tools:context=".LlmBenchmarkRunner">
+
+    <TextView
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:id="@+id/log_view" />
+
+</LinearLayout>

+ 74 - 0
getting-started/demo/ArticleSummarizer/app/src/main/res/layout/activity_homescreen.xml

@@ -0,0 +1,74 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical"
+    android:padding="16dp"
+    tools:context=".MainActivity">
+
+    <ImageView
+        android:id="@+id/img_logo"
+        android:layout_width="250dp"
+        android:layout_height="250dp"
+        android:layout_gravity="center_horizontal"
+        android:src="@drawable/summarizer"
+        android:scaleType="centerInside"
+        android:layout_marginTop="30dp"
+        android:layout_marginBottom="30dp"/>
+
+    <TextView
+        android:id="@+id/tv_welcome"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text="Welcome to Article Summarizer!"
+        android:textSize="20sp"
+        android:textStyle="bold"
+        android:gravity="center"
+        android:layout_gravity="center_horizontal"
+        android:paddingBottom="12dp"/>
+
+    <TextView
+        android:id="@+id/tv_description"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:text="Send any image article to summarize and translate into any supported languages"
+        android:textAlignment="center"
+        android:paddingBottom="16dp"/>
+
+    <Button
+        android:id="@+id/btn_start_chat"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginVertical="70dp"
+        android:text="Start a Chat"
+        android:layout_marginTop="8dp"/>
+
+    <TextView
+        android:id="@+id/tv_recent_questions"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:text="Recent Questions"
+        android:textStyle="bold"
+        android:visibility="gone"
+        android:layout_marginTop="50dp"/>
+
+    <ListView
+        android:id="@+id/lv_recent_questions"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:visibility="gone"
+        android:divider="@android:color/darker_gray"
+        android:dividerHeight="1dp"/>
+
+    <TextView
+        android:id="@+id/tv_description3"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="50dp"
+        android:paddingBottom="16dp"
+        android:text="This app operates using Llama 4 models through any of the supported remote inference providers."
+        android:textAlignment="center" />
+
+</LinearLayout>

+ 55 - 0
getting-started/demo/ArticleSummarizer/app/src/main/res/layout/activity_logs.xml

@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:id="@+id/main"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    tools:context=".LogsActivity">
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:orientation="vertical">
+
+        <LinearLayout
+            android:id="@+id/top_banner"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:background="@drawable/banner_shape"
+            android:orientation="horizontal">
+
+            <TextView
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:paddingLeft="10dp"
+                android:paddingTop="20dp"
+                android:paddingBottom="7dp"
+                android:text="Logs"
+                android:textColor="@android:color/white"
+                android:textSize="20sp"
+                android:textStyle="bold" />
+            <View
+                android:layout_width="0dp"
+                android:layout_height="0dp"
+                android:layout_weight="1"
+                />
+            <ImageButton
+                android:id="@+id/clearLogsButton"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:paddingTop="20dp"
+                android:backgroundTint="@android:color/transparent"
+                android:src="@drawable/baseline_delete_forever_24"
+                />
+        </LinearLayout>
+
+        <ListView
+            android:id="@+id/logsListView"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent">
+
+        </ListView>
+    </LinearLayout>
+
+</androidx.constraintlayout.widget.ConstraintLayout>

+ 255 - 0
getting-started/demo/ArticleSummarizer/app/src/main/res/layout/activity_main.xml

@@ -0,0 +1,255 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="wrap_content"
+    android:layout_height="match_parent"
+    android:background="#DCD7D7"
+    android:clipToPadding="false"
+    android:focusableInTouchMode="true"
+    android:orientation="vertical"
+    tools:context=".MainActivity">
+
+    <LinearLayout
+        android:id="@+id/top_banner"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:background="@drawable/banner_shape"
+        android:orientation="horizontal">
+
+        <TextView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:paddingLeft="20dp"
+            android:paddingTop="20dp"
+            android:text="Llama"
+            android:layout_weight="1"
+            android:textColor="@android:color/white"
+            android:textSize="16sp"
+            android:textStyle="bold" />
+
+        <TextView
+            android:id="@+id/ram_usage_live"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_weight="1"
+            android:fontFamily="sans-serif-black"
+            android:paddingLeft="5dp"
+            android:text="0 MB"
+            android:textAlignment="viewEnd"
+            android:textColor="#FFFFFF"
+            android:visibility="gone"
+            android:textSize="14sp" />
+
+        <ImageButton
+            android:id="@+id/showLogsButton"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:backgroundTint="@android:color/transparent"
+            android:paddingTop="20dp"
+            android:visibility="gone"
+            android:src="@drawable/baseline_article_24" />
+
+        <ImageButton
+            android:id="@+id/settings"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_alignParentRight="true"
+            android:backgroundTint="@android:color/transparent"
+            android:paddingTop="20dp"
+            android:src="@drawable/baseline_settings_24" />
+
+        <TextView
+            android:id="@+id/generationMode"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_weight="1"
+            android:text="Remote"
+            android:textAlignment="center"
+            android:textColor="#FFFFFF"
+            android:visibility="gone"
+            android:textStyle="bold" />
+
+    </LinearLayout>
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:orientation="vertical">
+
+        <ListView
+            android:id="@+id/messages_view"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_weight="2"
+            android:background="@drawable/chat_background"
+            android:divider="#fff"
+            android:stackFromBottom="true"
+            android:transcriptMode="alwaysScroll" />
+
+        <androidx.constraintlayout.widget.ConstraintLayout
+            android:id="@+id/mediaPreviewConstraintLayout"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:background="#16293D"
+            android:visibility="gone">
+
+            <HorizontalScrollView
+                android:id="@+id/mediaPreviewScrollView"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:padding="5dp"
+                app:layout_constraintEnd_toStartOf="@id/mediaPreviewCloseButton"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintTop_toTopOf="parent">
+
+                <LinearLayout
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:orientation="horizontal">
+
+                    <ImageView
+                        android:id="@+id/mediaPreviewImageView1"
+                        android:layout_width="80dp"
+                        android:layout_height="80dp"
+                        android:visibility="gone"
+                        app:srcCompat="@drawable/ic_launcher_foreground" />
+
+                    <ImageView
+                        android:id="@+id/mediaPreviewImageView2"
+                        android:layout_width="80dp"
+                        android:layout_height="80dp"
+                        android:layout_marginStart="10dp"
+                        android:visibility="gone"
+                        app:srcCompat="@drawable/ic_launcher_foreground" />
+
+                    <ImageView
+                        android:id="@+id/mediaPreviewImageView3"
+                        android:layout_width="80dp"
+                        android:layout_height="80dp"
+                        android:layout_marginStart="10dp"
+                        android:visibility="gone"
+                        app:srcCompat="@drawable/ic_launcher_foreground" />
+
+                    <ImageView
+                        android:id="@+id/mediaPreviewImageView4"
+                        android:layout_width="80dp"
+                        android:layout_height="80dp"
+                        android:layout_marginStart="10dp"
+                        android:visibility="gone"
+                        app:srcCompat="@drawable/ic_launcher_foreground" />
+
+                    <ImageView
+                        android:id="@+id/mediaPreviewImageView5"
+                        android:layout_width="80dp"
+                        android:layout_height="80dp"
+                        android:layout_marginStart="10dp"
+                        android:visibility="gone"
+                        app:srcCompat="@drawable/ic_launcher_foreground" />
+
+                    <ImageButton
+                        android:id="@+id/addMoreImageButton"
+                        android:layout_width="80dp"
+                        android:layout_height="80dp"
+                        android:background="#16293D"
+                        android:padding="5dp"
+                        android:src="@drawable/outline_add_box_48" />
+
+
+                </LinearLayout>
+
+
+            </HorizontalScrollView>
+
+            <ImageButton
+                android:id="@+id/mediaPreviewCloseButton"
+                android:layout_width="24dp"
+                android:layout_height="24dp"
+                android:background="@android:color/transparent"
+                android:padding="5dp"
+                android:src="@drawable/baseline_close_24"
+                app:layout_constraintEnd_toEndOf="parent"
+                app:layout_constraintTop_toTopOf="parent" />
+
+
+        </androidx.constraintlayout.widget.ConstraintLayout>
+
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:background="#16293D"
+            android:orientation="horizontal">
+
+            <ImageButton
+                android:id="@+id/addMediaButton"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:background="@android:color/transparent"
+                android:padding="10dp"
+                android:src="@drawable/baseline_add_24" />
+
+            <ImageButton
+                android:id="@+id/addLanguageButton"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:background="@android:color/transparent"
+                android:padding="10dp"
+                android:src="@drawable/baseline_language_24" />
+
+            <EditText
+                android:id="@+id/editTextMessage"
+                android:layout_width="match_parent"
+                android:layout_height="35dp"
+                android:layout_weight="2"
+                android:background="@drawable/input_text_shape"
+                android:ems="8"
+                android:inputType="text"
+                android:paddingHorizontal="10dp"
+                android:text=""
+                android:textColor="#ffffff"
+                android:textColorHint="#ffffff"
+                android:translationY="5dp" />
+
+            <ImageButton
+                android:id="@+id/sendButton"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:background="@android:color/transparent"
+                android:padding="10dp"
+                android:src="@drawable/baseline_send_24" />
+        </LinearLayout>
+
+        <LinearLayout
+            android:id="@+id/addMediaLayout"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:background="#16293D"
+            android:orientation="vertical">
+
+            <LinearLayout
+                android:layout_width="wrap_content"
+                android:layout_height="match_parent"
+                android:layout_gravity="center"
+                android:orientation="horizontal"
+                android:paddingTop="20dp"
+                android:paddingBottom="20dp">
+
+                <ImageButton
+                    android:id="@+id/cameraButton"
+                    android:layout_width="80dp"
+                    android:layout_height="80dp"
+                    android:background="@drawable/custom_button_round"
+                    android:src="@drawable/outline_camera_alt_48" />
+
+                <ImageButton
+                    android:id="@+id/galleryButton"
+                    android:layout_width="80dp"
+                    android:layout_height="80dp"
+                    android:layout_marginStart="40dp"
+                    android:background="@drawable/custom_button_round"
+                    android:src="@drawable/outline_image_48" />
+            </LinearLayout>
+        </LinearLayout>
+
+    </LinearLayout>
+</LinearLayout>

+ 209 - 0
getting-started/demo/ArticleSummarizer/app/src/main/res/layout/activity_settings.xml

@@ -0,0 +1,209 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:id="@+id/main"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    tools:context=".SettingsActivity">
+    <ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:fillViewport="true">
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:background="#16293D"
+        android:orientation="vertical"
+        app:layout_constraintTop_toTopOf="parent"
+        tools:layout_editor_absoluteX="1dp">
+
+        <TextView
+            android:id="@+id/textView"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:fontFamily="sans-serif-medium"
+            android:text="Settings"
+            android:textAlignment="viewStart"
+            android:textColor="#FFFFFF"
+            android:textSize="22sp"
+            android:translationX="5dp"
+            android:translationY="5dp" />
+
+        <TextView
+            android:id="@+id/remoteInferenceView"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="20dp"
+            android:layout_marginBottom="20dp"
+            android:text="Remote Inference"
+            android:textColor="#FFFFFF"
+            android:textSize="18sp"
+            android:textStyle="bold"
+            android:translationX="5dp" />
+
+        <LinearLayout
+            android:id="@+id/remoteURLLayout"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginBottom="10dp"
+            android:orientation="horizontal">
+
+            <TextView
+                android:id="@+id/remoteURLtextView"
+                android:layout_width="150dp"
+                android:layout_height="wrap_content"
+                android:text="Remote URL"
+                android:textColor="#FFFFFF"
+                android:textSize="16sp"
+                android:translationX="5dp" />
+
+            <EditText
+                android:id="@+id/remoteURLEditText"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_weight="1"
+                android:ems="10"
+                android:textAlignment="textEnd"
+                android:textColor="#FFFFFF"
+                android:textColorHint="#FFFFFF"
+                android:textSize="16sp" />
+        </LinearLayout>
+
+        <LinearLayout
+            android:id="@+id/remoteModelLayout"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="10dp"
+            android:orientation="horizontal">
+
+            <TextView
+                android:id="@+id/remoteModelLabel"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:gravity="center_vertical"
+                android:text="Model"
+                android:textColor="#FFFFFF"
+                android:textSize="16sp"
+                android:translationX="5dp" />
+
+            <TextView
+                android:id="@+id/remoteModelTextView"
+                android:layout_width="0dp"
+                android:layout_height="match_parent"
+                android:layout_weight="1"
+                android:gravity="center_vertical|end"
+                android:text="no model selected"
+                android:textColor="#FFFFFF" />
+
+            <ImageButton
+                android:id="@+id/remoteModelImageButton"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginStart="5dp"
+                android:background="#00FFFFFF"
+                android:scaleX="0.7"
+                android:scaleY="0.7"
+                android:src="@drawable/expand_circle_down" />
+
+        </LinearLayout>
+        <TextView
+            android:id="@+id/parametersView"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginTop="20dp"
+            android:layout_marginBottom="20dp"
+            android:text="Parameters"
+            android:textColor="#FFFFFF"
+            android:textSize="18sp"
+            android:textStyle="bold"
+            android:translationX="5dp" />
+
+        <LinearLayout
+            android:id="@+id/temperatureLayout"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginBottom="10dp"
+            android:orientation="horizontal">
+
+            <TextView
+                android:id="@+id/textView5"
+                android:layout_width="150dp"
+                android:layout_height="wrap_content"
+                android:text="Temperature"
+                android:textColor="#FFFFFF"
+                android:textSize="16sp"
+                android:translationX="5dp" />
+
+            <EditText
+                android:id="@+id/temperatureEditText"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_weight="1"
+                android:ems="10"
+                android:inputType="numberDecimal"
+                android:text="0.1"
+                android:textAlignment="textEnd"
+                android:textColor="#FFFFFF"
+                android:textColorHint="#FFFFFF"
+                android:textSize="16sp" />
+        </LinearLayout>
+
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginBottom="10dp"
+            android:orientation="vertical">
+
+            <LinearLayout
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:orientation="horizontal">
+
+                <TextView
+                    android:id="@+id/systemPromptTitle"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_marginTop="20dp"
+                    android:layout_marginBottom="20dp"
+                    android:text="System Prompt"
+                    android:textColor="#FFFAFA"
+                    android:textSize="18sp"
+                    android:textStyle="bold"
+                    android:translationX="5dp" />
+
+                <ImageButton
+                    android:id="@+id/resetSystemPrompt"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_marginTop="10dp"
+                    android:backgroundTint="@android:color/transparent"
+                    android:src="@drawable/baseline_restart_alt_24" />
+            </LinearLayout>
+
+
+            <EditText
+                android:id="@+id/systemPromptText"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:height="60dp"
+                android:background="@drawable/prompt_shape"
+                android:hint="Type custom system prompt"
+                android:textColor="#FFFFFF"
+                android:textColorHint="#FFFCFC"
+                android:textSize="16sp" />
+        </LinearLayout>
+
+        <Button
+            android:id="@+id/clearChatButton"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center"
+            android:text="Clear Chat History"
+            android:textColor="@android:color/white"
+            android:theme="@style/DefaultButton" />
+
+    </LinearLayout>
+    </ScrollView>
+
+</androidx.constraintlayout.widget.ConstraintLayout>

+ 16 - 0
getting-started/demo/ArticleSummarizer/app/src/main/res/layout/logs_message.xml

@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:layout_marginTop="10dp">
+
+        <TextView
+            android:id="@+id/logsTextView"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:padding="8dp"
+            android:text="TextView" />
+
+</LinearLayout>

+ 70 - 0
getting-started/demo/ArticleSummarizer/app/src/main/res/layout/received_message.xml

@@ -0,0 +1,70 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:paddingVertical="10dp"
+    android:paddingLeft="15dp"
+    android:paddingRight="60dp"
+    android:clipToPadding="false">
+
+    <TextView
+        android:id="@+id/name"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginLeft="15dp"
+        android:paddingBottom="4dp"
+        android:text="Llama"
+        android:textColor="#FFFFFF" />
+
+    <TextView
+        android:id="@+id/message_text"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_below="@+id/name"
+        android:layout_alignLeft="@+id/name"
+        android:background="@drawable/received_message"
+        android:elevation="2dp"
+        android:paddingHorizontal="16dp"
+        android:paddingVertical="12dp"
+        android:text="Generated text"
+        android:textColor="#FFFFFF"
+        android:textSize="16sp" />
+
+    <LinearLayout
+        android:id="@+id/subtitles"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_below="@+id/message_text">
+
+        <TextView
+            android:id="@+id/timestamp"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginLeft="15dp"
+            android:paddingLeft="4dp"
+            android:paddingBottom="4dp"
+            android:text=""
+            android:textColor="#FFFFFF" />
+
+        <TextView
+            android:id="@+id/bar"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginLeft="15dp"
+            android:paddingLeft="4dp"
+            android:paddingBottom="4dp"
+            android:text="|"
+            android:textColor="#FFFFFF"
+            android:visibility="gone" />
+
+        <TextView
+            android:id="@+id/generation_metrics"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginLeft="15dp"
+            android:layout_toRightOf="@+id/bar"
+            android:paddingBottom="4dp"
+            android:text=""
+            android:textColor="#FDFDFD" />
+    </LinearLayout>
+</RelativeLayout>

+ 63 - 0
getting-started/demo/ArticleSummarizer/app/src/main/res/layout/sent_message.xml

@@ -0,0 +1,63 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:paddingVertical="10dp"
+    android:paddingRight="15dp"
+    android:paddingLeft="60dp"
+    android:clipToPadding="false">
+
+    <LinearLayout
+        android:id="@+id/message_content"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_alignParentRight="true"
+        android:orientation="vertical">
+
+        <TextView
+            android:id="@+id/name"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_above="@+id/message_text"
+            android:layout_alignParentTop="true"
+            android:layout_alignParentRight="true"
+            android:layout_marginRight="15dp"
+            android:paddingBottom="4dp"
+            android:textColor="#FFFFFF" />
+
+        <TextView
+            android:id="@+id/message_text"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_alignParentRight="true"
+            android:background="@drawable/sent_message"
+            android:elevation="2dp"
+            android:padding="10dp"
+            android:text="My prompt"
+            android:textColor="#fff"
+            android:textSize="16sp" />
+
+        <ImageView
+            android:id="@+id/message_image"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_alignParentEnd="true"
+            android:adjustViewBounds="true"
+            tools:srcCompat="@tools:sample/avatars" />
+
+    </LinearLayout>
+
+    <TextView
+        android:id="@+id/timestamp"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_below="@+id/message_content"
+        android:layout_alignParentRight="true"
+        android:layout_marginRight="10dp"
+        android:paddingBottom="4dp"
+        android:text=""
+        android:textColor="#FFFFFF" />
+
+</RelativeLayout>

+ 23 - 0
getting-started/demo/ArticleSummarizer/app/src/main/res/layout/system_message.xml

@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:paddingVertical="10dp"
+    android:paddingLeft="15dp"
+    android:paddingRight="60dp"
+    android:clipToPadding="false">
+
+    <TextView
+        android:id="@+id/message_text"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_centerHorizontal="true"
+        android:elevation="2dp"
+        android:paddingHorizontal="16dp"
+        android:paddingVertical="12dp"
+        android:text="Generated text"
+        android:textAlignment="center"
+        android:textColor="#9C9C9C"
+        android:textSize="15dp" />
+
+</RelativeLayout>

+ 6 - 0
getting-started/demo/ArticleSummarizer/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+    <background android:drawable="@drawable/ic_launcher_background" />
+    <foreground android:drawable="@drawable/ic_launcher_foreground" />
+    <monochrome android:drawable="@drawable/ic_launcher_foreground" />
+</adaptive-icon>

+ 6 - 0
getting-started/demo/ArticleSummarizer/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+    <background android:drawable="@drawable/ic_launcher_background" />
+    <foreground android:drawable="@drawable/ic_launcher_foreground" />
+    <monochrome android:drawable="@drawable/ic_launcher_foreground" />
+</adaptive-icon>

BIN
getting-started/demo/ArticleSummarizer/app/src/main/res/mipmap-hdpi/ic_launcher.webp


BIN
getting-started/demo/ArticleSummarizer/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp


BIN
getting-started/demo/ArticleSummarizer/app/src/main/res/mipmap-mdpi/ic_launcher.webp


BIN
getting-started/demo/ArticleSummarizer/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp


BIN
getting-started/demo/ArticleSummarizer/app/src/main/res/mipmap-xhdpi/ic_launcher.webp


BIN
getting-started/demo/ArticleSummarizer/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp


BIN
getting-started/demo/ArticleSummarizer/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp


BIN
getting-started/demo/ArticleSummarizer/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp


BIN
getting-started/demo/ArticleSummarizer/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp


BIN
getting-started/demo/ArticleSummarizer/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp


+ 10 - 0
getting-started/demo/ArticleSummarizer/app/src/main/res/values/colors.xml

@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <color name="colorPrimary">#4294F0</color>
+    <color name="colorPrimaryDark">#3700B3</color>
+    <color name="colorAccent">#03DAC5</color>
+    <color name="btn_enabled">#007CBA</color>
+    <color name="btn_disabled">#A2A4B6</color>
+    <color name="nav_bar">#16293D</color>
+    <color name="status_bar">#16293D</color>
+</resources>

+ 8 - 0
getting-started/demo/ArticleSummarizer/app/src/main/res/values/strings.xml

@@ -0,0 +1,8 @@
+<resources>
+    <string name="app_name">Llama Demo</string>
+    <string name="demo_pref_file_key">DemoPrefFileKey</string>
+    <string name="saved_messages_json_key">SavedMessagesJsonKey</string>
+    <string name="settings_json_key">SettingsJsonKey</string>
+    <string name="logs_json_key">LogsJsonKey</string>
+    <string name="start_chat_button_text">Start Chat</string>
+</resources>

+ 14 - 0
getting-started/demo/ArticleSummarizer/app/src/main/res/values/styles.xml

@@ -0,0 +1,14 @@
+<resources>
+    <!-- Base application theme. -->
+    <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
+        <!-- Customize your theme here. -->
+        <item name="colorPrimary">@color/colorPrimary</item>
+        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
+        <item name="colorAccent">@color/colorAccent</item>
+    </style>
+
+    <style name="DefaultButton" parent="Theme.AppCompat.Light.DarkActionBar">
+        <item name="colorButtonNormal">@drawable/btn</item>
+        <item name="android:textColor">@color/colorPrimaryDark</item>
+    </style>
+</resources>

+ 4 - 0
getting-started/demo/ArticleSummarizer/app/src/main/res/values/themes.xml

@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <style name="Theme.ExecuTorchLlamaDemo" parent="android:Theme.Light" />
+</resources>

+ 13 - 0
getting-started/demo/ArticleSummarizer/app/src/main/res/xml/backup_rules.xml

@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+   Sample backup rules file; uncomment and customize as necessary.
+   See https://developer.android.com/guide/topics/data/autobackup
+   for details.
+   Note: This file is ignored for devices older that API 31
+   See https://developer.android.com/about/versions/12/backup-restore
+-->
+<full-backup-content>
+    <!--
+   <include domain="sharedpref" path="."/>
+   <exclude domain="sharedpref" path="device.xml"/>
+-->
+</full-backup-content>

+ 19 - 0
getting-started/demo/ArticleSummarizer/app/src/main/res/xml/data_extraction_rules.xml

@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+   Sample data extraction rules file; uncomment and customize as necessary.
+   See https://developer.android.com/about/versions/12/backup-restore#xml-changes
+   for details.
+-->
+<data-extraction-rules>
+    <cloud-backup>
+        <!-- TODO: Use <include> and <exclude> to control what is backed up.
+        <include .../>
+        <exclude .../>
+        -->
+    </cloud-backup>
+    <!--
+    <device-transfer>
+        <include .../>
+        <exclude .../>
+    </device-transfer>
+    -->
+</data-extraction-rules>

+ 4 - 0
getting-started/demo/ArticleSummarizer/app/src/main/res/xml/file_paths.xml

@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<paths xmlns:android="http://schemas.android.com/apk/res/android">
+    <external-cache-path name="my_cache" path="." />
+</paths>

+ 13 - 0
getting-started/demo/ArticleSummarizer/build.gradle.kts

@@ -0,0 +1,13 @@
+/*
+ * 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.
+ */
+
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+plugins {
+  id("com.android.application") version "8.1.0" apply false
+  id("org.jetbrains.kotlin.android") version "1.8.10" apply false
+}

+ 23 - 0
getting-started/demo/ArticleSummarizer/gradle.properties

@@ -0,0 +1,23 @@
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
+# AndroidX package structure to make it clearer which packages are bundled with the
+# Android operating system, and which are packaged with your app's APK
+# https://developer.android.com/topic/libraries/support-library/androidx-rn
+android.useAndroidX=true
+# Kotlin code style for this project: "official" or "obsolete":
+kotlin.code.style=official
+# Enables namespacing of each library's R class so that its R class includes only the
+# resources declared in the library itself and none from the library's dependencies,
+# thereby reducing the size of the R class for that library
+android.nonTransitiveRClass=true

+ 6 - 0
getting-started/demo/ArticleSummarizer/gradle/wrapper/gradle-wrapper.properties

@@ -0,0 +1,6 @@
+#Mon Sep 25 11:23:11 PDT 2023
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists

+ 185 - 0
getting-started/demo/ArticleSummarizer/gradlew

@@ -0,0 +1,185 @@
+#!/usr/bin/env sh
+
+#
+# Copyright 2015 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+##############################################################################
+##
+##  Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+    ls=`ls -ld "$PRG"`
+    link=`expr "$ls" : '.*-> \(.*\)$'`
+    if expr "$link" : '/.*' > /dev/null; then
+        PRG="$link"
+    else
+        PRG=`dirname "$PRG"`"/$link"
+    fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+    echo "$*"
+}
+
+die () {
+    echo
+    echo "$*"
+    echo
+    exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+  CYGWIN* )
+    cygwin=true
+    ;;
+  Darwin* )
+    darwin=true
+    ;;
+  MINGW* )
+    msys=true
+    ;;
+  NONSTOP* )
+    nonstop=true
+    ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+    if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+        # IBM's JDK on AIX uses strange locations for the executables
+        JAVACMD="$JAVA_HOME/jre/sh/java"
+    else
+        JAVACMD="$JAVA_HOME/bin/java"
+    fi
+    if [ ! -x "$JAVACMD" ] ; then
+        die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+    fi
+else
+    JAVACMD="java"
+    which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+    MAX_FD_LIMIT=`ulimit -H -n`
+    if [ $? -eq 0 ] ; then
+        if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+            MAX_FD="$MAX_FD_LIMIT"
+        fi
+        ulimit -n $MAX_FD
+        if [ $? -ne 0 ] ; then
+            warn "Could not set maximum file descriptor limit: $MAX_FD"
+        fi
+    else
+        warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+    fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+    GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
+    APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+    CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+
+    JAVACMD=`cygpath --unix "$JAVACMD"`
+
+    # We build the pattern for arguments to be converted via cygpath
+    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+    SEP=""
+    for dir in $ROOTDIRSRAW ; do
+        ROOTDIRS="$ROOTDIRS$SEP$dir"
+        SEP="|"
+    done
+    OURCYGPATTERN="(^($ROOTDIRS))"
+    # Add a user-defined pattern to the cygpath arguments
+    if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+        OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+    fi
+    # Now convert the arguments - kludge to limit ourselves to /bin/sh
+    i=0
+    for arg in "$@" ; do
+        CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+        CHECK2=`echo "$arg"|egrep -c "^-"`                                 ### Determine if an option
+
+        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition
+            eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+        else
+            eval `echo args$i`="\"$arg\""
+        fi
+        i=`expr $i + 1`
+    done
+    case $i in
+        0) set -- ;;
+        1) set -- "$args0" ;;
+        2) set -- "$args0" "$args1" ;;
+        3) set -- "$args0" "$args1" "$args2" ;;
+        4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+        5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+        6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+        7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+        8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+        9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+    esac
+fi
+
+# Escape application args
+save () {
+    for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+    echo " "
+}
+APP_ARGS=`save "$@"`
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+exec "$JAVACMD" "$@"

+ 95 - 0
getting-started/demo/ArticleSummarizer/gradlew.bat

@@ -0,0 +1,95 @@
+@REM Copyright (c) Meta Platforms, Inc. and affiliates.
+@REM All rights reserved.
+@REM
+@REM This source code is licensed under the BSD-style license found in the
+@REM LICENSE file in the root directory of this source tree.
+
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem      https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem  Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if  not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega

BIN
getting-started/demo/ArticleSummarizer/screenshot.png


+ 27 - 0
getting-started/demo/ArticleSummarizer/settings.gradle.kts

@@ -0,0 +1,27 @@
+/*
+ * 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.
+ */
+
+pluginManagement {
+  repositories {
+    google()
+    mavenCentral()
+    gradlePluginPortal()
+  }
+}
+
+dependencyResolutionManagement {
+  repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
+  repositories {
+    google()
+    mavenCentral()
+  }
+}
+
+rootProject.name = "Llama Android Demo"
+
+include(":app")