diff --git a/.github/workflows/build-aar.yml b/.github/workflows/build-aar.yml new file mode 100644 index 00000000..1fcd3453 --- /dev/null +++ b/.github/workflows/build-aar.yml @@ -0,0 +1,104 @@ +name: Build Android AAR + +on: + push: + branches: + - main + - android-sdk + pull_request: + +jobs: + build: + name: Build AAR + runs-on: ubuntu-latest + if: github.event_name == 'push' + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + distribution: 'temurin' + java-version: '17' + + - name: Cache Gradle packages + uses: actions/cache@v3 + with: + path: ~/.gradle/caches + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle- + + - name: Navigate to android Directory and Build AAR + run: | + echo "Navigating to the example directory..." + cd android/llama.android + echo "Starting Gradle build process in $(pwd)..." + ./gradlew assembleRelease --stacktrace --info + shell: bash + + - name: Rename and upload AAR + run: | + echo "Navigating to the android directory to find AAR output..." + cd android/llama.android + mkdir -p ../artifacts + ls -ld ../artifacts || echo "Artifacts directory does not exist." + AAR_PATH=$(find ./llama/build/outputs/aar -type f -name "*.aar" | head -n 1) + if [ -z "$AAR_PATH" ]; then + echo "No AAR file found. Build might have failed." + exit 1 + fi + BRANCH_NAME=${{ github.ref_name }} + CUSTOM_NAME="com-nexa-${BRANCH_NAME}-${{ github.run_number }}.aar" + echo "Found AAR at $AAR_PATH, renaming to $CUSTOM_NAME..." + mv "$AAR_PATH" "../artifacts/$CUSTOM_NAME" + shell: bash + + - name: Upload AAR as an artifact + uses: actions/upload-artifact@v3 + with: + name: custom-aar-${{ github.ref_name }}-${{ github.run_number }} + path: android/artifacts/ + + release: + name: Create GitHub Release + needs: build + runs-on: ubuntu-latest + if: github.event_name == 'push' && contains(github.ref, 'main') + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Download Artifacts + uses: actions/download-artifact@v3 + with: + name: custom-aar-${{ github.ref_name }}-${{ github.run_number }} + path: release-artifacts + + - name: Create Release + id: create_release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: v${{ github.run_number }} + release_name: "Release v${{ github.run_number }}" + body: | + This is an automated release containing the latest AAR build. + - **Branch:** ${{ github.ref_name }} + - **Build Number:** ${{ github.run_number }} + draft: false + prerelease: false + + - name: Upload AAR to Release + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: release-artifacts/com-nexa-${{ github.ref_name }}-${{ github.run_number }}.aar + asset_name: com-nexa-${{ github.ref_name }}-${{ github.run_number }}.aar + asset_content_type: application/java-archive \ No newline at end of file diff --git a/android/llama.android/.gitignore b/android/llama.android/.gitignore new file mode 100644 index 00000000..347e252e --- /dev/null +++ b/android/llama.android/.gitignore @@ -0,0 +1,33 @@ +# 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 diff --git a/android/llama.android/README.md b/android/llama.android/README.md new file mode 100644 index 00000000..aa91234c --- /dev/null +++ b/android/llama.android/README.md @@ -0,0 +1,54 @@ +# Nexa + +**Nexa** is a Kotlin wrapper for the [llama.cpp](https://github.com/ggerganov/llama.cpp.git) library. offering a convenient Kotlin API for Android developers. It allows seamless integration of llama.cpp models into Android applications. +**NOTE:** Currently, Nexa supports Vision-Language Model (VLM) inference capabilities. + +## Installation + +To add Nexa to your Android project, follow these steps: + +- Create a libs folder in your project’s root directory. +- Copy the .aar file into the libs folder. +- Add dependency to your build.gradle file: + +``` +implementation files("libs/com.nexa.aar") +``` + +## Usage +### 1. Initialize NexaSwift with model path and projector path + +Create a configuration and initialize NexaSwift with the path to your model file: + +```kotlin +nexaVlmInference = NexaVlmInference(pathToModel, + mmprojectorPath, imagePath, + maxNewTokens = 128, + stopWords = listOf("")) +nexaVlmInference.loadModel() +``` + +### 2. Completion API + +#### Streaming Mode + +```swift +nexaVlmInference.createCompletionStream(prompt, imagePath) + ?.catch { + print(it.message) + } + ?.collect { print(it) } +``` + +### 3. release all resources +```kotlin +nexaVlmInference.dispose() +``` + +## Quick Start + +Open the [android test project](./app-java) folder in Android Studio and run the project. + +## Download Models + +You can download models from the [Nexa AI ModelHub](https://nexa.ai/models). \ No newline at end of file diff --git a/android/llama.android/app-java/.gitignore b/android/llama.android/app-java/.gitignore new file mode 100644 index 00000000..42df58a2 --- /dev/null +++ b/android/llama.android/app-java/.gitignore @@ -0,0 +1,2 @@ +/build +!*.png \ No newline at end of file diff --git a/android/llama.android/app-java/build.gradle b/android/llama.android/app-java/build.gradle new file mode 100644 index 00000000..2729f317 --- /dev/null +++ b/android/llama.android/app-java/build.gradle @@ -0,0 +1,52 @@ +plugins { + id 'com.android.application' + id 'kotlin-android' +} + +android { + namespace 'ai.nexa.app_java' + compileSdk 34 + + defaultConfig { + applicationId "ai.nexa.app_java" + minSdk 33 + targetSdk 34 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_17 // or VERSION_1_8 + targetCompatibility JavaVersion.VERSION_17 // or VERSION_1_8 + } + + kotlinOptions { + jvmTarget = "17" // or "1.8" + } +} + +dependencies { + + implementation 'androidx.appcompat:appcompat:1.7.0' + implementation 'com.google.android.material:material:1.12.0' + testImplementation 'junit:junit:4.13.2' + androidTestImplementation 'androidx.test.ext:junit:1.2.1' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1' + + implementation "org.jetbrains.kotlin:kotlin-stdlib:1.9.20" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3" + + implementation 'com.github.bumptech.glide:glide:4.16.0' + annotationProcessor 'com.github.bumptech.glide:compiler:4.16.0' + + implementation project(":llama") + // implementation files("libs/com.nexa.aar") +} \ No newline at end of file diff --git a/android/llama.android/app-java/proguard-rules.pro b/android/llama.android/app-java/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/android/llama.android/app-java/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 \ No newline at end of file diff --git a/android/llama.android/app-java/src/androidTest/java/ai/nexa/app_java/ExampleInstrumentedTest.java b/android/llama.android/app-java/src/androidTest/java/ai/nexa/app_java/ExampleInstrumentedTest.java new file mode 100644 index 00000000..7f3c2198 --- /dev/null +++ b/android/llama.android/app-java/src/androidTest/java/ai/nexa/app_java/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package ai.nexa.app_java; + +import android.content.Context; + +import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); + assertEquals("ai.nexa.app_java", appContext.getPackageName()); + } +} \ No newline at end of file diff --git a/android/llama.android/app-java/src/main/AndroidManifest.xml b/android/llama.android/app-java/src/main/AndroidManifest.xml new file mode 100644 index 00000000..8aaea0a2 --- /dev/null +++ b/android/llama.android/app-java/src/main/AndroidManifest.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + > + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android/llama.android/app-java/src/main/java/ai/nexa/app_java/ImagePathHelper.java b/android/llama.android/app-java/src/main/java/ai/nexa/app_java/ImagePathHelper.java new file mode 100644 index 00000000..a8b0ef00 --- /dev/null +++ b/android/llama.android/app-java/src/main/java/ai/nexa/app_java/ImagePathHelper.java @@ -0,0 +1,112 @@ +package ai.nexa.app_java; + +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.provider.DocumentsContract; +import android.provider.MediaStore; +import android.util.Log; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +public class ImagePathHelper { + private static final String TAG = "MessageProcessor"; + private final Context context; + + public ImagePathHelper(Context context) { + this.context = context; + } + + public String getPathFromUri(String uriString) { + try { + Uri uri = Uri.parse(uriString); + + // Handle "content://" scheme + if ("content".equals(uri.getScheme())) { + // Handle Google Photos and other document providers + if (DocumentsContract.isDocumentUri(context, uri)) { + final String docId = DocumentsContract.getDocumentId(uri); + + // MediaStore documents + if ("com.android.providers.media.documents".equals(uri.getAuthority())) { + final String[] split = docId.split(":"); + final String type = split[0]; + Uri contentUri = null; + + if ("image".equals(type)) { + contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI; + } + + final String selection = "_id=?"; + final String[] selectionArgs = new String[]{split[1]}; + return getDataColumn(context, contentUri, selection, selectionArgs); + } + } + // MediaStore (general case) + return getDataColumn(context, uri, null, null); + } + // Handle "file://" scheme + else if ("file".equals(uri.getScheme())) { + return uri.getPath(); + } + // Handle absolute path + else if (new File(uriString).exists()) { + return uriString; + } + + return null; + } catch (Exception e) { + Log.e(TAG, "Error getting path from URI: " + uriString, e); + return null; + } + } + + public String copyUriToPrivateFile(Context context, String uriString) throws IOException { + // 将字符串转换回 Uri + Uri uri = Uri.parse(uriString); + + // 应用私有目录 + File privateDir = context.getExternalFilesDir("images"); + if (privateDir == null) { + throw new IOException("Private directory not available"); + } + + // 创建目标文件 + File destFile = new File(privateDir, "temp_image_" + System.currentTimeMillis() + ".jpg"); + + try (InputStream inputStream = context.getContentResolver().openInputStream(uri); + OutputStream outputStream = new FileOutputStream(destFile)) { + + if (inputStream == null) { + throw new IOException("Failed to open URI input stream"); + } + + // 读取并写入数据 + byte[] buffer = new byte[4096]; + int bytesRead; + while ((bytesRead = inputStream.read(buffer)) != -1) { + outputStream.write(buffer, 0, bytesRead); + } + } + + // 返回文件路径 + return destFile.getAbsolutePath(); + } + + private String getDataColumn(Context context, Uri uri, String selection, String[] selectionArgs) { + final String[] projection = {MediaStore.Images.Media.DATA}; + try (Cursor cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs, null)) { + if (cursor != null && cursor.moveToFirst()) { + final int columnIndex = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA); + return cursor.getString(columnIndex); + } + } catch (Exception e) { + Log.e(TAG, "Error getting data column", e); + } + return null; + } +} diff --git a/android/llama.android/app-java/src/main/java/ai/nexa/app_java/KotlinFlowHelper.kt b/android/llama.android/app-java/src/main/java/ai/nexa/app_java/KotlinFlowHelper.kt new file mode 100644 index 00000000..0183ff14 --- /dev/null +++ b/android/llama.android/app-java/src/main/java/ai/nexa/app_java/KotlinFlowHelper.kt @@ -0,0 +1,44 @@ +package ai.nexa.app_java + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.cancelChildren +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class KotlinFlowHelper { + private val scope = CoroutineScope(Dispatchers.IO) + + fun collectFlow( + flow: Flow, // Added missing flow parameter + onToken: (String) -> Unit, + onComplete: (String) -> Unit, + onError: (String) -> Unit + ) { + scope.launch { + try { + val fullResponse = StringBuilder() + withContext(Dispatchers.IO) { + flow.collect { value -> + fullResponse.append(value) + withContext(Dispatchers.Main) { + onToken(value) + } + } + } + withContext(Dispatchers.Main) { + onComplete(fullResponse.toString()) + } + } catch (e: Exception) { + withContext(Dispatchers.Main) { + onError(e.message ?: "Unknown error") + } + } + } + } + + fun cancel() { + scope.coroutineContext.cancelChildren() + } +} \ No newline at end of file diff --git a/android/llama.android/app-java/src/main/java/ai/nexa/app_java/KotlinJavaUtils.kt b/android/llama.android/app-java/src/main/java/ai/nexa/app_java/KotlinJavaUtils.kt new file mode 100644 index 00000000..1fc8437c --- /dev/null +++ b/android/llama.android/app-java/src/main/java/ai/nexa/app_java/KotlinJavaUtils.kt @@ -0,0 +1,11 @@ +package ai.nexa.app_java + +import java.util.function.Consumer + +object KotlinJavaUtils { + @JvmStatic + fun toKotlinCallback(callback: Consumer): (String) -> Unit = { value -> + callback.accept(value) + Unit + } +} \ No newline at end of file diff --git a/android/llama.android/app-java/src/main/java/ai/nexa/app_java/LlamaBridge.java b/android/llama.android/app-java/src/main/java/ai/nexa/app_java/LlamaBridge.java new file mode 100644 index 00000000..e48ec8d5 --- /dev/null +++ b/android/llama.android/app-java/src/main/java/ai/nexa/app_java/LlamaBridge.java @@ -0,0 +1,283 @@ +package ai.nexa.app_java; + +import android.content.Context; +import com.nexa.NexaVlmInference; +import android.util.Log; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.concurrent.CancellationException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import kotlin.Unit; +import kotlin.coroutines.Continuation; +import kotlin.jvm.functions.Function1; +import kotlinx.coroutines.BuildersKt; +import kotlinx.coroutines.CoroutineStart; +import kotlinx.coroutines.Dispatchers; +import kotlinx.coroutines.GlobalScope; +import kotlinx.coroutines.Job; +import kotlinx.coroutines.flow.Flow; +import kotlinx.coroutines.flow.FlowCollector; + +public class LlamaBridge { + private static final String TAG = "LlamaBridge"; + private final Context context; + private final ExecutorService executor; + private final MessageHandler messageHandler; + private final VlmModelManager modelManager; + private final ImagePathHelper imagePathHelper; + private NexaVlmInference nexaVlmInference; + private boolean isModelLoaded = false; + + private final KotlinFlowHelper flowHelper = new KotlinFlowHelper(); + + // Default inference parameters + private static final float DEFAULT_TEMPERATURE = 1.0f; + private static final int DEFAULT_MAX_TOKENS = 64; + private static final int DEFAULT_TOP_K = 50; + private static final float DEFAULT_TOP_P = 0.9f; + + public interface InferenceCallback { + void onStart(); + void onToken(String token); + void onComplete(String fullResponse); + void onError(String error); + } + + public LlamaBridge(Context context, MessageHandler messageHandler) { + this.context = context; + this.messageHandler = messageHandler; + this.executor = Executors.newSingleThreadExecutor(); + this.modelManager = new VlmModelManager(context); + this.imagePathHelper = new ImagePathHelper(context); + } + + public boolean areModelsAvailable() { + return modelManager.areModelsAvailable(); + } + + public void loadModel() { + executor.execute(() -> { + try { + if (!modelManager.areModelsAvailable()) { + throw new IOException("Required model files are not available"); + } + + String modelPath = modelManager.getTextModelPath(); + String projectorPath = modelManager.getMmProjModelPath(); + + Log.d(TAG, "Loading model from: " + modelPath); + Log.d(TAG, "Loading projector from: " + projectorPath); + + // Create with default values for optional parameters + nexaVlmInference = new NexaVlmInference( + modelPath, // modelPath + projectorPath, // projectorPath + "", // imagePath (empty string as default) + new ArrayList<>(Arrays.asList("")), // stopWords (empty list) + DEFAULT_TEMPERATURE, // temperature + DEFAULT_MAX_TOKENS, // maxNewTokens + DEFAULT_TOP_K, // topK + DEFAULT_TOP_P // topP + ); + nexaVlmInference.loadModel(); + isModelLoaded = true; + + Log.d(TAG, "Model loaded successfully."); +// messageHandler.addMessage(new MessageModal("Model loaded successfully", "assistant", null)); + } catch (Exception e) { + Log.e(TAG, "Failed to load model", e); + messageHandler.addMessage(new MessageModal("Error loading model: " + e.getMessage(), "assistant", null)); + } + }); + } + +// public void processMessage(String message, String imageUri, InferenceCallback callback) { +// if (!isModelLoaded) { +// callback.onError("Model not loaded yet"); +// return; +// } +// +// try { +// // Add user message first +// MessageModal userMessage = new MessageModal(message, "user", imageUri); +// messageHandler.addMessage(userMessage); +// +// // Create an initial empty assistant message +// MessageModal assistantMessage = new MessageModal("", "assistant", null); +// messageHandler.addMessage(assistantMessage); +// +// // Convert image URI to absolute path +// String imageAbsolutePath = imagePathHelper.getPathFromUri(imageUri); +// +// Flow flow = nexaVlmInference.createCompletionStream( +// message, +// imageAbsolutePath, +// new ArrayList<>(), +// DEFAULT_TEMPERATURE, +// DEFAULT_MAX_TOKENS, +// DEFAULT_TOP_K, +// DEFAULT_TOP_P +// ); +// +// if (flow != null) { +// CoroutineScope scope = CoroutineScopeKt.CoroutineScope(Dispatchers.getMain()); +// +// Job job = FlowKt.launchIn( +// FlowKt.onEach(flow, new Function2, Object>() { +// @Override +// public Object invoke(String token, Continuation continuation) { +// messageHandler.updateLastAssistantMessage(token); +// callback.onToken(token); +// return Unit.INSTANCE; +// } +// }), +// scope +// ); +// } else { +// messageHandler.finalizeLastAssistantMessage("Error: Failed to create completion stream"); +// callback.onError("Failed to create completion stream"); +// } +// } catch (Exception e) { +// Log.e(TAG, "Error processing message", e); +// messageHandler.finalizeLastAssistantMessage("Error: " + e.getMessage()); +// callback.onError(e.getMessage()); +// } +// } + + public void processMessage(String message, String imageUri, InferenceCallback callback) { + if (!isModelLoaded) { + callback.onError("Model not loaded yet"); + return; + } + + String imageAbsolutePath = null; + try { + imageAbsolutePath = imagePathHelper.copyUriToPrivateFile(context, imageUri); + } catch (IOException e) { + callback.onError("Failed to process image: " + e.getMessage()); + return; + } + + final String imagePath = imageAbsolutePath; + MessageModal assistantMessage = new MessageModal("", "bot", null); + messageHandler.addMessage(assistantMessage); + + try { + Flow flow = nexaVlmInference.createCompletionStream( + message, + imagePath, + new ArrayList<>(Arrays.asList("")), + DEFAULT_TEMPERATURE, + DEFAULT_MAX_TOKENS, + DEFAULT_TOP_K, + DEFAULT_TOP_P + ); + + callback.onStart(); + StringBuilder fullResponse = new StringBuilder(); + + Job collectJob = BuildersKt.launch( + GlobalScope.INSTANCE, + Dispatchers.getIO(), + CoroutineStart.DEFAULT, + (coroutineScope, continuation) -> { + flow.collect(new FlowCollector() { + @Override + public Object emit(String token, Continuation continuation) { + fullResponse.append(token); + callback.onToken(token); + return Unit.INSTANCE; + } + }, continuation); + callback.onComplete(fullResponse.toString()); + return Unit.INSTANCE; + } + ); + + collectJob.invokeOnCompletion(new Function1() { + @Override + public Unit invoke(Throwable throwable) { + if (throwable != null && !(throwable instanceof CancellationException)) { + callback.onError("Stream collection failed: " + throwable.getMessage()); + } + return Unit.INSTANCE; + } + }); + + } catch (Exception e) { + Log.e(TAG, "Inference failed", e); + callback.onError(e.getMessage()); + } + } + + public void cleanup() { + flowHelper.cancel(); + } + +// public void processMessageWithParams( +// String message, +// String imageUri, +// float temperature, +// int maxTokens, +// int topK, +// float topP, +// InferenceCallback callback) { +// +// if (!isModelLoaded) { +// callback.onError("Model not loaded yet"); +// return; +// } +// +// executor.execute(() -> { +// StringBuilder fullResponse = new StringBuilder(); +// try { +// callback.onStart(); +// +// Flow completionStream = nexaVlmInference.createCompletionStream( +// message, +// imageUri, +// new ArrayList<>(), +// temperature, +// maxTokens, +// topK, +// topP +// ); +// +// completionStream.collect(new FlowCollector() { +// @Override +// public Object emit(String value, Continuation continuation) { +// fullResponse.append(value); +// callback.onToken(value); +// return Unit.INSTANCE; +// } +// }); +// +// callback.onComplete(fullResponse.toString()); +// +// } catch (Exception e) { +// Log.e(TAG, "Inference failed", e); +// callback.onError(e.getMessage()); +// } +// }); +// } + + + public void shutdown() { + if (nexaVlmInference != null) { + executor.execute(() -> { + try { + nexaVlmInference.dispose(); + } catch (Exception e) { + Log.e(TAG, "Error closing inference", e); + } + nexaVlmInference = null; + isModelLoaded = false; + }); + } + executor.shutdown(); + } +} \ No newline at end of file diff --git a/android/llama.android/app-java/src/main/java/ai/nexa/app_java/MainActivity.java b/android/llama.android/app-java/src/main/java/ai/nexa/app_java/MainActivity.java new file mode 100644 index 00000000..29be7214 --- /dev/null +++ b/android/llama.android/app-java/src/main/java/ai/nexa/app_java/MainActivity.java @@ -0,0 +1,345 @@ +package ai.nexa.app_java; + +import android.Manifest; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.net.Uri; +import android.os.Bundle; +import android.os.Message; +import android.provider.MediaStore; +import android.speech.RecognizerIntent; +import android.speech.SpeechRecognizer; +import android.util.Log; +import android.view.MotionEvent; +import android.view.View; +import android.view.inputmethod.InputMethodManager; +import android.widget.EditText; +import android.widget.ImageButton; +import android.widget.LinearLayout; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.AppCompatActivity; +import androidx.core.app.ActivityCompat; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +public class MainActivity extends AppCompatActivity { + + private static final String TAG = "ChatApp"; + private static final int PICK_IMAGE_REQUEST = 30311; + private static final int REQUEST_RECORD_AUDIO_PERMISSION = 200; + private static final int READ_EXTERNAL_STORAGE_PERMISSION = 303; + + private RecyclerView chatsRV; + private ImageButton selectImageButton; + private ImageButton sendMsgIB; + private EditText userMsgEdt; + private String justSelectedImageUri; + + private LinearLayout linearLayout; + private TextView titleAfterChatTextView; + private RecyclerView recyclerView; + + private ArrayList messageModalArrayList; + private MessageRVAdapter messageRVAdapter; + private MessageHandler messageHandler; + private LlamaBridge llamaBridge; + private SpeechRecognizer speechRecognizer; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + + Log.d(TAG, "onCreate: Starting MainActivity"); + + initializeViews(); + setupRecyclerView(); + initializeLlamaBridge(); + createSpeechRecognizerIntent(); + setupClickListeners(); + + Log.d(TAG, "onCreate: MainActivity setup complete"); + } + + private void initializeViews() { + chatsRV = findViewById(R.id.idRVChats); + selectImageButton = findViewById(R.id.btnUploadImage); + sendMsgIB = findViewById(R.id.idIBSend); + userMsgEdt = findViewById(R.id.idEdtMessage); + linearLayout = findViewById(R.id.idLayoutBeforeChat); + titleAfterChatTextView = findViewById(R.id.textView); + recyclerView = findViewById(R.id.idRVChats); + } + + private void setupRecyclerView() { + messageModalArrayList = new ArrayList<>(); + messageRVAdapter = new MessageRVAdapter(messageModalArrayList, this); + chatsRV.setLayoutManager(new LinearLayoutManager(this, RecyclerView.VERTICAL, false)); + chatsRV.setAdapter(messageRVAdapter); + messageHandler = new MessageHandler(messageModalArrayList, messageRVAdapter, recyclerView); + } + + private void initializeLlamaBridge() { + llamaBridge = new LlamaBridge(this, messageHandler); + if (!llamaBridge.areModelsAvailable()) { + Toast.makeText(this, "Required model files are not available", Toast.LENGTH_LONG).show(); + return; + } + llamaBridge.loadModel(); + } + + private void setupClickListeners() { + selectImageButton.setOnClickListener(v -> { + Intent intent = new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI); + startActivityForResult(intent, PICK_IMAGE_REQUEST); + }); + + sendMsgIB.setOnClickListener(v -> { + hideKeyboard(v); + sendTextMessage(); + }); + } + + private void updateChatBotDisplay() { + linearLayout.setVisibility(View.GONE); + titleAfterChatTextView.setVisibility(View.VISIBLE); + recyclerView.setVisibility(View.VISIBLE); + } + + private void sendTextMessage() { + updateChatBotDisplay(); + + String userMessage = userMsgEdt.getText().toString().trim(); + if (!userMessage.isEmpty()) { + Log.d(TAG, "Sending message: " + userMessage); + messageHandler.addMessage(new MessageModal(userMessage, "user", null)); + + if (justSelectedImageUri == null) { + messageHandler.addMessage(new MessageModal("Please select an image first.", "bot", null)); + return; + } + + // Use LlamaBridge for inference + llamaBridge.processMessage(userMessage, justSelectedImageUri, new LlamaBridge.InferenceCallback() { + @Override + public void onStart() { + // Optional: Show loading indicator + } + + @Override + public void onToken(String token) { + // Update the UI with each token as it comes in + runOnUiThread(() -> { + messageHandler.updateLastBotMessage(token); + }); + } + + @Override + public void onComplete(String fullResponse) { + // Final update with complete response + runOnUiThread(() -> { + messageHandler.finalizeLastBotMessage(fullResponse); + }); + } + + @Override + public void onError(String error) { + runOnUiThread(() -> { + Toast.makeText(MainActivity.this, "Error: " + error, Toast.LENGTH_SHORT).show(); + messageHandler.addMessage(new MessageModal("Error processing message: " + error, "assistant", null)); + }); + } + }); + + userMsgEdt.setText(""); // Clear the input field after sending + justSelectedImageUri = null; // Clear the image URI after sending + } else { + Toast.makeText(MainActivity.this, "Please enter your message.", Toast.LENGTH_SHORT).show(); + } + } + + private void sendImageAsMessage(String imageUri) { + updateChatBotDisplay(); + messageHandler.addMessage(new MessageModal("", "user", imageUri)); + justSelectedImageUri = imageUri; + } + + @Override + protected void onDestroy() { + super.onDestroy(); + if (llamaBridge != null) { + llamaBridge.shutdown(); + } + if (speechRecognizer != null) { + speechRecognizer.destroy(); + } + } + + private void createSpeechRecognizerIntent() { + requestMicrophonePermission(); + + ImageButton btnStart = findViewById(R.id.btnStart); + + speechRecognizer = SpeechRecognizer.createSpeechRecognizer(this); + + Intent speechRecognizerIntent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH); + speechRecognizerIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_FREE_FORM); + speechRecognizerIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE, Locale.getDefault()); + speechRecognizerIntent.putExtra(RecognizerIntent.EXTRA_PARTIAL_RESULTS, true); + + speechRecognizer.setRecognitionListener(new android.speech.RecognitionListener() { + @Override + public void onReadyForSpeech(Bundle params) { + } + + @Override + public void onBeginningOfSpeech() { + } + + @Override + public void onRmsChanged(float rmsdB) { + } + + @Override + public void onBufferReceived(byte[] buffer) { + } + + @Override + public void onEndOfSpeech() { + } + + @Override + public void onError(int error) { + String errorMessage = getErrorText(error); + Log.d("SpeechRecognition", "Error occurred: " + errorMessage); + } + + public String getErrorText(int errorCode) { + String message; + switch (errorCode) { + case SpeechRecognizer.ERROR_AUDIO: + message = "Audio recording error"; + break; + case SpeechRecognizer.ERROR_CLIENT: + message = "Client side error"; + break; + case SpeechRecognizer.ERROR_INSUFFICIENT_PERMISSIONS: + message = "Insufficient permissions"; + break; + case SpeechRecognizer.ERROR_NETWORK: + message = "Network error"; + break; + case SpeechRecognizer.ERROR_NETWORK_TIMEOUT: + message = "Network timeout"; + break; + case SpeechRecognizer.ERROR_NO_MATCH: + message = "No match"; + break; + case SpeechRecognizer.ERROR_RECOGNIZER_BUSY: + message = "RecognitionService busy"; + break; + case SpeechRecognizer.ERROR_SERVER: + message = "Error from server"; + break; + case SpeechRecognizer.ERROR_SPEECH_TIMEOUT: + message = "No speech input"; + break; + default: + message = "Didn't understand, please try again."; + break; + } + return message; + } + + @Override + public void onResults(Bundle results) { + ArrayList matches = results.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION); + if (matches != null && !matches.isEmpty()) { + userMsgEdt.setText(matches.get(0)); // Set the recognized text to the EditText + sendTextMessage(); + } + } + + @Override + public void onPartialResults(Bundle partialResults) { + // This is called for partial results + ArrayList partialMatches = partialResults.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION); + if (partialMatches != null && !partialMatches.isEmpty()) { + userMsgEdt.setText(partialMatches.get(0)); // Update EditText with the partial result + } + } + + @Override + public void onEvent(int eventType, Bundle params) { + } + }); + + btnStart.setOnTouchListener(new View.OnTouchListener() { + @Override + public boolean onTouch(View v, MotionEvent event) { + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + // Button is pressed + speechRecognizer.startListening(speechRecognizerIntent); + return true; // Return true to indicate the event was handled + case MotionEvent.ACTION_UP: + // Button is released + speechRecognizer.stopListening(); + return true; // Return true to indicate the event was handled + } + return false; // Return false for other actions + } + }); + } + + private void requestMicrophonePermission() { + ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.RECORD_AUDIO}, REQUEST_RECORD_AUDIO_PERMISSION); + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + switch (requestCode) { + case READ_EXTERNAL_STORAGE_PERMISSION: + if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + Toast.makeText(this, "Read External Storage Permission Granted", Toast.LENGTH_SHORT).show(); + Intent intent = new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI); + startActivityForResult(intent, PICK_IMAGE_REQUEST); + } else { + Toast.makeText(this, "Read External Storage Permission Denied", Toast.LENGTH_SHORT).show(); + } + break; + default: + break; + } + + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if (requestCode == PICK_IMAGE_REQUEST && resultCode == RESULT_OK && data != null) { + Uri selectedImage = data.getData(); + if (selectedImage != null) { + String imageUriString = selectedImage.toString(); + sendImageAsMessage(imageUriString); + } + } + } + + public void hideKeyboard(View view) { + InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); + if (inputMethodManager != null) { + inputMethodManager.hideSoftInputFromWindow(view.getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS); + } + } + +} \ No newline at end of file diff --git a/android/llama.android/app-java/src/main/java/ai/nexa/app_java/MessageHandler.java b/android/llama.android/app-java/src/main/java/ai/nexa/app_java/MessageHandler.java new file mode 100644 index 00000000..39720c1f --- /dev/null +++ b/android/llama.android/app-java/src/main/java/ai/nexa/app_java/MessageHandler.java @@ -0,0 +1,127 @@ +package ai.nexa.app_java; + +import androidx.recyclerview.widget.RecyclerView; +import android.os.Handler; +import android.os.Looper; + +import java.util.ArrayList; + +public class MessageHandler { + private final ArrayList messageModalArrayList; + private final MessageRVAdapter messageRVAdapter; + private final RecyclerView recyclerView; + private final Handler mainHandler; + + public MessageHandler(ArrayList messageModalArrayList, MessageRVAdapter messageRVAdapter, RecyclerView recyclerView) { + this.messageModalArrayList = messageModalArrayList; + this.messageRVAdapter = messageRVAdapter; + this.recyclerView = recyclerView; + this.mainHandler = new Handler(Looper.getMainLooper()); + } + + /** + * Add a new message to the chat + */ + public void addMessage(MessageModal message) { + ensureMainThread(() -> { + messageModalArrayList.add(message); + messageRVAdapter.notifyItemInserted(messageModalArrayList.size() - 1); + scrollToBottom(); + }); + } + + /** + * Update the last bot message with new token + */ + public void updateLastBotMessage(String newToken) { + ensureMainThread(() -> { + if (!messageModalArrayList.isEmpty()) { + int lastIndex = messageModalArrayList.size() - 1; + MessageModal lastMessage = messageModalArrayList.get(lastIndex); + + // If last message is from bot, update it + if ("bot".equals(lastMessage.getSender())) { + String currentMessage = lastMessage.getMessage(); + lastMessage.setMessage(currentMessage + newToken); + messageRVAdapter.notifyItemChanged(lastIndex); + } else { + // Create new bot message + MessageModal newMessage = new MessageModal(newToken, "bot", null); + messageModalArrayList.add(newMessage); + messageRVAdapter.notifyItemInserted(messageModalArrayList.size() - 1); + } + scrollToBottom(); + } + }); + } + + /** + * Finalize the last bot message with complete response + */ + public void finalizeLastBotMessage(String completeMessage) { + ensureMainThread(() -> { + if (!messageModalArrayList.isEmpty()) { + int lastIndex = messageModalArrayList.size() - 1; + MessageModal lastMessage = messageModalArrayList.get(lastIndex); + + if ("bot".equals(lastMessage.getSender())) { + lastMessage.setMessage(completeMessage); + messageRVAdapter.notifyItemChanged(lastIndex); + } else { + MessageModal newMessage = new MessageModal(completeMessage, "bot", null); + messageModalArrayList.add(newMessage); + messageRVAdapter.notifyItemInserted(messageModalArrayList.size() - 1); + } + scrollToBottom(); + } + }); + } + + /** + * Clear all messages from the chat + */ + public void clearMessages() { + ensureMainThread(() -> { + messageModalArrayList.clear(); + messageRVAdapter.notifyDataSetChanged(); + }); + } + + /** + * Get the last message in the chat + */ + public MessageModal getLastMessage() { + if (!messageModalArrayList.isEmpty()) { + return messageModalArrayList.get(messageModalArrayList.size() - 1); + } + return null; + } + + /** + * Check if the last message is from the bot + */ + public boolean isLastMessageFromBot() { + MessageModal lastMessage = getLastMessage(); + return lastMessage != null && "bot".equals(lastMessage.getSender()); + } + + /** + * Scroll the RecyclerView to the bottom + */ + private void scrollToBottom() { + if (messageModalArrayList.size() > 1) { + recyclerView.smoothScrollToPosition(messageModalArrayList.size() - 1); + } + } + + /** + * Ensure all UI updates happen on the main thread + */ + private void ensureMainThread(Runnable action) { + if (Looper.myLooper() == Looper.getMainLooper()) { + action.run(); + } else { + mainHandler.post(action); + } + } +} \ No newline at end of file diff --git a/android/llama.android/app-java/src/main/java/ai/nexa/app_java/MessageModal.java b/android/llama.android/app-java/src/main/java/ai/nexa/app_java/MessageModal.java new file mode 100644 index 00000000..1e60921b --- /dev/null +++ b/android/llama.android/app-java/src/main/java/ai/nexa/app_java/MessageModal.java @@ -0,0 +1,42 @@ +package ai.nexa.app_java; + +public class MessageModal { + + + private String message; + private String sender; + + private String imageUri; + + public MessageModal(String message, String sender, String imageUri) { + this.message = message; + this.sender = sender; + this.imageUri = imageUri; + } + + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public String getSender() { + return sender; + } + + public void setSender(String sender) { + this.sender = sender; + } + + public String getImageUri() { + return imageUri; + } + + public void setImageUri(String imageUri) { + this.imageUri = imageUri; + } +} + diff --git a/android/llama.android/app-java/src/main/java/ai/nexa/app_java/MessageRVAdapter.java b/android/llama.android/app-java/src/main/java/ai/nexa/app_java/MessageRVAdapter.java new file mode 100644 index 00000000..90977681 --- /dev/null +++ b/android/llama.android/app-java/src/main/java/ai/nexa/app_java/MessageRVAdapter.java @@ -0,0 +1,102 @@ +package ai.nexa.app_java; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.bumptech.glide.Glide; + +import java.util.ArrayList; + +public class MessageRVAdapter extends RecyclerView.Adapter { + + private ArrayList messageModalArrayList; + private Context context; + + public MessageRVAdapter(ArrayList messageModalArrayList, Context context) { + this.messageModalArrayList = messageModalArrayList; + this.context = context; + } + + @NonNull + @Override + public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View view; + switch (viewType) { + case 0: + view = LayoutInflater.from(parent.getContext()).inflate(R.layout.user_msg, parent, false); + return new UserViewHolder(view); + case 1: + view = LayoutInflater.from(parent.getContext()).inflate(R.layout.bot_msg, parent, false); + return new BotViewHolder(view); + } + return null; + } + + @Override + public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { + MessageModal modal = messageModalArrayList.get(position); + switch (modal.getSender()) { + case "user": + UserViewHolder userHolder = (UserViewHolder) holder; + if (modal.getImageUri() != null && !modal.getImageUri().isEmpty()) { + userHolder.userImage.setVisibility(View.VISIBLE); + userHolder.userTV.setVisibility(View.GONE); + Glide.with(userHolder.itemView.getContext()) + .load(modal.getImageUri()) + .into(userHolder.userImage); + } else { + userHolder.userImage.setVisibility(View.GONE); + userHolder.userTV.setVisibility(View.VISIBLE); + userHolder.userTV.setText(modal.getMessage()); + } + break; + case "bot": + ((BotViewHolder) holder).botTV.setText(modal.getMessage()); + break; + } + } + + @Override + public int getItemCount() { + return messageModalArrayList.size(); + } + + @Override + public int getItemViewType(int position) { + switch (messageModalArrayList.get(position).getSender()) { + case "user": + return 0; + case "bot": + return 1; + default: + return -1; + } + } + + public static class UserViewHolder extends RecyclerView.ViewHolder { + TextView userTV; + ImageView userImage; + + public UserViewHolder(@NonNull View itemView) { + super(itemView); + userTV = itemView.findViewById(R.id.idTVUser); + userImage = itemView.findViewById(R.id.idIVUserImage); + } + } + + public static class BotViewHolder extends RecyclerView.ViewHolder { + TextView botTV; + + public BotViewHolder(@NonNull View itemView) { + super(itemView); + botTV = itemView.findViewById(R.id.idTVBot); + } + } +} diff --git a/android/llama.android/app-java/src/main/java/ai/nexa/app_java/VlmModelManager.java b/android/llama.android/app-java/src/main/java/ai/nexa/app_java/VlmModelManager.java new file mode 100644 index 00000000..9ebd8d45 --- /dev/null +++ b/android/llama.android/app-java/src/main/java/ai/nexa/app_java/VlmModelManager.java @@ -0,0 +1,125 @@ +package ai.nexa.app_java; + +import android.content.Context; +import android.os.Environment; +import android.util.Log; + +import java.io.File; +import java.io.IOException; + +public class VlmModelManager { + private static final String TAG = "LlamaBridge"; + private static final String MODELS_DIR = "models"; + private static final String MODEL_TEXT_FILENAME = "nanollava-text-model-q4_0.gguf"; + private static final String MODEL_MMPROJ_FILENAME = "nanollava-mmproj-f16.gguf"; + + private final Context context; + private File textModelFile; + private File mmProjModelFile; + private final File externalModelDir; + + public VlmModelManager(Context context) { + this.context = context; + this.externalModelDir = new File(Environment.getExternalStorageDirectory(), + "Android/data/" + context.getPackageName() + "/files"); + } + + /** + * Search for model in common locations + * @param modelFilename The name of the model file to find + * @return File path to the model if found, null otherwise + */ + private String findExistingModel(String modelFilename) { + // List of possible locations to check + File[] locations = { + // External storage specific path + new File(externalModelDir, modelFilename), + // Downloads folder + new File(Environment.getExternalStoragePublicDirectory( + Environment.DIRECTORY_DOWNLOADS), modelFilename), + // App's private external storage + new File(context.getExternalFilesDir(null), MODELS_DIR + "/" + modelFilename), + // App's private internal storage + new File(context.getFilesDir(), MODELS_DIR + "/" + modelFilename) + }; + + for (File location : locations) { + if (location.exists() && location.canRead()) { + Log.d(TAG, "Found model at: " + location.getAbsolutePath()); + return location.getAbsolutePath(); + } + } + return null; + } + + /** + * Get text model path, searching in storage locations + * @return Path to the model file + * @throws IOException if model cannot be found or accessed + */ + public String getTextModelPath() throws IOException { + // If we already have a valid model file, return it + if (textModelFile != null && textModelFile.exists() && textModelFile.canRead()) { + return textModelFile.getAbsolutePath(); + } + + // Search for existing model + String path = findExistingModel(MODEL_TEXT_FILENAME); + if (path != null) { + textModelFile = new File(path); + return path; + } + + throw new IOException("Text model not found in any storage location"); + } + + /** + * Get mmproj model path, searching in storage locations + * @return Path to the model file + * @throws IOException if model cannot be found or accessed + */ + public String getMmProjModelPath() throws IOException { + // If we already have a valid model file, return it + if (mmProjModelFile != null && mmProjModelFile.exists() && mmProjModelFile.canRead()) { + return mmProjModelFile.getAbsolutePath(); + } + + // Search for existing model + String path = findExistingModel(MODEL_MMPROJ_FILENAME); + if (path != null) { + mmProjModelFile = new File(path); + return path; + } + + throw new IOException("MMProj model not found in any storage location"); + } + + /** + * Check if both required models exist in any location + * @return true if both models are found + */ + public boolean areModelsAvailable() { + try { + getTextModelPath(); + getMmProjModelPath(); + return true; + } catch (IOException e) { + Log.w(TAG, "Models not available: " + e.getMessage()); + return false; + } + } + + /** + * Get the directory containing the models + * @return File object for the models directory, or null if models aren't found + */ + public File getModelsDirectory() { + try { + String textModelPath = getTextModelPath(); + return new File(textModelPath).getParentFile(); + } catch (IOException e) { + Log.w(TAG, "Could not determine models directory: " + e.getMessage()); + return null; + } + } +} diff --git a/android/llama.android/app-java/src/main/res/drawable-hdpi/ic_menu_send.png b/android/llama.android/app-java/src/main/res/drawable-hdpi/ic_menu_send.png new file mode 100644 index 00000000..f34a9658 Binary files /dev/null and b/android/llama.android/app-java/src/main/res/drawable-hdpi/ic_menu_send.png differ diff --git a/android/llama.android/app-java/src/main/res/drawable-mdpi/ic_menu_send.png b/android/llama.android/app-java/src/main/res/drawable-mdpi/ic_menu_send.png new file mode 100644 index 00000000..e83f6010 Binary files /dev/null and b/android/llama.android/app-java/src/main/res/drawable-mdpi/ic_menu_send.png differ diff --git a/android/llama.android/app-java/src/main/res/drawable-v24/ic_launcher_foreground.xml b/android/llama.android/app-java/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 00000000..2b068d11 --- /dev/null +++ b/android/llama.android/app-java/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/android/llama.android/app-java/src/main/res/drawable-xhdpi/ic_menu_send.png b/android/llama.android/app-java/src/main/res/drawable-xhdpi/ic_menu_send.png new file mode 100644 index 00000000..882722eb Binary files /dev/null and b/android/llama.android/app-java/src/main/res/drawable-xhdpi/ic_menu_send.png differ diff --git a/android/llama.android/app-java/src/main/res/drawable-xxhdpi/ic_menu_send.png b/android/llama.android/app-java/src/main/res/drawable-xxhdpi/ic_menu_send.png new file mode 100644 index 00000000..08108e76 Binary files /dev/null and b/android/llama.android/app-java/src/main/res/drawable-xxhdpi/ic_menu_send.png differ diff --git a/android/llama.android/app-java/src/main/res/drawable-xxxhdpi/ic_menu_send.png b/android/llama.android/app-java/src/main/res/drawable-xxxhdpi/ic_menu_send.png new file mode 100644 index 00000000..8f7eb62c Binary files /dev/null and b/android/llama.android/app-java/src/main/res/drawable-xxxhdpi/ic_menu_send.png differ diff --git a/android/llama.android/app-java/src/main/res/drawable/bg_send_message.xml b/android/llama.android/app-java/src/main/res/drawable/bg_send_message.xml new file mode 100644 index 00000000..972981d8 --- /dev/null +++ b/android/llama.android/app-java/src/main/res/drawable/bg_send_message.xml @@ -0,0 +1,9 @@ + + + + diff --git a/android/llama.android/app-java/src/main/res/drawable/bot_message.xml b/android/llama.android/app-java/src/main/res/drawable/bot_message.xml new file mode 100644 index 00000000..8dda5f87 --- /dev/null +++ b/android/llama.android/app-java/src/main/res/drawable/bot_message.xml @@ -0,0 +1,11 @@ + + + + + + diff --git a/android/llama.android/app-java/src/main/res/drawable/gradient_background.xml b/android/llama.android/app-java/src/main/res/drawable/gradient_background.xml new file mode 100644 index 00000000..6d9a5345 --- /dev/null +++ b/android/llama.android/app-java/src/main/res/drawable/gradient_background.xml @@ -0,0 +1,8 @@ + + + + diff --git a/android/llama.android/app-java/src/main/res/drawable/ic_bot.xml b/android/llama.android/app-java/src/main/res/drawable/ic_bot.xml new file mode 100644 index 00000000..660ed4e0 --- /dev/null +++ b/android/llama.android/app-java/src/main/res/drawable/ic_bot.xml @@ -0,0 +1,171 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/llama.android/app-java/src/main/res/drawable/ic_launcher.png b/android/llama.android/app-java/src/main/res/drawable/ic_launcher.png new file mode 100644 index 00000000..e3c90853 Binary files /dev/null and b/android/llama.android/app-java/src/main/res/drawable/ic_launcher.png differ diff --git a/android/llama.android/app-java/src/main/res/drawable/ic_launcher_background.xml b/android/llama.android/app-java/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 00000000..07d5da9c --- /dev/null +++ b/android/llama.android/app-java/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/llama.android/app-java/src/main/res/drawable/ic_launcher_fav_background.xml b/android/llama.android/app-java/src/main/res/drawable/ic_launcher_fav_background.xml new file mode 100644 index 00000000..ca3826a4 --- /dev/null +++ b/android/llama.android/app-java/src/main/res/drawable/ic_launcher_fav_background.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/llama.android/app-java/src/main/res/drawable/ic_user.xml b/android/llama.android/app-java/src/main/res/drawable/ic_user.xml new file mode 100644 index 00000000..725adb58 --- /dev/null +++ b/android/llama.android/app-java/src/main/res/drawable/ic_user.xml @@ -0,0 +1,22 @@ + + + + + diff --git a/android/llama.android/app-java/src/main/res/drawable/input_text_box.xml b/android/llama.android/app-java/src/main/res/drawable/input_text_box.xml new file mode 100644 index 00000000..1c132b0b --- /dev/null +++ b/android/llama.android/app-java/src/main/res/drawable/input_text_box.xml @@ -0,0 +1,10 @@ + + + + + + + \ No newline at end of file diff --git a/android/llama.android/app-java/src/main/res/drawable/microphone.xml b/android/llama.android/app-java/src/main/res/drawable/microphone.xml new file mode 100644 index 00000000..75fe9341 --- /dev/null +++ b/android/llama.android/app-java/src/main/res/drawable/microphone.xml @@ -0,0 +1,13 @@ + + + diff --git a/android/llama.android/app-java/src/main/res/drawable/octopus_background.xml b/android/llama.android/app-java/src/main/res/drawable/octopus_background.xml new file mode 100644 index 00000000..ca3826a4 --- /dev/null +++ b/android/llama.android/app-java/src/main/res/drawable/octopus_background.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/llama.android/app-java/src/main/res/drawable/octopus_menu_send.xml b/android/llama.android/app-java/src/main/res/drawable/octopus_menu_send.xml new file mode 100644 index 00000000..4254a34f --- /dev/null +++ b/android/llama.android/app-java/src/main/res/drawable/octopus_menu_send.xml @@ -0,0 +1,11 @@ + + + diff --git a/android/llama.android/app-java/src/main/res/drawable/octopus_original.xml b/android/llama.android/app-java/src/main/res/drawable/octopus_original.xml new file mode 100644 index 00000000..92048641 --- /dev/null +++ b/android/llama.android/app-java/src/main/res/drawable/octopus_original.xml @@ -0,0 +1,171 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/llama.android/app-java/src/main/res/drawable/ocutopus_v3_full_size.png b/android/llama.android/app-java/src/main/res/drawable/ocutopus_v3_full_size.png new file mode 100644 index 00000000..de1bb864 Binary files /dev/null and b/android/llama.android/app-java/src/main/res/drawable/ocutopus_v3_full_size.png differ diff --git a/android/llama.android/app-java/src/main/res/drawable/roundcorner.xml b/android/llama.android/app-java/src/main/res/drawable/roundcorner.xml new file mode 100644 index 00000000..5c795c41 --- /dev/null +++ b/android/llama.android/app-java/src/main/res/drawable/roundcorner.xml @@ -0,0 +1,9 @@ + + + + + + + diff --git a/android/llama.android/app-java/src/main/res/drawable/title.xml b/android/llama.android/app-java/src/main/res/drawable/title.xml new file mode 100644 index 00000000..a7bad4f8 --- /dev/null +++ b/android/llama.android/app-java/src/main/res/drawable/title.xml @@ -0,0 +1,11 @@ + + + + + + + + + diff --git a/android/llama.android/app-java/src/main/res/drawable/upload_image_icon.xml b/android/llama.android/app-java/src/main/res/drawable/upload_image_icon.xml new file mode 100644 index 00000000..f4a86832 --- /dev/null +++ b/android/llama.android/app-java/src/main/res/drawable/upload_image_icon.xml @@ -0,0 +1,13 @@ + + + diff --git a/android/llama.android/app-java/src/main/res/font/abhaya_libre_bold.ttf b/android/llama.android/app-java/src/main/res/font/abhaya_libre_bold.ttf new file mode 100644 index 00000000..6f4a231d Binary files /dev/null and b/android/llama.android/app-java/src/main/res/font/abhaya_libre_bold.ttf differ diff --git a/android/llama.android/app-java/src/main/res/font/alegreya_sans_sc_extrabold.xml b/android/llama.android/app-java/src/main/res/font/alegreya_sans_sc_extrabold.xml new file mode 100644 index 00000000..8112a231 --- /dev/null +++ b/android/llama.android/app-java/src/main/res/font/alegreya_sans_sc_extrabold.xml @@ -0,0 +1,7 @@ + + + diff --git a/android/llama.android/app-java/src/main/res/layout/activity_main.xml b/android/llama.android/app-java/src/main/res/layout/activity_main.xml new file mode 100644 index 00000000..625d9923 --- /dev/null +++ b/android/llama.android/app-java/src/main/res/layout/activity_main.xml @@ -0,0 +1,115 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android/llama.android/app-java/src/main/res/layout/bot_msg.xml b/android/llama.android/app-java/src/main/res/layout/bot_msg.xml new file mode 100644 index 00000000..5ee58e1d --- /dev/null +++ b/android/llama.android/app-java/src/main/res/layout/bot_msg.xml @@ -0,0 +1,35 @@ + + + + + + + + + + diff --git a/android/llama.android/app-java/src/main/res/layout/user_msg.xml b/android/llama.android/app-java/src/main/res/layout/user_msg.xml new file mode 100644 index 00000000..20aa126a --- /dev/null +++ b/android/llama.android/app-java/src/main/res/layout/user_msg.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android/llama.android/app-java/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/android/llama.android/app-java/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 00000000..036d09bc --- /dev/null +++ b/android/llama.android/app-java/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/android/llama.android/app-java/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/android/llama.android/app-java/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 00000000..036d09bc --- /dev/null +++ b/android/llama.android/app-java/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/android/llama.android/app-java/src/main/res/mipmap-anydpi-v26/octopus.xml b/android/llama.android/app-java/src/main/res/mipmap-anydpi-v26/octopus.xml new file mode 100644 index 00000000..2e533e65 --- /dev/null +++ b/android/llama.android/app-java/src/main/res/mipmap-anydpi-v26/octopus.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/android/llama.android/app-java/src/main/res/mipmap-anydpi-v26/octopus_round.xml b/android/llama.android/app-java/src/main/res/mipmap-anydpi-v26/octopus_round.xml new file mode 100644 index 00000000..2e533e65 --- /dev/null +++ b/android/llama.android/app-java/src/main/res/mipmap-anydpi-v26/octopus_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/android/llama.android/app-java/src/main/res/mipmap-hdpi/ic_launcher.png b/android/llama.android/app-java/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 00000000..cf0c3458 Binary files /dev/null and b/android/llama.android/app-java/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/android/llama.android/app-java/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/android/llama.android/app-java/src/main/res/mipmap-hdpi/ic_launcher_foreground.png new file mode 100644 index 00000000..8acbf0ea Binary files /dev/null and b/android/llama.android/app-java/src/main/res/mipmap-hdpi/ic_launcher_foreground.png differ diff --git a/android/llama.android/app-java/src/main/res/mipmap-hdpi/ic_launcher_round.png b/android/llama.android/app-java/src/main/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 00000000..12580bdb Binary files /dev/null and b/android/llama.android/app-java/src/main/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/android/llama.android/app-java/src/main/res/mipmap-hdpi/octopus.webp b/android/llama.android/app-java/src/main/res/mipmap-hdpi/octopus.webp new file mode 100644 index 00000000..29daecf1 Binary files /dev/null and b/android/llama.android/app-java/src/main/res/mipmap-hdpi/octopus.webp differ diff --git a/android/llama.android/app-java/src/main/res/mipmap-hdpi/octopus_foreground.webp b/android/llama.android/app-java/src/main/res/mipmap-hdpi/octopus_foreground.webp new file mode 100644 index 00000000..88bf3149 Binary files /dev/null and b/android/llama.android/app-java/src/main/res/mipmap-hdpi/octopus_foreground.webp differ diff --git a/android/llama.android/app-java/src/main/res/mipmap-hdpi/octopus_round.webp b/android/llama.android/app-java/src/main/res/mipmap-hdpi/octopus_round.webp new file mode 100644 index 00000000..0883ba1c Binary files /dev/null and b/android/llama.android/app-java/src/main/res/mipmap-hdpi/octopus_round.webp differ diff --git a/android/llama.android/app-java/src/main/res/mipmap-mdpi/ic_launcher.png b/android/llama.android/app-java/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 00000000..b3990457 Binary files /dev/null and b/android/llama.android/app-java/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/android/llama.android/app-java/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/android/llama.android/app-java/src/main/res/mipmap-mdpi/ic_launcher_foreground.png new file mode 100644 index 00000000..b8a59f47 Binary files /dev/null and b/android/llama.android/app-java/src/main/res/mipmap-mdpi/ic_launcher_foreground.png differ diff --git a/android/llama.android/app-java/src/main/res/mipmap-mdpi/ic_launcher_round.png b/android/llama.android/app-java/src/main/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 00000000..75aec75a Binary files /dev/null and b/android/llama.android/app-java/src/main/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/android/llama.android/app-java/src/main/res/mipmap-mdpi/octopus.webp b/android/llama.android/app-java/src/main/res/mipmap-mdpi/octopus.webp new file mode 100644 index 00000000..c192866e Binary files /dev/null and b/android/llama.android/app-java/src/main/res/mipmap-mdpi/octopus.webp differ diff --git a/android/llama.android/app-java/src/main/res/mipmap-mdpi/octopus_foreground.webp b/android/llama.android/app-java/src/main/res/mipmap-mdpi/octopus_foreground.webp new file mode 100644 index 00000000..34251871 Binary files /dev/null and b/android/llama.android/app-java/src/main/res/mipmap-mdpi/octopus_foreground.webp differ diff --git a/android/llama.android/app-java/src/main/res/mipmap-mdpi/octopus_round.webp b/android/llama.android/app-java/src/main/res/mipmap-mdpi/octopus_round.webp new file mode 100644 index 00000000..edb57427 Binary files /dev/null and b/android/llama.android/app-java/src/main/res/mipmap-mdpi/octopus_round.webp differ diff --git a/android/llama.android/app-java/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/llama.android/app-java/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 00000000..a6324636 Binary files /dev/null and b/android/llama.android/app-java/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/android/llama.android/app-java/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/android/llama.android/app-java/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png new file mode 100644 index 00000000..98708fa5 Binary files /dev/null and b/android/llama.android/app-java/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png differ diff --git a/android/llama.android/app-java/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/android/llama.android/app-java/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 00000000..30de3067 Binary files /dev/null and b/android/llama.android/app-java/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/android/llama.android/app-java/src/main/res/mipmap-xhdpi/octopus.webp b/android/llama.android/app-java/src/main/res/mipmap-xhdpi/octopus.webp new file mode 100644 index 00000000..372b8bdb Binary files /dev/null and b/android/llama.android/app-java/src/main/res/mipmap-xhdpi/octopus.webp differ diff --git a/android/llama.android/app-java/src/main/res/mipmap-xhdpi/octopus_foreground.webp b/android/llama.android/app-java/src/main/res/mipmap-xhdpi/octopus_foreground.webp new file mode 100644 index 00000000..fcdd6ddf Binary files /dev/null and b/android/llama.android/app-java/src/main/res/mipmap-xhdpi/octopus_foreground.webp differ diff --git a/android/llama.android/app-java/src/main/res/mipmap-xhdpi/octopus_round.webp b/android/llama.android/app-java/src/main/res/mipmap-xhdpi/octopus_round.webp new file mode 100644 index 00000000..5b864a66 Binary files /dev/null and b/android/llama.android/app-java/src/main/res/mipmap-xhdpi/octopus_round.webp differ diff --git a/android/llama.android/app-java/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/llama.android/app-java/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 00000000..196c1ef5 Binary files /dev/null and b/android/llama.android/app-java/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/android/llama.android/app-java/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/android/llama.android/app-java/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png new file mode 100644 index 00000000..34fc4e7e Binary files /dev/null and b/android/llama.android/app-java/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png differ diff --git a/android/llama.android/app-java/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/android/llama.android/app-java/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 00000000..984bb8d9 Binary files /dev/null and b/android/llama.android/app-java/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/android/llama.android/app-java/src/main/res/mipmap-xxhdpi/octopus.webp b/android/llama.android/app-java/src/main/res/mipmap-xxhdpi/octopus.webp new file mode 100644 index 00000000..ad3daafc Binary files /dev/null and b/android/llama.android/app-java/src/main/res/mipmap-xxhdpi/octopus.webp differ diff --git a/android/llama.android/app-java/src/main/res/mipmap-xxhdpi/octopus_foreground.webp b/android/llama.android/app-java/src/main/res/mipmap-xxhdpi/octopus_foreground.webp new file mode 100644 index 00000000..ca878a67 Binary files /dev/null and b/android/llama.android/app-java/src/main/res/mipmap-xxhdpi/octopus_foreground.webp differ diff --git a/android/llama.android/app-java/src/main/res/mipmap-xxhdpi/octopus_round.webp b/android/llama.android/app-java/src/main/res/mipmap-xxhdpi/octopus_round.webp new file mode 100644 index 00000000..fd780d66 Binary files /dev/null and b/android/llama.android/app-java/src/main/res/mipmap-xxhdpi/octopus_round.webp differ diff --git a/android/llama.android/app-java/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/llama.android/app-java/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 00000000..1f10f330 Binary files /dev/null and b/android/llama.android/app-java/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/android/llama.android/app-java/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/android/llama.android/app-java/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png new file mode 100644 index 00000000..13f3147e Binary files /dev/null and b/android/llama.android/app-java/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png differ diff --git a/android/llama.android/app-java/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/android/llama.android/app-java/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 00000000..b81a70ba Binary files /dev/null and b/android/llama.android/app-java/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/android/llama.android/app-java/src/main/res/mipmap-xxxhdpi/octopus.webp b/android/llama.android/app-java/src/main/res/mipmap-xxxhdpi/octopus.webp new file mode 100644 index 00000000..ef8923ce Binary files /dev/null and b/android/llama.android/app-java/src/main/res/mipmap-xxxhdpi/octopus.webp differ diff --git a/android/llama.android/app-java/src/main/res/mipmap-xxxhdpi/octopus_foreground.webp b/android/llama.android/app-java/src/main/res/mipmap-xxxhdpi/octopus_foreground.webp new file mode 100644 index 00000000..e8b6489c Binary files /dev/null and b/android/llama.android/app-java/src/main/res/mipmap-xxxhdpi/octopus_foreground.webp differ diff --git a/android/llama.android/app-java/src/main/res/mipmap-xxxhdpi/octopus_round.webp b/android/llama.android/app-java/src/main/res/mipmap-xxxhdpi/octopus_round.webp new file mode 100644 index 00000000..d0b5881a Binary files /dev/null and b/android/llama.android/app-java/src/main/res/mipmap-xxxhdpi/octopus_round.webp differ diff --git a/android/llama.android/app-java/src/main/res/values-night/themes.xml b/android/llama.android/app-java/src/main/res/values-night/themes.xml new file mode 100644 index 00000000..2bd72d37 --- /dev/null +++ b/android/llama.android/app-java/src/main/res/values-night/themes.xml @@ -0,0 +1,11 @@ + + + + \ No newline at end of file diff --git a/android/llama.android/app-java/src/main/res/values/colors.xml b/android/llama.android/app-java/src/main/res/values/colors.xml new file mode 100644 index 00000000..b15af47b --- /dev/null +++ b/android/llama.android/app-java/src/main/res/values/colors.xml @@ -0,0 +1,17 @@ + + + #FF000000 + #FFFFFFFF + #813BBA + #FF202020 + #17CE92 + #E5E5E5 + #0A1528 + #313D50 + #03070D + #03070D + #03070D + #03070D + #FFFFFF + #B00020 + \ No newline at end of file diff --git a/android/llama.android/app-java/src/main/res/values/font_certs.xml b/android/llama.android/app-java/src/main/res/values/font_certs.xml new file mode 100644 index 00000000..d2226ac0 --- /dev/null +++ b/android/llama.android/app-java/src/main/res/values/font_certs.xml @@ -0,0 +1,17 @@ + + + + @array/com_google_android_gms_fonts_certs_dev + @array/com_google_android_gms_fonts_certs_prod + + + + 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 + + + diff --git a/android/llama.android/app-java/src/main/res/values/ic_launcher_background.xml b/android/llama.android/app-java/src/main/res/values/ic_launcher_background.xml new file mode 100644 index 00000000..c5d5899f --- /dev/null +++ b/android/llama.android/app-java/src/main/res/values/ic_launcher_background.xml @@ -0,0 +1,4 @@ + + + #FFFFFF + \ No newline at end of file diff --git a/android/llama.android/app-java/src/main/res/values/preloaded_fonts.xml b/android/llama.android/app-java/src/main/res/values/preloaded_fonts.xml new file mode 100644 index 00000000..56657f17 --- /dev/null +++ b/android/llama.android/app-java/src/main/res/values/preloaded_fonts.xml @@ -0,0 +1,6 @@ + + + + @font/alegreya_sans_sc_extrabold + + diff --git a/android/llama.android/app-java/src/main/res/values/strings.xml b/android/llama.android/app-java/src/main/res/values/strings.xml new file mode 100644 index 00000000..2ff67712 --- /dev/null +++ b/android/llama.android/app-java/src/main/res/values/strings.xml @@ -0,0 +1,4 @@ + + LayoutTest + User Message + \ No newline at end of file diff --git a/android/llama.android/app-java/src/main/res/values/styles.xml b/android/llama.android/app-java/src/main/res/values/styles.xml new file mode 100644 index 00000000..864fcf30 --- /dev/null +++ b/android/llama.android/app-java/src/main/res/values/styles.xml @@ -0,0 +1,16 @@ + + + + diff --git a/android/llama.android/app-java/src/main/res/values/themes.xml b/android/llama.android/app-java/src/main/res/values/themes.xml new file mode 100644 index 00000000..2ef46f0c --- /dev/null +++ b/android/llama.android/app-java/src/main/res/values/themes.xml @@ -0,0 +1,13 @@ + + + + +