AndroidでAndroid Neural Networks APIを使ってデバイスの情報を取得する

# 動機
世の中Deep Neural Networkばやりだが、クラウドのAPIを叩くのはデータが持っていかれそうで怖いので、Androidのデバイス内でできないのかと思っていたら、Androidに[Android Neural Networks API（NNAPI）](https://developer.android.com/ndk/guides/neuralnetworks)というものがあるというので、小手調べに、デバイスの情報を取り出してみた。
# 環境
<dl>
<dt>OS</dt><dd>Android 12</dd>
<dt>Hard</dt><dd>Sense 4</dd>
<dt>IDE</dt><dd>Android Studio Dolphin | 2021.3.1 Patch 1</dd>
</dl>

# 手順
1. AndroidStudio で Native C++ を選んでプロジェクトを作る

:::note info
参照:
* [NDKを使ってみる](https://developer.android.com/ndk/guides?hl=ja)
* [C / C++ のサポートを使用した新しいプロジェクトの作成](https://developer.android.com/studio/projects/add-native-code?hl=ja#new-project)
:::

2\. gradle の設定

```text:build.gradle(:app)
android {
...
   externalNativeBuild {
        cmake {
            path file('src/main/cpp/CMakeLists.txt')
            version '3.22.1'
        }
    }
    buildFeatures {
        viewBinding true
    }
    ndkVersion "25.1.8937393"
}
```

Native C++ を選ぶと自動的に生成されるが、必要なら`externalNativeBuild`と`ndkVersion`のバージョンを、使用するもの（ダウンロードしたもの）に合わせておく。
表示で利用するので、`viewBinding`を追記した。

3\. Device数の取得

Native C++でプロジェクトを生成すると、native-lib.cppというものが作成されるので、これを利用することにする。その前に、CMakeでのコンパイルのための情報である CMakeLists.txt でNNAPIを使うための宣言を追加する。

```text:CMakeLists.txt
target_link_libraries( # Specifies the target library.
        ...
        # Link with libneuralnetworks.so for NN API
        neuralnetworks
        android
        ...)
```

次が本題のNNAPIである。まず、簡単なところで、Device数を取得してみる。NNAPIの関数はアドレス渡しで引数に指定した変数に返り値を入れる仕様になっている。C++の長ったらしい関数名は native-lib.cpp に例が作成されていると思うので、それを参考に設定する。

```cpp:native-lib.cpp
#include <android/NeuralNetworks.h>

extern "C" JNIEXPORT jint JNICALL
Java_com_yoo6309_firstndkapplication_MainActivity_getDeviceCount(
        JNIEnv* env,
        jobject /* this */) {
    uint32_t numDevices;
    ANeuralNetworks_getDeviceCount(&numDevices);
    return (jint)numDevices;
}
```

Native C++ では [JNI](https://developer.android.com/training/articles/perf-jni?hl=ja) を使うことになっているので、NNAPIで取得したデバイス数である`numDevices`を、javaのint型に対応するjintに変換してjava側に返すことになる。
これを使うjava側のプログラムは以下のようになる。関数を`native`で宣言するだけ。楽勝である。

```java:MainActivity.java
int nDevice = getDeviceCount();
...
public native int getDeviceCount();
```

4\. デバイス情報を取得する

デバイス情報としては、[デバイス検出](https://developer.android.com/ndk/guides/neuralnetworks#device-discovery)に挙げられた以下の関数がある。

> ANeuralNetworksDevice_getFeatureLevel
> ANeuralNetworksDevice_getName
> ANeuralNetworksDevice_getType
> ANeuralNetworksDevice_getVersion

1つずつ取得するのはだるいので、クラスを使いたいところだ。C++側からクラスのインスタンスを生成して値を返す方法については、[夏休みの宿題は、JNI](https://kazuhira-r.hatenablog.com/entry/20110820/1313813194)を参考にした。[JNI](https://docs.oracle.com/en/java/javase/11/docs/specs/jni/)の勉強が必要だ。また、NNAPIの実際の使い方は、[TensorFlow](https://github.com/tensorflow/tensorflow)の[ソースコード](https://github.com/tensorflow/tensorflow/blob/master/tensorflow/lite/nnapi/nnapi_util.cc)を参考にした。
まず、返り値用のクラスをjava側で宣言する。

```java:MainActivity.java
    public static class NNDevice {
        long feature;
        String name;
        int type;
        String version;

        public NNDevice(long feature, String name, int type, String version) {
            this.feature = feature;
            this.name = name;
            this.type = type;
            this.version = version;
        }
    }
```

C++では、NNAPIの各関数で取得した値を、このクラスから生成したインスタンスの中に設定して返すことになる。FindClassのクラスの指定と、GetMethodIDでのmethod signatureの指定がわかりにくい。以下の例では、long, String, int, Stringの引数（入力）があり、出力がvoidという指定をしている。

```cpp:native-lib.cpp
extern "C" JNIEXPORT jobject JNICALL
Java_com_yoo6309_firstndkapplication_MainActivity_getDeviceParam(
        JNIEnv* env,
        jobject MainActivity /* this */,
        jint i) {
    ANeuralNetworksDevice* device;
    int64_t featureLevel;
    int32_t type;
    const char* buffer;
    int ans = ANeuralNetworks_getDevice((uint32_t)i, &device);
    ans = ANeuralNetworksDevice_getFeatureLevel(device, &featureLevel);
    ans = ANeuralNetworksDevice_getName(device, &buffer);
    jstring name = env->NewStringUTF(buffer);
    ans = ANeuralNetworksDevice_getType(device, &type);
    ans = ANeuralNetworksDevice_getVersion(device, &buffer);
    jstring version = env->NewStringUTF(buffer);
    jclass deviceClass = env->FindClass("com/yoo6309/firstndkapplication/MainActivity$NNDevice");
    jmethodID deviceConstructor;
    deviceConstructor = env->GetMethodID(deviceClass, "<init>",
                                         "(JLjava/lang/String;ILjava/lang/String;)V");
    jobject ret = env->NewObject(deviceClass,
                             deviceConstructor,
                             featureLevel,
                             name,
                             type,
                             version);
    return ret;
}
```

これを呼ぶjava側のプログラムは、以下のような感じ。

```
int i;
...
nnDevice = getDeviceParam(i);
...
public native NNDevice getDeviceParam(int i);
```

5\. ソース全体

```java:MainActivity.java
package com.yoo6309.firstndkapplication;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.widget.TextView;

import com.yoo6309.firstndkapplication.databinding.ActivityMainBinding;

import java.util.Locale;

public class MainActivity extends AppCompatActivity {

    // Used to load the 'firstndkapplication' library on application startup.
    static {
        System.loadLibrary("firstndkapplication");
    }

    private ActivityMainBinding binding;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        NNDevice nnDevice;

        binding = ActivityMainBinding.inflate(getLayoutInflater());
        setContentView(binding.getRoot());

        // Example of a call to a native method
        TextView tv = binding.sampleText;
        int nDevice = getDeviceCount();
        tv.setText(String.format(Locale.JAPAN, "Device Count: %d", nDevice));
        StringBuilder msg = new StringBuilder();
        for(int i = 0; i < nDevice; i++) {
            nnDevice = getDeviceParam(i);
            if (i > 0) msg.append("\n");
            msg.append("feature:").append(nnDevice.feature).append("\nname:").append(nnDevice.name).append("\ntype:").append(nnDevice.type).append("\nversion:").append(nnDevice.version);
        }
        binding.deviceParamText.setText(msg.toString());
    }

    public static class NNDevice {
        long feature;
        String name;
        int type;
        String version;

        public NNDevice(long feature, String name, int type, String version) {
            this.feature = feature;
            this.name = name;
            this.type = type;
            this.version = version;
        }
    }
    /**
     * A native method that is implemented by the 'firstndkapplication' native library,
     * which is packaged with this application.
     */
    public native int getDeviceCount();
    public native NNDevice getDeviceParam(int i);
}
```

```cpp:native-lib.cpp
#include <jni.h>
#include <string>
#include <android/NeuralNetworks.h>

extern "C" JNIEXPORT jint JNICALL
Java_com_yoo6309_firstndkapplication_MainActivity_getDeviceCount(
        JNIEnv* env,
        jobject /* this */) {
    uint32_t numDevices;
    ANeuralNetworks_getDeviceCount(&numDevices);
    return (jint)numDevices;
}

extern "C" JNIEXPORT jobject JNICALL
Java_com_yoo6309_firstndkapplication_MainActivity_getDeviceParam(
        JNIEnv* env,
        jobject MainActivity /* this */,
        jint i) {
    ANeuralNetworksDevice* device;
    int64_t featureLevel;
    int32_t type;
    const char* buffer;
    int ans = ANeuralNetworks_getDevice((uint32_t)i, &device);
    ans = ANeuralNetworksDevice_getFeatureLevel(device, &featureLevel);
    ans = ANeuralNetworksDevice_getName(device, &buffer);
    jstring name = env->NewStringUTF(buffer);
    ans = ANeuralNetworksDevice_getType(device, &type);
    ans = ANeuralNetworksDevice_getVersion(device, &buffer);
    jstring version = env->NewStringUTF(buffer);
    jclass deviceClass = env->FindClass("com/yoo6309/firstndkapplication/MainActivity$NNDevice");
    jmethodID deviceConstructor;
    deviceConstructor = env->GetMethodID(deviceClass, "<init>",
                                         "(JLjava/lang/String;ILjava/lang/String;)V");
    jobject ret = env->NewObject(deviceClass,
                             deviceConstructor,
                             featureLevel,
                             name,
                             type,
                             version);
    return ret;
}
```

```xml:activity_main.xml
<?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:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/sample_text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="device count"
        android:textAlignment="gravity"
        app:layout_constraintBottom_toTopOf="@+id/device_param_text"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/device_param_text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="device param"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="@id/sample_text" />

</androidx.constraintlayout.widget.ConstraintLayout>
```

6\. 実行

これをsense 4で実行したところ、以下のような出力を得た。[Qualcomm](https://www.qualcomm.com/products/application/smartphones/snapdragon-7-series-mobile-platforms/snapdragon-720g-mobile-platform)によれば、gpuはAdreno 618、htaは、"Qualcomm® Hexagon™ Tensor Accelerator"というものらしい。

![Screenshot_20230122-182443.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2180229/a76dc381-0b69-c3bf-8a7e-47c8690859a3.png)