Skip to content

Commit 12d7d06

Browse files
committed
Updates android example.
1 parent 249b205 commit 12d7d06

File tree

128 files changed

+3751
-1124
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

128 files changed

+3751
-1124
lines changed

examples/llama.android/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
.gradle/
33
build/
44

5+
.idea
6+
57
# Local configuration file (sdk path, etc)
68
local.properties
79

examples/llama.android/README.md

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# Nexa
2+
3+
**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.
4+
**NOTE:** Currently, Nexa supports Vision-Language Model (VLM) inference capabilities.
5+
6+
## Installation
7+
8+
To add Nexa to your Android project, follow these steps:
9+
10+
- Create a libs folder in your project’s root directory.
11+
- Copy the .aar file into the libs folder.
12+
- Add dependency to your build.gradle file:
13+
14+
```
15+
implementation files("libs/com.nexa.aar")
16+
```
17+
18+
## Usage
19+
### 1. Initialize NexaSwift with model path and projector path
20+
21+
Create a configuration and initialize NexaSwift with the path to your model file:
22+
23+
```kotlin
24+
nexaVlmInference = NexaVlmInference(pathToModel,
25+
mmprojectorPath, imagePath,
26+
maxNewTokens = 128,
27+
stopWords = listOf("</s>"))
28+
nexaVlmInference.loadModel()
29+
```
30+
31+
### 2. Completion API
32+
33+
#### Streaming Mode
34+
35+
```swift
36+
nexaVlmInference.createCompletionStream(prompt, imagePath)
37+
?.catch {
38+
print(it.message)
39+
}
40+
?.collect { print(it) }
41+
```
42+
43+
### 3. release all resources
44+
```kotlin
45+
nexaVlmInference.dispose()
46+
```
47+
48+
## Quick Start
49+
50+
Open the [android test project](./app-java) folder in Android Studio and run the project.
51+
52+
## Download Models
53+
54+
You can download models from the [Nexa AI ModelHub](https://nexa.ai/models).
55+
56+
## How to estimate power usage
57+
58+
- ```adb shell dumpsys batterystats --reset```
59+
- ```adb shell dumpsys batterystats > batterystats.txt```
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
/build
2+
!*.png
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
plugins {
2+
id 'com.android.application'
3+
id 'kotlin-android'
4+
}
5+
6+
android {
7+
namespace 'ai.nexa.app_java'
8+
compileSdk 34
9+
10+
defaultConfig {
11+
applicationId "ai.nexa.app_java"
12+
minSdk 33
13+
targetSdk 34
14+
versionCode 1
15+
versionName "1.0"
16+
17+
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
18+
}
19+
20+
buildTypes {
21+
release {
22+
minifyEnabled false
23+
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
24+
}
25+
}
26+
compileOptions {
27+
sourceCompatibility JavaVersion.VERSION_17 // or VERSION_1_8
28+
targetCompatibility JavaVersion.VERSION_17 // or VERSION_1_8
29+
}
30+
31+
kotlinOptions {
32+
jvmTarget = "17" // or "1.8"
33+
}
34+
}
35+
36+
dependencies {
37+
38+
implementation 'androidx.appcompat:appcompat:1.7.0'
39+
implementation 'com.google.android.material:material:1.12.0'
40+
testImplementation 'junit:junit:4.13.2'
41+
androidTestImplementation 'androidx.test.ext:junit:1.2.1'
42+
androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1'
43+
44+
implementation "org.jetbrains.kotlin:kotlin-stdlib:1.9.20"
45+
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3"
46+
47+
implementation 'com.github.bumptech.glide:glide:4.16.0'
48+
annotationProcessor 'com.github.bumptech.glide:compiler:4.16.0'
49+
50+
implementation project(":llama")
51+
// implementation files("libs/com.nexa.aar")
52+
}

examples/llama.android/app/proguard-rules.pro renamed to examples/llama.android/app-java/proguard-rules.pro

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,4 @@
1818

1919
# If you keep the line number information, uncomment this to
2020
# hide the original source file name.
21-
#-renamesourcefileattribute SourceFile
21+
#-renamesourcefileattribute SourceFile
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package ai.nexa.app_java;
2+
3+
import android.content.Context;
4+
5+
import androidx.test.platform.app.InstrumentationRegistry;
6+
import androidx.test.ext.junit.runners.AndroidJUnit4;
7+
8+
import org.junit.Test;
9+
import org.junit.runner.RunWith;
10+
11+
import static org.junit.Assert.*;
12+
13+
/**
14+
* Instrumented test, which will execute on an Android device.
15+
*
16+
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
17+
*/
18+
@RunWith(AndroidJUnit4.class)
19+
public class ExampleInstrumentedTest {
20+
@Test
21+
public void useAppContext() {
22+
// Context of the app under test.
23+
Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
24+
assertEquals("ai.nexa.app_java", appContext.getPackageName());
25+
}
26+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
3+
xmlns:tools="http://schemas.android.com/tools">
4+
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
5+
<uses-permission android:name="android.permission.INTERNET" />
6+
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
7+
<uses-permission android:name="android.permission.SEND_SMS"/>
8+
<uses-permission android:name="android.permission.CAMERA"/>
9+
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
10+
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
11+
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
12+
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
13+
14+
<application
15+
android:largeHeap="true"
16+
android:hardwareAccelerated="true"
17+
tools:replace="android:largeHeap"
18+
android:allowBackup="true"
19+
android:dataExtractionRules="@xml/data_extraction_rules"
20+
android:fullBackupContent="@xml/backup_rules"
21+
android:icon="@mipmap/octopus"
22+
android:label="VlmSDK"
23+
android:roundIcon="@mipmap/octopus_round"
24+
android:supportsRtl="true"
25+
android:theme="@style/Theme.LayoutTest"
26+
android:networkSecurityConfig="@xml/network_security_config">
27+
<activity
28+
android:name=".MainActivity"
29+
android:exported="true"
30+
android:windowSoftInputMode="adjustResize">>
31+
<intent-filter>
32+
<action android:name="android.intent.action.MAIN" />
33+
34+
<category android:name="android.intent.category.LAUNCHER" />
35+
</intent-filter>
36+
</activity>
37+
<meta-data
38+
android:name="preloaded_fonts"
39+
android:resource="@array/preloaded_fonts" />
40+
41+
<provider
42+
android:name="androidx.core.content.FileProvider"
43+
android:authorities="${applicationId}.fileprovider"
44+
android:exported="false"
45+
android:grantUriPermissions="true">
46+
<meta-data
47+
android:name="android.support.FILE_PROVIDER_PATHS"
48+
android:resource="@xml/file_paths" />
49+
</provider>
50+
</application>
51+
52+
</manifest>
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
package ai.nexa.app_java;
2+
3+
import android.content.Context;
4+
import android.database.Cursor;
5+
import android.net.Uri;
6+
import android.provider.DocumentsContract;
7+
import android.provider.MediaStore;
8+
import android.util.Log;
9+
10+
import java.io.File;
11+
import java.io.FileOutputStream;
12+
import java.io.IOException;
13+
import java.io.InputStream;
14+
import java.io.OutputStream;
15+
16+
public class ImagePathHelper {
17+
private static final String TAG = "MessageProcessor";
18+
private final Context context;
19+
20+
public ImagePathHelper(Context context) {
21+
this.context = context;
22+
}
23+
24+
public String getPathFromUri(String uriString) {
25+
try {
26+
Uri uri = Uri.parse(uriString);
27+
28+
// Handle "content://" scheme
29+
if ("content".equals(uri.getScheme())) {
30+
// Handle Google Photos and other document providers
31+
if (DocumentsContract.isDocumentUri(context, uri)) {
32+
final String docId = DocumentsContract.getDocumentId(uri);
33+
34+
// MediaStore documents
35+
if ("com.android.providers.media.documents".equals(uri.getAuthority())) {
36+
final String[] split = docId.split(":");
37+
final String type = split[0];
38+
Uri contentUri = null;
39+
40+
if ("image".equals(type)) {
41+
contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
42+
}
43+
44+
final String selection = "_id=?";
45+
final String[] selectionArgs = new String[]{split[1]};
46+
return getDataColumn(context, contentUri, selection, selectionArgs);
47+
}
48+
}
49+
// MediaStore (general case)
50+
return getDataColumn(context, uri, null, null);
51+
}
52+
// Handle "file://" scheme
53+
else if ("file".equals(uri.getScheme())) {
54+
return uri.getPath();
55+
}
56+
// Handle absolute path
57+
else if (new File(uriString).exists()) {
58+
return uriString;
59+
}
60+
61+
return null;
62+
} catch (Exception e) {
63+
Log.e(TAG, "Error getting path from URI: " + uriString, e);
64+
return null;
65+
}
66+
}
67+
68+
public String copyUriToPrivateFile(Context context, String uriString) throws IOException {
69+
// 将字符串转换回 Uri
70+
Uri uri = Uri.parse(uriString);
71+
72+
// 应用私有目录
73+
File privateDir = context.getExternalFilesDir("images");
74+
if (privateDir == null) {
75+
throw new IOException("Private directory not available");
76+
}
77+
78+
// 创建目标文件
79+
File destFile = new File(privateDir, "temp_image_" + System.currentTimeMillis() + ".jpg");
80+
81+
try (InputStream inputStream = context.getContentResolver().openInputStream(uri);
82+
OutputStream outputStream = new FileOutputStream(destFile)) {
83+
84+
if (inputStream == null) {
85+
throw new IOException("Failed to open URI input stream");
86+
}
87+
88+
// 读取并写入数据
89+
byte[] buffer = new byte[4096];
90+
int bytesRead;
91+
while ((bytesRead = inputStream.read(buffer)) != -1) {
92+
outputStream.write(buffer, 0, bytesRead);
93+
}
94+
}
95+
96+
// 返回文件路径
97+
return destFile.getAbsolutePath();
98+
}
99+
100+
private String getDataColumn(Context context, Uri uri, String selection, String[] selectionArgs) {
101+
final String[] projection = {MediaStore.Images.Media.DATA};
102+
try (Cursor cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs, null)) {
103+
if (cursor != null && cursor.moveToFirst()) {
104+
final int columnIndex = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA);
105+
return cursor.getString(columnIndex);
106+
}
107+
} catch (Exception e) {
108+
Log.e(TAG, "Error getting data column", e);
109+
}
110+
return null;
111+
}
112+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package ai.nexa.app_java
2+
3+
import kotlinx.coroutines.CoroutineScope
4+
import kotlinx.coroutines.Dispatchers
5+
import kotlinx.coroutines.cancelChildren
6+
import kotlinx.coroutines.flow.Flow
7+
import kotlinx.coroutines.launch
8+
import kotlinx.coroutines.withContext
9+
10+
class KotlinFlowHelper {
11+
private val scope = CoroutineScope(Dispatchers.IO)
12+
13+
fun collectFlow(
14+
flow: Flow<String>, // Added missing flow parameter
15+
onToken: (String) -> Unit,
16+
onComplete: (String) -> Unit,
17+
onError: (String) -> Unit
18+
) {
19+
scope.launch {
20+
try {
21+
val fullResponse = StringBuilder()
22+
withContext(Dispatchers.IO) {
23+
flow.collect { value ->
24+
fullResponse.append(value)
25+
withContext(Dispatchers.Main) {
26+
onToken(value)
27+
}
28+
}
29+
}
30+
withContext(Dispatchers.Main) {
31+
onComplete(fullResponse.toString())
32+
}
33+
} catch (e: Exception) {
34+
withContext(Dispatchers.Main) {
35+
onError(e.message ?: "Unknown error")
36+
}
37+
}
38+
}
39+
}
40+
41+
fun cancel() {
42+
scope.coroutineContext.cancelChildren()
43+
}
44+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package ai.nexa.app_java
2+
3+
import java.util.function.Consumer
4+
5+
object KotlinJavaUtils {
6+
@JvmStatic
7+
fun toKotlinCallback(callback: Consumer<String>): (String) -> Unit = { value ->
8+
callback.accept(value)
9+
Unit
10+
}
11+
}

0 commit comments

Comments
 (0)