ElixirDesktopを動かすAndroid Projectを０から作成する

# はじめに
この記事は Elixirアドベントカレンダーのシリーズ４の6日目の記事です

ElixirDekstopのiOSアプリを以下を参考にXCodeでプロジェクトを作るところから初めて
実際に起動するまで行います

https://github.com/elixir-desktop/android-example-app


# フォルダ構成

別のネイティブアプリを作るライブラリにLiveViewNativeというものがあり、そのサンプルアプリのフォルダ構成が、 `app_name/native/[ios or android pj]`となっていたのでそちらを踏襲します

https://github.com/liveview-native/elixirconf_chat

```
mkdir native
```

# プロジェクトを新規に作成
Native C++ を選択
![スクリーンショット 2023-10-07 21.18.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/12b9ee06-2e0d-2207-7d91-65add479745b.png)

Save Locationは bookshelf/native/androidを指定します
gradleはElixirDesktopに合わせてgrooby DSLにする

![スクリーンショット 2023-12-06 0.52.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/29866d3c-bc30-61c3-9167-82e3f036f4d0.png)

defaultでFinish

![スクリーンショット 2023-12-06 0.52.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/1214d86c-a2ff-af66-e46d-d150f411fa54.png)

okをクリック

![スクリーンショット 2023-12-06 0.52.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/503dca9c-beac-5e2d-5b2f-648b30f34243.png)


# サンプルPJからファイルをコピー

`app/libs/erlang.jar`ファイルを
`native/android/app/libs/`にコピー

`app/src/main/assets`フォルダを
`native/android/app/src/main/`にコピー

`app/src/main/res/xml/provider_paths.xml`ファイルを
`native/android/app/src/main/res/xml/`にコピー

`app/src/main/res/layout/activity_main.xml`ファイルを
`native/android/app/src/main/res/layout/activity_main.xml`と差し替える

# main/java のファイルをコピー & fix

`app/src/main/java/io/elixirdesktop/example/Bridge.kt`ファイルを
`native/android/app/src/main/java/com/example/bookshelf/`にコピー

packageを記述を差し替えます

```diff_kotlin:native/android/app/src/main/java/com/example/bookshelf/Bridge.kt
- package io.elixirdesktop.example
+ package com.example.bookshelf

import android.annotation.SuppressLint
...
```

`MainActivity.kt`の`package`以外を差し替えます
コピー後
`databinding.ActivityMainBinding`の前半分を差し替え


```diff_kotlin:native/android/app/src/main/java/com/example/bookshelf/MainActivity.kt
- package io.elixirdesktop.example
+ package com.example.bookshelf

import android.os.Bundle
import android.system.Os
import android.app.Activity
import android.view.KeyEvent
import android.view.View
- import io.elixirdesktop.example.databinding.ActivityMainBinding
+ import com.example.bookshelf.databinding.ActivityMainBinding
import java.io.*
import java.util.*
import android.webkit.WebView

import android.webkit.WebViewClient

class MainActivity : Activity() {
    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
        binding.browser.webViewClient = object : WebViewClient() {
            override fun onPageFinished(view: WebView, url: String) {
                if (binding.browser.visibility != View.VISIBLE) {
                    binding.browser.visibility = View.VISIBLE
                    binding.splash.visibility = View.GONE
                }
            }
        }

        if (bridge != null) {
            // This happens on re-creation of the activity e.g. after rotating the screen
            bridge!!.setWebView(binding.browser)
        } else {
            // This happens only on the first time when starting the app
            bridge = Bridge(applicationContext, binding.browser)
        }
    }

    override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
        if (event.action == KeyEvent.ACTION_DOWN) {
            when (keyCode) {
                KeyEvent.KEYCODE_BACK -> {
                    if (binding.browser.canGoBack()) {
                        binding.browser.goBack()
                    } else {
                        finish()
                    }
                    return true
                }
            }
        }
        return super.onKeyDown(keyCode, event)
    }

    companion object {
        var bridge: Bridge? = null
    }
}

```


# manifestファイル変更
providerのauthoritiesの変更漏れに気をつけましょう

```diff_xml:native/android/app/src/main/AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
+    android:installLocation="internalOnly">

+   <uses-permission android:name="android.permission.INTERNET" />

    <application
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.Trareco"
+        android:extractNativeLibs="true"
+        android:usesCleartextTraffic="true"
        tools:targetApi="31"
        >

+        <provider
+            android:name="androidx.core.content.FileProvider"
+            android:authorities="com.example.bookshelf.fileprovider"
+            android:exported="false"
+            android:grantUriPermissions="true">
+            <meta-data
+                android:name="android.support.FILE_PROVIDER_PATHS"
+                android:resource="@xml/provider_paths" />
+        </provider>


        <activity
+            android:configChanges="orientation|screenSize|screenLayout|keyboardHidden|density|fontScale|keyboard|layoutDirection|mcc|mnc|navigation|smallestScreenSize|touchscreen|uiMode"
            android:name=".MainActivity"
            android:exported="true"
            android:label="@string/app_name"
            android:theme="@style/Theme.Trareco">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>
```

# run_mixをコピーして編集します
```diff_shell:native/android/app/run_mix
#!/bin/bash
set -e

BASE=`pwd`
APP_FILE="$BASE/src/main/assets/app.zip"
export MIX_ENV=prod
export MIX_TARGET=android

- if [ ! -d "elixir-app" ]; then
-  git clone https://github.com/elixir-desktop/desktop-example-app.git elixir-app
- fi

- # using the right runtime versions
- if [ ! -f "elixir-app/.tool-versions" ]; then
-  cp .tool-versions elixir-app/
- fi

- cd elixir-app
+ cd ../../../

if [ ! -d "deps/desktop" ]; then
  mix local.hex --force
  mix local.rebar
  mix deps.get
fi

# npmライブラリを使うまではコメントアウト
# if [ ! -d "assets/node_modules" ]; then
#  cd assets && npm i && cd ..
# fi

if [ -f "$APP_FILE" ]; then
  rm "$APP_FILE"
fi

mix assets.deploy && \
  mix release --overwrite && \
-  cd "_build/${MIX_TARGET}_${MIX_ENV}/rel/todo_app" && \
+  cd "_build/${MIX_TARGET}_${MIX_ENV}/rel/bookeshelf" && \
  zip -9r "$APP_FILE" lib/ releases/ --exclude "*.so"


```
# proguardに修正

以下を追加

```native/android/app/proguard-rules.pro
-dontskipnonpubliclibraryclasses
-dontobfuscate
-forceprocessing
-optimizationpasses 5

-keep class * extends android.app.Activity
-assumenosideeffects class android.util.Log {
    public static *** d(...);
    public static *** v(...);
}
```

# cpp ファイルの改修
CMakeLists.txtの改修
```native/android/app/src/main/cpp/CMakeLists.txt
project("bookshelf")
# 以下差し替え
add_library(native-lib SHARED native-lib.cpp)

# Searches for a specified prebuilt library and stores the path as a
# variable. Because CMake includes system libraries in the search path by
# default, you only need to specify the name of the public NDK library
# you want to add. CMake verifies that the library exists before
# completing its build.

find_library(log-lib log)

# Specifies libraries CMake should link to your target library. You
# can link multiple libraries, such as libraries you define in this
# build script, prebuilt third-party libraries, or system libraries.

target_link_libraries(native-lib ${log-lib})
```
native-lib.cppの改修

一旦全部コピーして環境依存の箇所のみ修正します
```diff_c:native/android/app/src/main/cpp/native-lib.cpp:L160
extern "C" JNIEXPORT jstring JNICALL
- Java_io_elixirdesktop_example_Bridge_startErlang(
+ Java_com_example_bookshelf_Bridge_startErlang(
```

# build.gradleの修正
色々足していきます

```diff_groovy:native/android/app/build.gradle
plugins {
    id 'com.android.application'
    id 'org.jetbrains.kotlin.android'
}

android {
    namespace 'com.example.trareco'
-   compileSdk 33
+   compileSdk 34

+    task buildNum(type: Exec, description: 'Update Elixir App') {
+        commandLine './run_mix', 'package.mobile'
+    }

+    tasks.withType(JavaCompile) {
+        compileTask -> compileTask.dependsOn buildNum
+    }

    defaultConfig {
        applicationId "com.example.trareco"
        minSdk 29
-       targetSdk 33
+       targetSdk 34
        versionCode 1
        versionName "1.0"
+       ndk {
+           abiFilters "armeabi-v7a", "arm64-v8a" , "x86_64"
+       }
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+       externalNativeBuild {
+           cmake {
+               cppFlags ''
+           }
+       }
    }

    buildTypes {
        release {
-            minifyEnabled false
+            minifyEnabled true
+            shrinkResources true
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
    kotlinOptions {
        jvmTarget = '1.8'
    }
    externalNativeBuild {
        cmake {
            path file('src/main/cpp/CMakeLists.txt')
            version '3.22.1'
        }
    }
    buildFeatures {
        viewBinding true
    }
+    packagingOptions {
+        jniLibs {
+            useLegacyPackaging = true
+        }
+    }
}

dependencies {
+    implementation fileTree(dir: "libs", include: '*.jar')
    
    implementation 'androidx.core:core-ktx:1.9.0'
    implementation 'androidx.appcompat:appcompat:1.6.1'
    implementation 'com.google.android.material:material:1.8.0'
    implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
    testImplementation 'junit:junit:4.13.2'
    androidTestImplementation 'androidx.test.ext:junit:1.1.5'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
}
```

全部終わったら右上のsync gradleで同期してからビルドを実行します

![スクリーンショット 2023-10-07 21.42.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/32b50a71-baa3-3e55-d22f-d0474c665fab.png)

ファイルを追加したらAndroidの実行ボタンをクリックすると以下のように表示されます

![スクリーンショット 2023-12-06 2.23.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/2487ef42-2390-6b72-257c-207007064e70.png)


# 最後に
Androidでプロジェクトを作るところから実際にElixirDesktopが動くところまでを実装しました
ファイルをコピーしたりといろいろ面倒なので、LiveViewNativeみたいに自動化されないかなーとは思っています

次はCRUD画面作成とデータベースマイグレーションについて書きます

本記事は以上になります、ありがとうございました