---
# System prepended metadata

title: 驅動測試 - Android 模擬器系統、NDK
tags: [設備開發, Android HAL]

---

---
title: '驅動測試 - Android 模擬器系統、NDK'
disqus: kyleAlien
---

驅動測試 - Android 模擬器系統、NDK
===

## Overview of Content

如有引用參考請詳註出處，感謝 :cat:

在這章，我們要在 Android 模擬器上通過 Native C 程序測試 Linux 驅動
[TOC]

## Android 模擬器內核

在編譯 Android 模擬器驅動時，**必須選擇與 ++當前運行的 Android 模擬器內核版本++ 相同的 ++Android 內核++ 進行編譯** ，否則會無法安裝 Android 驅動

> 也就是說 Android 內核用哪個版本，我們的設備驅動程式就要用對應的版本編譯，才能正確安裝應用

```mermaid
graph LR;

Android_驅動 --> |在 Android_kernel_2.3.4 下編譯| ko_檔

ko_檔 --> |ok| Android_裝置_A
ko_檔 -.-> |fail| Android_裝置_B

subgraph Android_裝置_B
Android_kernel_5.5.4
end

subgraph Android_裝置_A
Android_kernel_2.3.4
end
```

```shell=
# 查看當前模擬器的內核版本

adb shell cat /proc/version
```
> 可以看到當前內核版本是 `Linux version 2.6.29-g46b05b2`
> 
> ![image](https://hackmd.io/_uploads/HJugz7DLT.png)

## 源碼 & 核心 - 設置、編譯

以下分別先來編譯 Android 源碼、Android 核心

* 以下使用 ^1.^ Android 內核 `android-goldfish-2.6.29`

    > ![image](https://hackmd.io/_uploads/B1meXQw8a.png)

* ^2.^ Android 源碼為 `android-4.0.3_r1.1`

    > ![image](https://hackmd.io/_uploads/Sy5vX7PIa.png)
    > 
### Android 內核 - 設置 .config

* **接下來目標是要在 Android 裝置上手動安裝驅動**，但在這之前，需要一些設置
    
    1. **複製預設 `.config` 檔案**：每個核心都有一些預設的 `config` 檔案

        ```shell=
        export ARCH=arm
        
        export SUBARCH=arm

        make goldfish_armv7_defconfig
        ```
        
        :::info
        * **可以嘗試手動將檔案複製到根目錄**
            
            我們可以透過複製這些檔案到內核的根目錄之下，並重新改名為 `.config` 檔案（就可以省略一些細節設置）

            ```shell=
            cp arch/arm/configs/goldfish_defconfig .

            mv goldfish_defconfig .config
            ```
        
        * **可以看到該核心支援的架為有如下圖幾種，像是 `arm`、`x86`… 等等**

            > ![image](https://hackmd.io/_uploads/HkS-w7wUp.png)

            而個別架構資料夾之下的 `configs` 資料夾中，又可以看到預設的 config 檔，其中就有 `goldfish_defconfig`

            > ![image](https://hackmd.io/_uploads/r1YqPmw86.png)
        :::


    2. **設置 `.config` 為動態加載 ko 檔**：
        
        用於 Android 模擬器的 **goldfish 內核預設 ++不允許++** 動態加載 Linux 驅動模塊，因此需要，在編譯 Linux 內核之前 **進行 `make menuconfig` 配置**
        
        > 代表重新編譯後的 `zImage` 內核可以動態加載、卸載 Linux 驅動模塊
        
        進入 Kernel 根目錄，並運行 `make menuconfig` 命令
        
        ```shell=
        make menuconfig
        ```
        
        * 配置 **設定 `Enable loadable module support` 選項**（按下空白)

            > ![image](https://hackmd.io/_uploads/ByW4dmPUp.png)

        * 按下 `Enter` 後進入子選項，選擇如下圖的項目（`Forced module loading`、`Module unloading`、`Forced module unloading` 這三項）
        
            > ![image](https://hackmd.io/_uploads/SksFdmDIT.png)
        
        * 一直按 `Exit` 退出，直到最後選擇 `save configuration` 為 `Yes`
            
            >　![image](https://hackmd.io/_uploads/r1dpOQwLp.png)

            

### Android 內核 - 編譯產出 zImage 

* 進行 Android 內核編譯
    
    1. **下載預編譯 toolchain**
        
        有正確的 toolchain 才可以編譯 Android 核心源碼，而不同的核心會需要不同的 toolchain… 目前使用的是 `arm-eabi-4.6`（如下）
        
        > ![image](https://hackmd.io/_uploads/BJ0jcmDIp.png)
        
        ```shell=
        # 下載 arm-eabi-4.6
        git clone https://android.googlesource.com/platform/prebuilts/gcc/linux-x86/arm/arm-eabi-4.6
        
        cd arm-eabi-4.6
        
        # 切換分支
        git checkout -b tools_r21 origin/tools_r21
        ```
        > ![image](https://hackmd.io/_uploads/BJ6WnQDLT.png)


    
    2. **設置 toolchain 路徑** 到臨時 PATH 路徑

        ```shell=
        # 目的是將 toolchain(剛剛下載的 arm-eabi-4.6/bin) 加入 PATH
        # 記得使用 export

        export PATH=$PATH:$PWD/arm-eabi-4.6/bin
        ```

        > ![image](https://hackmd.io/_uploads/rkDJaQwUa.png)
        
    3. **設置環境變數 `ARCH`、`SUBARCH`、`CROSS_COMPILE`**
        
        目的是為了指定目標設備的體系架構和交叉編譯器，以下指定 `arm` 架構、指定預編譯的前置 `arm-eabi-`
        
        ```shell=
        # 進入源碼目錄
        cd android_goldfish/
        
        # 設備的體系架構
        export ARCH=arm  
        export SUBARCH=arm
        
        # 預編譯的前置名
        export CROSS_COMPILE=arm-eabi- 
        ```

    4. **源碼根目錄執行 `make` 編譯內核**

        ```shell=
        make clean
        
        make -j$(nproc)
        ```
        > 最終會產出 `zImage`
        > 
        > ![image](https://hackmd.io/_uploads/BJ0m1ED86.png)


### Android 源碼 - 下載、編譯、運行模擬器

* **下載 Android 源碼**：
    
    使用 **Android 源碼 `android-4.0.3_r1.1`**，使用 Android 提供的 repo 工具下載 Android source code

    1. repo 初始化、並切換到 `android-4.0.3_r1.1` 分支

        ```shell=
        repo init -u https://android.googlesource.com/platform/manifest \
            -b android-4.0.3_r1.1
        ```

    2. repo 同步（下載）

        ```shell=
        repo sync -j$(nproc)
        ```

* **編譯 Android 源碼**：

    1. **初始化環境**：在 Android 下載好的根資料夾下達以下命令

        ```shell=
        ## 移動到下載的資料夾內
        cd ~/android_source

        source ./build/envsetup.sh

        make clobber    ## 編譯前清除 build 資料夾
        ```

    2. **lunch 選擇編譯目標**：選擇要編譯的目標，**格式組成是 `BUILD`、`BUILDTYPE`**

        * Build 是指編譯完後要運行的平台

            | BUILD | 目標設備 | 補充 |
            | -------- | -------- | -------- |
            | `full` | 模擬器 | 全編譯，包含所有語言、應用、輸入法... 等等 |
            | `full_manguro` | manguro | 全編譯，運行於 Galaxy Nexus GSM/HSPA+("manguro") |
            | `full_panda` | panda | 全編譯，運行於 PandaBoard("panda") |

        * BUILDTYPE 則分為三種類型

            | BUILDTYPE | 功能特點 |
            | -------- | -------- |
            | `user` | 發佈到市場上的版本，**預設沒有 root 權限、adb 關閉** |
            | `userdebug` | **開放 root、adb 權限**，一般用於真機調適 |
            | `eng` | 擁有最大權限 (**root**)，並具有額外調適工具，**一般用於模擬器** |


            > 有再細分為 `x86`、`x86_64`... 等等
            ```shell=
            # 查看所有可編譯的版本類型
            lunch 

            lunch 39    # 選擇 `aosp_x86-eng` 版本
            ```

            > ![image](https://hackmd.io/_uploads/r1Ao9_u8p.png)

    3. `make` 編譯：可以按照自己電腦的 CPU 核心數量來編譯

        ```shell=
        make -j$(nproc)
        ```
        > ![](https://i.imgur.com/jnWmxwD.png)


* Android 源碼經過編譯過後，就可以直接使用 `emulator` 命令，並啟動該源碼對應的 Android 模擬器

    ```shell=
    # emulator 命令的位置 `out/host/linux-x86/bin/emulator`
    which emulator
    
    # 查看檔案類型
    file $(which emulator)
    
    # 運行模擬器
    emulator
    ```

    > ![image](https://hackmd.io/_uploads/SyFujOdIa.png)

    Android 4 模擬器樣式
    
    > ![image](https://hackmd.io/_uploads/Bk5g2duLa.png)

    
## Android 模擬器驅動

Android 模擬器上不僅可以使用 Linux 命令測試驅動，也可以像 Ubuntu Linux 一樣，可以使用 C/C++ 程式進行測試；但有 **前提**，前提如下

* Android 模擬器、開發版、手機... **具有 root 權限**

* 可執行文件需要進行 **交叉編譯**，而這要注意到 Android 核心的版本、編譯目標是 `ARM` 架構

### Android 驅動程式

* 驅動程式的範例代碼如下

    功能是用來紀錄輸入到驅動中的單字數量（這邊不特別說明驅動… 驅動的詳解請看 [**Linux 設備開發 - 概述、編譯、安裝、HelloWrold**](https://hackmd.io/SYKjLpF7Q4qLwytxeFO2FQ)）

    ```c=
    #include <linux/module.h>
    #include <linux/init.h>
    #include <linux/kernel.h>
    #include <linux/fs.h>
    #include <linux/miscdevice.h>
    #include <asm/uaccess.h>

    #define TRUE	1
    #define FALSE	0
    #define DEVICE_NAME "hello_world"

    static unsigned char mem[10000];
    static int word_count = 0;

    static int is_space_char(char c) {
        if(c == ' ' || c == 9 || c == 13 || c== 10) {
            return TRUE;
        } else {
            return FALSE;
        }
    }

    static int get_word_count(const char *buf) {
        if(*buf == '\0') {
            return 0;	
        }

        int count = 1;

        if(is_space_char(*buf) == TRUE) {
            count--;
        }

        int index = 0;
        int is_space = 0;
        char curChar = ' ';	

        for(; (curChar = *(buf + index)) != '\0'; index++) {
            if(is_space == 1 && is_space_char(curChar) == FALSE) {
                is_space = 0;
            }
            else if(is_space == 1 && is_space_char(curChar) == TRUE) {
                continue;
            }

            if(is_space_char(curChar) == TRUE) {
                count++;
                is_space = 1;
            }
        }

        if(is_space_char(*(buf + index - 1)) == TRUE) {
            count--;
        }

        return count;
    }

    static ssize_t word_count_read(struct file *file, 
                    char __user *buf, 
                    size_t count, 
                    loff_t *ppos) {
        unsigned char tmp[4];

        tmp[0] = word_count >> 24;
        printk("--- tmp[0]: %d\n", (int) tmp[0]);

        tmp[1] = word_count >> 16;
        printk("--- tmp[1]: %d\n", (int) tmp[1]);

        tmp[2] = word_count >> 8;
        printk("--- tmp[2]: %d\n", (int) tmp[2]);

        tmp[3] = word_count;
        printk("--- tmp[3]: %d\n", (int) tmp[3]);

        copy_to_user(buf, (void*) tmp, 4);
        printk("--- count: %d\n", (int) count);
        printk("--- read word count: %d\n", (int) word_count);

        return count;
    }

    static ssize_t word_count_write(struct file *file, 
                    const char __user *buf, 
                    size_t count, 
                    loff_t *ppos) {
        copy_from_user(mem, buf, count);
        mem[count] = '\0';

        word_count = get_word_count(mem);	

        printk("--- count: %d\n", (int) count);
        printk("--- write word count: %d\n", (int) word_count);

        return count;
    }

    static struct file_operations dev_fops = {
        .owner = THIS_MODULE,
        .read = word_count_read,
        .write = word_count_write
    };

    static struct miscdevice misc = {
        .name = DEVICE_NAME,
        .minor = MISC_DYNAMIC_MINOR,
        .fops = &dev_fops
    };

    static int hello_world_init(void) {
        int ret;
        ret = misc_register(&misc);

        printk("--- hello_world_init_success, ret = %d.\n", ret);

        return ret;
    }

    static void hello_world_exit(void) {
        misc_deregister(&misc);

        printk("--- hello_world_exit_success.\n");
    }

    module_init(hello_world_init);
    module_exit(hello_world_exit);

    MODULE_AUTHOR("Hello author");
    MODULE_DESCRIPTION("Hello world device");
    MODULE_ALIAS("Hello world module.");
    MODULE_LICENSE("GPL");
    ```
    
### 編譯 Android 驅動 - ko 檔

* 要將 ko 驅檔動態安裝到 Android 系統中，需要設置、重新編譯 Android 內核，使其可以動態加載驅動檔（上面小節以說明）

* 要編譯 Android 系統可安裝的 ko 檔，使用 make 指令時必須指定 `-C` 選項

    > make 指令中的 `-C` 選項用於指定在讀取 makefile 檔案或執行其他操作之 **前先切換到指定的目錄**
    > 
    > 而這裡我們要先切換到 Android Kernel 的目錄（目前指定的版本是 `goldfish-2.6.29` 版本）

    ```shell=
    # 先切換到 Android kernel 目錄，
    cd android_goldfish/
    
    # 自動取用 Android kernel 目錄來編譯，我們的目標 ko 檔
    make -C $PWD M=/home/kyle/Desktop/drivers/4_android/
    ```

    > 最終編譯結果… 可以得到 ko 驅動檔（之後可以用來動態加載到 Android 系統）
    > 
    > ![image](https://hackmd.io/_uploads/Sko6LNwLT.png)


### Android 模擬器 - 動態安裝驅動

> 在看這個小節之前請先確保 ^1.^ Android source 已編譯、^2.^ Android Kernel 已編譯（可動態安裝驅動的版本）
> 
> 運行模擬器時，使用自己編譯的核心；切換到 About phone 可以看到核心版本為自己設置的版本    
    > ![image](https://hackmd.io/_uploads/rkMMFppIT.png)

1. **運行可動態安裝驅動的 Android 內核**：
    
    經過編譯過後的 Android source 可以直接 **運行 `emulator` 指令**… 我們只需要 **使用 `-kernel` 指定編譯過後的 `zImage` 檔**

    ```shell=
    emulator -kernel ../android_kernel/goldfish/android_goldfish/arch/arm/boot/zImage
    ```
    
    可以透過 adb 工具進入 android 模擬器中查看，是否已經運行指定的核心
    
    ```shell=
    adb shell
    
    cat /proc/version
    ```

    > ![image](https://hackmd.io/_uploads/Bkg5Y4wIT.png)

2. **將 `ko` 檔丟入模擬器**：

    ```shell=
    adb push hello_world.ko /data/local/
    ```
    > ![image](https://hackmd.io/_uploads/SJp95Ew8T.png)

3. **使用 `insmod` 指令安裝驅動**：

    ```shell=
    # 動態安裝驅動 ko 檔
    adb shell insmod /data/local/hello_world.ko
    
    # 進入 adb
    adb shell
    
    # 查看 Android 系統的 /dev 目錄，並找到 hello_world 裝置
    ls -laF /dev
    ```
    > ![image](https://hackmd.io/_uploads/rJA6oVw86.png)

4. **對驅動輸入數據，並讀取**：

    ```shell=
    # 在 adb 內
    
    echo "1 2 3 Hello world" > /dev/hello_world
    
    cat /dev/hello_world
    ```
    > ![image](https://hackmd.io/_uploads/BJMn3VDLa.png)


### 測試 Android 驅動 - 程式

* 接下來是測試 Android 驅動（上面範例的 `hello_world` 驅動）的範例程式
    
    以下檔案是用來讀取，輸入到 `hello_world` 驅動中的單字數量
    
    ```c=
    #include <stdio.h>
    #include <fcntl.h>
    #include <unistd.h>
    #include <stdlib.h>
    #include <string.h>

    int main(int argc, char *argv[]) {

        int testDev;
        unsigned char buf[4];

        testDev = open("/dev/hello_world", O_RDWR);

        if(testDev == -1) {
            printf("Cannot open hello_world file, did you insmod?\n");
            return -1;
        }

        if(argc > 1) {
            char* str = argv[1];		

            write(testDev, str, strlen(str));

            printf("The string (%s) been written.", str);
        }

        read(testDev, buf, 4);

        int word_count = ((int) buf[0] << 24 | 
            (int) buf[1] << 16 |
            (int) buf[2] << 8 |
            (int) buf[3]
        );

        printf("word byte display: %d, %d, %d, %d\n", buf[0], buf[1], buf[2], buf[4]);

        printf("word count: %d\n", word_count);	

        close(testDev);

        return 0;

    }
    ```


### 測試 Android 驅動 - 交叉編譯、運行 可執行檔

* **交叉編譯 Linux 程式**：需要的檔案、步驟如下

    1. **編寫 `Android.mk` 檔**（編譯規則）
    
        :::warning
        * **`Android.mk` 檔**

            以往我們在編譯程式時會撰寫 `Makefile` 檔案，來設定編譯規則、目標；而 Android 中的編譯文件則不使用 `Makefile`

            > Android 內的編譯規則檔案是 `Android.mk` 檔
        :::
        
        ```shell=
        LOCAL_PATH := $(call my-dir)
        include $(CLEAR_VARS)
        
        # 指定要編譯的文件
        LOCAL_SRC_FILES := hello_world_test.c
        
        # 指定模塊名稱，也是編譯後生成的可執行文件名
        LOCAL_MODULE := hello_world_test
        
        # 設定在哪個模式下編譯，optional 代表任何模式
        LOCAL_MODULE_TAGS := optional
        
        # 建立的目標
        include $(BUILD_EXECUTABLE)

        ```
        
        其中 `include $(BUILD_EXECUTABLE)` 是編譯為可執行文件
        
        | 變量名稱 | 說明 | 補充 |
        | -------- | -------- | - |
        | `BUILD_SHARED_LIBRARY` | 編譯成 so 文件 | 可執行文件的建立路徑則是 `<Android source>/out/target/product/generic/system/lib/<驅動名>.so` |
        | `BUILD_EXTCUTEABLE` | 編譯為可執行模塊 | 可執行文件的建立路徑則是 `<Android source>/out/target/product/generic/system/bin/<驅動名>` |

    
    2. **鏈結到 Android Source 編譯**：
        
        接著為了 **讓 `.c` 程式可以編譯，並運行在 Android 模擬器上**；我們可以有以下兩種方案
        
        * 將 **程式源碼複製** 到 `<Android 源碼目錄>` 下的子目錄
        
        * 也可以在 `<Android 源碼目錄>` 下，建立一個 **符號連結**

            ```shell=
            # 將目錄連結到 android source 的 development 資料夾下

            ln -s ~/Desktop/drivers/hello_world_test_android_emulator/ $PWD/development/test_hello_world
            ```
            
            最終這些檔案會編譯到 

            
            >　![image](https://hackmd.io/_uploads/BkLyZHD8T.png)

    3. **透過 Android source 編譯測試驅動的可執行檔案**

        ```shell=
        mmm development/hello_world_test
        ```
        
        在圖中可以看到，最終可執行檔輸出到 `<Android source>/out/target/product/generic/system/bin/` 資料夾中（`hello_world_test` 為最終的可執行檔）
        
        > ![image](https://hackmd.io/_uploads/H13ifrD86.png)


* **運行可執行檔 `hello_world_test`**

    ```shell=
    # 將可執行檔放入模擬器中
    adb push out/target/product/generic/system/bin/hello_world_test /data/local

    adb shell 

    ## -------------------------- 進入模擬器 shell
    cd /data/local

    ls -l

    # 執行驅動測試檔（可執行檔）
    ./hello_world_test "1 2 3 hello World good night"
    ```
    
    從結果可以看出可執行檔會運算輸入到驅動中的單字數量

    > ![image](https://hackmd.io/_uploads/HJSb4HPIp.png)


## Android NDK 測試驅動

看這個章節的前提是

1. 啟動模擬器，並指定可以動態安裝驅動（`ko` 檔）的模擬器
    
    ```shell=
    emulator -kernel ../android_kernel/goldfish/android_goldfish/arch/arm/boot/zImage -partition-size 2000
    ```

2. 測試前先手動使用 `insmod` 指令安裝驅動

    ```shell=
    # 動態安裝驅動 ko 檔
    adb shell insmod /data/local/hello_world.ko
    
    # 進入 adb
    adb shell
    
    # 查看 Android 系統的 /dev 目錄，並找到 hello_world 裝置
    ls -laF /dev
    ```
    > ![image](https://hackmd.io/_uploads/rJA6oVw86.png)


### Android NDK 程式、設置

1. **Android gradle 設置**（以下只列出重點部份）
    
    Android 現在預設使用 CMake 作為編譯器，而 CMake 對應的設置檔是 `CMakeList.text`；如果要使用 GNU 編譯，並使用 `Android.mk` 檔作為編譯設置檔，則須設定 `externalNativeBuild`#`ndkBuild`
    
    ```kotlin=
    android {
        ...

        externalNativeBuild {
            
            // 設置 NDK Build 腳本檔案
            ndkBuild {
                path = file("src/main/cpp/Android.mk")
            }
        }
        
        // 設置 NDK 版本
        ndkVersion = "16.1.4479499"
    }
    ```
    > ![image](https://hackmd.io/_uploads/By4uR2aLT.png)

    :::warning
    * **運行時發生 `Cannot load library: reloc_library...` 問題**？
        
        那可能是 Android ndk 版本不符… 這要一個一個試試看，目前使用 Android NDK `16.1.4479499` 版本
        
        > ![image](https://hackmd.io/_uploads/H1-GyaTLp.png)

    :::

2. **Android NDK 檔案**
    
    透過 `open`（開啟）、`read`（讀取）、`write` （對裝置寫入）函數來操作裝置

    ```c=
    #include <string.h>
    #include <jni.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <sys/types.h>      // 包含了一些基本的系統資料類型的定義，例如 size_t、pid_t、off_t
    #include <sys/stat.h>       // 文件狀態資訊的結構體定義，如 struct stat
    #include <unistd.h>         // 對 POSIX 標準的存取支持，定義了一些通用的系統呼叫和符號常數, 包含了對檔案操作、進程控制、系統呼叫等方面的函數聲明，如read()、write()、close()、fork()等
    #include <fcntl.h>          // 用於控制文件描述符

    const char* const TARGET_DEV = "/dev/hello_world";
    char* jstring_to_point_char(JNIEnv* env, jstring str);

    // 嘗試開啟檔案
    jint Java_com_example_hello_1world_MainActivity_devOpen(JNIEnv* env, jobject this) {
        int dev = open(TARGET_DEV, O_RDWR);

        close(dev);

        return (jint) dev;
    }

    // 純粹讀取 /dev/hello_world 驅動
    jint Java_com_example_hello_1world_MainActivity_devBuf(JNIEnv* env, jobject this, jint index) {
        int dev = open(TARGET_DEV, O_RDWR);
        unsigned char buf[4] = {0, 0, 0, 0};

        read(dev, buf, 4);

        int value = ((int) buf[index]);

        close(dev);

        return (jint) value;
    }

    // 讀取裝置，並取得「單字數量」
    jint Java_com_example_hello_1world_MainActivity_readDev(JNIEnv* env, jobject this) {
        int dev = open(TARGET_DEV, O_RDWR);
        unsigned char buf[4] = {0, 0, 0, 0};

        read(dev, buf, 4);

        // big endian
        int wordCount = ((int) buf[0]) << 24 | ((int) buf[1]) << 16 | ((int) buf[2]) << 8 | ((int) buf[3]);

        jint jWordCount = (jint) wordCount;

        close(dev);

        return jWordCount;
    }

    // 將 Java 應用的數據，寫入到 /dev/hello_world 裝置
    jint Java_com_example_hello_1world_MainActivity_writeDev(JNIEnv* env, jobject this, jstring str) {
        int dev = open(TARGET_DEV, O_RDWR);
        char* pStr = jstring_to_point_char(env, str);

        if (pStr != NULL) {
            size_t len = strlen(pStr);

            write(dev, pStr, len);

            return (jint) len;
        }

        close(dev);

        return (jint) -1;
    }

    // 將 Java String 轉為 char 指標
    char* jstring_to_point_char(JNIEnv* env, jstring str) {

        jclass clzString = (*env) -> FindClass(env, "java/lang/String");
        jstring strEncode = (*env) -> NewStringUTF(env, "utf-8");

        // 取得 String#getBytes(String): byte[]
        jmethodID  getBytesMethodId = (*env) -> GetMethodID(env, clzString, "getBytes", "(Ljava/lang/String;)[B");      
        
        // String 呼叫 getBytes() 方法
        jbyteArray  byteArray = (jbyteArray)((*env) -> CallObjectMethod(env, str, getBytesMethodId, strEncode));            

        // 計算 string length
        jsize size = (*env) -> GetArrayLength(env, byteArray);      

        // byteArray 轉 jbyte
        jbyte* pByte = (*env) -> GetByteArrayElements(env, byteArray, JNI_FALSE);

        char* pStr = NULL;

        if (size > 0) {
            // 動態創建空間，用來保存 Java 應用的字串
            pStr = (char*) malloc(size);
            memcpy(pStr, pByte, size);          // pByte 複製 4 byte 到 pStr
        }

        return pStr;
    }
    ```
    :::warning
    * 無法正常開啟 `hello_world` 裝置？
        
        開啟後 dev 為 `-1`，這可能是因為 App 權限不足的關係，所以無法開啟目標裝置；僅需要使用 `chmod` 指令，將 `/dev/hello_world` 裝置的權限放寬即可使用
        
        ```shell=
        adb chmod 777 /dev/hello_world
        ```
    :::


3. **簡單的 Java 應用**

    ```java=
    public class MainActivity extends AppCompatActivity {

        private static String err = "";

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

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

            // Example of a call to a native method
            binding.readDevBtn.setOnClickListener(view -> {
                String userInput = binding.textInput.getText().toString();
                int writeCount = writeDev(userInput);
                Toast.makeText(this, "Write count: " + writeCount, Toast.LENGTH_LONG)
                                .show();

                StringBuffer stringBuffer = new StringBuffer();
                for (int i = 0; i < 4; i ++) {
                    stringBuffer.append("index: " + i + "(" + devBuf(i) + ")").append("\n");
                }
                stringBuffer.append("devOpen: " + devOpen());

                binding.showWordCount.setText(stringBuffer + "\nDev word count: " + readDev());
            });

            binding.showWordCount.setText(err);
        }

        public native int devOpen();

        public native int readDev();

        public native int writeDev(String string);

        public native int devBuf(int index);

        static {
            try {
                // 讀取 ndk 檔案
                System.loadLibrary("ndk_hello_world");
            } catch (Throwable t) {
                err = t.fillInStackTrace().getMessage();
            }
        }
    }
    ```
    
    :::info
    * 讀取 ndk 檔案時檔案為 `libndk_hello_world.so`，須去頭去尾轉為 `ndk_hello_world`

        > ![image](https://hackmd.io/_uploads/rkouWTTIa.png)
    :::

    > 測試結果
    > 
    > ![image](https://hackmd.io/_uploads/BynU1TpUT.png)


## Android Java 程式測試驅動

如果要透過 Java 應用 API 直接讀取驅動，則需要事先 Android Root 裝置、設定驅動權限為 777（所有人皆可操作）


### Android Java 程式

* 如果已經有 Android 裝置的 Root 權限，那就可以直接使用 Java IO 提供的 API 來操作驅動

    ```java=
    public class JavaIOApiActivity extends AppCompatActivity {

        private static final String DEV_NAME = "/dev/hello_world";

        private ActivityMain2Binding binding;

        @Override
        public void onCreate(@Nullable Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);

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

            binding.readDevBtn.setOnClickListener(view -> {
                writeDev();

                readDev();
            });

        }

        public void writeDev() {
            String inputStr = binding.textInput.getText().toString();
            try(FileOutputStream fileOutputStream = new FileOutputStream(DEV_NAME)) {

                fileOutputStream.write(inputStr.getBytes("UTF-8"));

            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        public void readDev() {

            try(FileInputStream fileInputStream = new FileInputStream(DEV_NAME)) {
                byte[] buf = new byte[4];

                fileInputStream.read(buf);

                int wordCount = ((int) buf[0]) << 24 | ((int) buf[1]) << 16 | ((int) buf[2]) << 8 | ((int) buf[3]);

                binding.showWordCount.setText("wordCount: " + wordCount);

            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    ```

    > ![image](https://hackmd.io/_uploads/ry8_Ap6Up.png)



## Appendix & FAQ

:::info
:::

###### tags: `設備開發`
