diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index bcb7791a3a..08887b3dd7 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -168,10 +168,18 @@ jobs:
strategy:
fail-fast: false
matrix:
- arch: [x64, x86, arm64, arm]
+ include:
+ - arch: x64
+ abi: x86_64
+ - arch: x86
+ abi: x86
+ - arch: arm64
+ abi: arm64-v8a
+ - arch: arm
+ abi: armeabi-v7a
env:
EXTRA_FLAGS: 'target_cpu="${{ matrix.arch }}" target_os="android"'
- BUNDLE: naiveproxy-${{ github.event.release.tag_name }}-${{ github.job }}-${{ matrix.arch }}
+ BUNDLE: naiveproxy-plugin-${{ github.event.release.tag_name || 'v1' }}-${{ matrix.abi }}.apk
steps:
- uses: actions/checkout@v4
- name: Cache toolchains (Linux, OpenWrt, Android)
@@ -219,20 +227,31 @@ jobs:
- run: ccache -s
- run: ./get-android-sys.sh
- run: ../tests/basic.sh out/Release/naive
- - name: Pack naiveproxy assets
+ - name: Gradle cache
+ uses: actions/cache@v3
+ with:
+ path: ~/.gradle
+ key: gradle-${{ hashFiles('**/*.gradle.kts') }}
+ - name: Create APK
+ working-directory: apk
+ env:
+ APK_ABI: ${{ matrix.abi }}
+ APK_VERSION_NAME: ${{ github.event.release.tag_name || 'v1' }}
+ KEYSTORE_PASS: ${{ secrets.KEYSTORE_PASS }}
run: |
- mkdir ${{ env.BUNDLE }}
- cp out/Release/naive config.json ../LICENSE ../USAGE.txt ${{ env.BUNDLE }}
- tar cJf ${{ env.BUNDLE }}.tar.xz ${{ env.BUNDLE }}
- openssl sha256 out/Release/naive >sha256sum.txt
+ mkdir -p app/libs/$APK_ABI
+ cp ../src/out/Release/naive app/libs/$APK_ABI/libnaive.so
+ ./gradlew :app:assembleRelease
+ openssl sha256 app/build/outputs/apk/release/${{ env.BUNDLE }} >sha256sum.txt
echo "SHA256SUM=$(cut -d' ' -f2 sha256sum.txt)" >>$GITHUB_ENV
- uses: actions/upload-artifact@v3
with:
- name: ${{ env.BUNDLE }}.tar.xz naive executable sha256 ${{ env.SHA256SUM }}
- path: src/sha256sum.txt
+ name: ${{ env.BUNDLE }} sha256 ${{ env.SHA256SUM }}
+ path: apk/sha256sum.txt
- name: Upload naiveproxy assets
if: ${{ github.event_name == 'release' }}
- run: gh release upload "${GITHUB_REF##*/}" ${{ env.BUNDLE }}.tar.xz --clobber
+ working-directory: apk/app/build/outputs/apk/release
+ run: gh release upload "${GITHUB_REF##*/}" ${{ env.BUNDLE }} --clobber
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
win:
@@ -394,108 +413,108 @@ jobs:
matrix:
include:
- arch: x86_64
- openwrt: 'target=x86 subtarget=64'
+ openwrt: "target=x86 subtarget=64"
target_cpu: x64
- arch: x86
- openwrt: 'target=x86 subtarget=generic'
+ openwrt: "target=x86 subtarget=generic"
target_cpu: x86
- arch: aarch64_cortex-a53
- openwrt: 'target=sunxi subtarget=cortexa53'
+ openwrt: "target=sunxi subtarget=cortexa53"
target_cpu: arm64
extra: 'arm_cpu="cortex-a53"'
- arch: aarch64_cortex-a53-static
- openwrt: 'target=sunxi subtarget=cortexa53'
+ openwrt: "target=sunxi subtarget=cortexa53"
target_cpu: arm64
extra: 'arm_cpu="cortex-a53" build_static=true no_madvise_syscall=true'
- arch: aarch64_cortex-a72
- openwrt: 'target=mvebu subtarget=cortexa72'
+ openwrt: "target=mvebu subtarget=cortexa72"
target_cpu: arm64
extra: 'arm_cpu="cortex-a72"'
- arch: aarch64_cortex-a72-static
- openwrt: 'target=mvebu subtarget=cortexa72'
+ openwrt: "target=mvebu subtarget=cortexa72"
target_cpu: arm64
extra: 'arm_cpu="cortex-a72" build_static=true no_madvise_syscall=true'
- arch: aarch64_generic
- openwrt: 'target=rockchip subtarget=armv8'
+ openwrt: "target=rockchip subtarget=armv8"
target_cpu: arm64
- arch: aarch64_generic-static
- openwrt: 'target=rockchip subtarget=armv8'
+ openwrt: "target=rockchip subtarget=armv8"
target_cpu: arm64
- extra: 'build_static=true no_madvise_syscall=true'
+ extra: "build_static=true no_madvise_syscall=true"
- arch: arm_arm1176jzf-s_vfp
- openwrt: 'target=bcm27xx subtarget=bcm2708'
+ openwrt: "target=bcm27xx subtarget=bcm2708"
target_cpu: arm
extra: 'arm_version=0 arm_cpu="arm1176jzf-s" arm_fpu="vfp" arm_float_abi="hard" arm_use_neon=false arm_use_thumb=false'
- arch: arm_arm926ej-s
- openwrt: 'target=mxs subtarget=generic'
+ openwrt: "target=mxs subtarget=generic"
target_cpu: arm
extra: 'arm_version=0 arm_cpu="arm926ej-s" arm_float_abi="soft" arm_use_neon=false arm_use_thumb=false'
- arch: arm_cortex-a15_neon-vfpv4
- openwrt: 'target=armsr subtarget=armv7'
+ openwrt: "target=armsr subtarget=armv7"
target_cpu: arm
extra: 'arm_version=0 arm_cpu="cortex-a15" arm_fpu="neon-vfpv4" arm_float_abi="hard" arm_use_neon=true'
- arch: arm_cortex-a5_vfpv4
- openwrt: 'target=at91 subtarget=sama5'
+ openwrt: "target=at91 subtarget=sama5"
target_cpu: arm
extra: 'arm_version=0 arm_cpu="cortex-a5" arm_fpu="vfpv4" arm_float_abi="hard" arm_use_neon=false'
- arch: arm_cortex-a7
- openwrt: 'target=mediatek subtarget=mt7629'
+ openwrt: "target=mediatek subtarget=mt7629"
target_cpu: arm
extra: 'arm_version=0 arm_cpu="cortex-a7" arm_float_abi="soft" arm_use_neon=false'
- arch: arm_cortex-a7_neon-vfpv4
- openwrt: 'target=sunxi subtarget=cortexa7'
+ openwrt: "target=sunxi subtarget=cortexa7"
target_cpu: arm
extra: 'arm_version=0 arm_cpu="cortex-a7" arm_fpu="neon-vfpv4" arm_float_abi="hard" arm_use_neon=true'
- arch: arm_cortex-a7_vfpv4
- openwrt: 'target=at91 subtarget=sama7'
+ openwrt: "target=at91 subtarget=sama7"
target_cpu: arm
extra: 'arm_version=0 arm_cpu="cortex-a7" arm_fpu="vfpv4" arm_float_abi="hard" arm_use_neon=false'
- arch: arm_cortex-a7_neon-vfpv4-static
- openwrt: 'target=sunxi subtarget=cortexa7'
+ openwrt: "target=sunxi subtarget=cortexa7"
target_cpu: arm
extra: 'arm_version=0 arm_cpu="cortex-a7" arm_fpu="neon-vfpv4" arm_float_abi="hard" arm_use_neon=true build_static=true no_madvise_syscall=true'
- arch: arm_cortex-a8_vfpv3
- openwrt: 'target=sunxi subtarget=cortexa8'
+ openwrt: "target=sunxi subtarget=cortexa8"
target_cpu: arm
extra: 'arm_version=0 arm_cpu="cortex-a8" arm_fpu="vfpv3" arm_float_abi="hard" arm_use_neon=false'
- arch: arm_cortex-a9
- openwrt: 'target=bcm53xx subtarget=generic'
+ openwrt: "target=bcm53xx subtarget=generic"
target_cpu: arm
extra: 'arm_version=0 arm_cpu="cortex-a9" arm_float_abi="soft" arm_use_neon=false'
- arch: arm_cortex-a9-static
- openwrt: 'target=bcm53xx subtarget=generic'
+ openwrt: "target=bcm53xx subtarget=generic"
target_cpu: arm
extra: 'arm_version=0 arm_cpu="cortex-a9" arm_float_abi="soft" arm_use_neon=false build_static=true no_madvise_syscall=true'
- arch: arm_cortex-a9_neon
- openwrt: 'target=zynq subtarget=generic'
+ openwrt: "target=zynq subtarget=generic"
target_cpu: arm
extra: 'arm_version=0 arm_cpu="cortex-a9" arm_fpu="neon" arm_float_abi="hard" arm_use_neon=true'
- arch: arm_cortex-a9_vfpv3-d16
- openwrt: 'target=tegra subtarget=generic'
+ openwrt: "target=tegra subtarget=generic"
target_cpu: arm
extra: 'arm_version=0 arm_cpu="cortex-a9" arm_fpu="vfpv3-d16" arm_float_abi="hard" arm_use_neon=false'
- arch: arm_mpcore
- openwrt: 'target=oxnas subtarget=ox820'
+ openwrt: "target=oxnas subtarget=ox820"
target_cpu: arm
extra: 'arm_version=0 arm_cpu="mpcore" arm_float_abi="soft" arm_use_neon=false arm_use_thumb=false'
- arch: arm_xscale
- openwrt: 'target=kirkwood subtarget=generic'
+ openwrt: "target=kirkwood subtarget=generic"
target_cpu: arm
extra: 'arm_version=0 arm_cpu="xscale" arm_float_abi="soft" arm_use_neon=false arm_use_thumb=false'
- arch: mipsel_24kc
- openwrt: 'target=ramips subtarget=rt305x'
+ openwrt: "target=ramips subtarget=rt305x"
target_cpu: mipsel
extra: 'mips_arch_variant="r2" mips_float_abi="soft"'
- arch: mipsel_24kc-static
- openwrt: 'target=ramips subtarget=rt305x'
+ openwrt: "target=ramips subtarget=rt305x"
target_cpu: mipsel
extra: 'mips_arch_variant="r2" mips_float_abi="soft" build_static=true no_madvise_syscall=true'
- arch: mipsel_mips32
- openwrt: 'target=bcm47xx subtarget=generic'
+ openwrt: "target=bcm47xx subtarget=generic"
target_cpu: mipsel
extra: 'mips_arch_variant="r1" mips_float_abi="soft"'
- arch: riscv64
- openwrt: 'target=sifiveu subtarget=generic'
+ openwrt: "target=sifiveu subtarget=generic"
target_cpu: riscv64
env:
EXTRA_FLAGS: target_cpu="${{ matrix.target_cpu }}" target_os="openwrt" ${{ matrix.extra }}
diff --git a/apk/.gitignore b/apk/.gitignore
new file mode 100644
index 0000000000..33e08ed755
--- /dev/null
+++ b/apk/.gitignore
@@ -0,0 +1,2 @@
+.gradle/
+app/build/
diff --git a/apk/app/build.gradle.kts b/apk/app/build.gradle.kts
new file mode 100644
index 0000000000..00cd5ac030
--- /dev/null
+++ b/apk/app/build.gradle.kts
@@ -0,0 +1,75 @@
+plugins {
+ id("com.android.application")
+ id("org.jetbrains.kotlin.android")
+}
+
+android {
+ namespace = "moe.matsuri.exe.naive"
+
+ signingConfigs {
+ create("release") {
+ storeFile = rootProject.file("release.keystore")
+ storePassword = System.getenv("KEYSTORE_PASS")
+ keyAlias = "release"
+ keyPassword = System.getenv("KEYSTORE_PASS")
+ }
+ }
+
+ buildTypes {
+ getByName("release") {
+ proguardFiles(
+ getDefaultProguardFile("proguard-android-optimize.txt"),
+ file("proguard-rules.pro")
+ )
+ isMinifyEnabled = true
+ signingConfig = signingConfigs.getByName("release")
+ }
+ }
+
+ compileSdk = 33
+
+ defaultConfig {
+ minSdk = 21
+ targetSdk = 33
+
+ applicationId = "moe.matsuri.exe.naive"
+ versionCode = System.getenv("APK_VERSION_NAME").removePrefix("v").split(".")[0].toInt()
+ versionName = System.getenv("APK_VERSION_NAME").removePrefix("v")
+ splits.abi {
+ isEnable = true
+ isUniversalApk = false
+ reset()
+ include(System.getenv("APK_ABI"))
+ }
+ }
+
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_1_8
+ targetCompatibility = JavaVersion.VERSION_1_8
+ }
+
+ kotlinOptions {
+ jvmTarget = "1.8"
+ }
+
+ lint {
+ showAll = true
+ checkAllWarnings = true
+ checkReleaseBuilds = false
+ warningsAsErrors = true
+ }
+
+ applicationVariants.all {
+ outputs.all {
+ this as com.android.build.gradle.internal.api.BaseVariantOutputImpl
+ outputFileName =
+ outputFileName.replace(project.name, "naiveproxy-plugin-v$versionName")
+ .replace("-release", "")
+ .replace("-oss", "")
+ }
+ }
+
+ sourceSets.getByName("main") {
+ jniLibs.srcDir("libs")
+ }
+}
diff --git a/apk/app/src/main/AndroidManifest.xml b/apk/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..72f3c643dc
--- /dev/null
+++ b/apk/app/src/main/AndroidManifest.xml
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apk/app/src/main/java/io/nekohasekai/sagernet/plugin/NativePluginProvider.kt b/apk/app/src/main/java/io/nekohasekai/sagernet/plugin/NativePluginProvider.kt
new file mode 100644
index 0000000000..ef37e2b3e3
--- /dev/null
+++ b/apk/app/src/main/java/io/nekohasekai/sagernet/plugin/NativePluginProvider.kt
@@ -0,0 +1,98 @@
+/******************************************************************************
+ * *
+ * Copyright (C) 2021 by nekohasekai *
+ * *
+ * This program is free software: you can redistribute it and/or modify *
+ * it under the terms of the GNU General Public License as published by *
+ * the Free Software Foundation, either version 3 of the License, or *
+ * (at your option) any later version. *
+ * *
+ * This program is distributed in the hope that it will be useful, *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+ * GNU General Public License for more details. *
+ * *
+ * You should have received a copy of the GNU General Public License *
+ * along with this program. If not, see . *
+ * *
+ ******************************************************************************/
+
+package io.nekohasekai.sagernet.plugin
+
+import android.content.ContentProvider
+import android.content.ContentValues
+import android.database.Cursor
+import android.database.MatrixCursor
+import android.net.Uri
+import android.os.Bundle
+import android.os.ParcelFileDescriptor
+
+abstract class NativePluginProvider : ContentProvider() {
+ override fun getType(uri: Uri): String? = "application/x-elf"
+
+ override fun onCreate(): Boolean = true
+
+ /**
+ * Provide all files needed for native plugin.
+ *
+ * @param provider A helper object to use to add files.
+ */
+ protected abstract fun populateFiles(provider: PathProvider)
+
+ override fun query(
+ uri: Uri,
+ projection: Array?,
+ selection: String?,
+ selectionArgs: Array?,
+ sortOrder: String?,
+ ): Cursor? {
+ check(selection == null && selectionArgs == null && sortOrder == null)
+ val result = MatrixCursor(projection)
+ populateFiles(PathProvider(uri, result))
+ return result
+ }
+
+ /**
+ * Returns executable entry absolute path.
+ * This is used for fast mode initialization where ss-local launches your native binary at the path given directly.
+ * In order for this to work, plugin app is encouraged to have the following in its AndroidManifest.xml:
+ * - android:installLocation="internalOnly" for
+ * - android:extractNativeLibs="true" for
+ *
+ * Default behavior is throwing UnsupportedOperationException. If you don't wish to use this feature, use the
+ * default behavior.
+ *
+ * @return Absolute path for executable entry.
+ */
+ open fun getExecutable(): String = throw UnsupportedOperationException()
+
+ abstract fun openFile(uri: Uri): ParcelFileDescriptor
+ override fun openFile(uri: Uri, mode: String): ParcelFileDescriptor {
+ check(mode == "r")
+ return openFile(uri)
+ }
+
+ override fun call(method: String, arg: String?, extras: Bundle?): Bundle? = when (method) {
+ PluginContract.METHOD_GET_EXECUTABLE -> {
+ Bundle().apply {
+ putString(PluginContract.EXTRA_ENTRY, getExecutable())
+ }
+ }
+ else -> super.call(method, arg, extras)
+ }
+
+ // Methods that should not be used
+ override fun insert(uri: Uri, values: ContentValues?): Uri? =
+ throw UnsupportedOperationException()
+
+ override fun update(
+ uri: Uri,
+ values: ContentValues?,
+ selection: String?,
+ selectionArgs: Array?,
+ ): Int =
+ throw UnsupportedOperationException()
+
+ override fun delete(uri: Uri, selection: String?, selectionArgs: Array?): Int =
+ throw UnsupportedOperationException()
+}
diff --git a/apk/app/src/main/java/io/nekohasekai/sagernet/plugin/PathProvider.kt b/apk/app/src/main/java/io/nekohasekai/sagernet/plugin/PathProvider.kt
new file mode 100644
index 0000000000..8008e5a510
--- /dev/null
+++ b/apk/app/src/main/java/io/nekohasekai/sagernet/plugin/PathProvider.kt
@@ -0,0 +1,53 @@
+/******************************************************************************
+ * *
+ * Copyright (C) 2021 by nekohasekai *
+ * *
+ * This program is free software: you can redistribute it and/or modify *
+ * it under the terms of the GNU General Public License as published by *
+ * the Free Software Foundation, either version 3 of the License, or *
+ * (at your option) any later version. *
+ * *
+ * This program is distributed in the hope that it will be useful, *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+ * GNU General Public License for more details. *
+ * *
+ * You should have received a copy of the GNU General Public License *
+ * along with this program. If not, see . *
+ * *
+ ******************************************************************************/
+
+package io.nekohasekai.sagernet.plugin
+
+import android.database.MatrixCursor
+import android.net.Uri
+import java.io.File
+
+/**
+ * Helper class to provide relative paths of files to copy.
+ */
+class PathProvider internal constructor(baseUri: Uri, private val cursor: MatrixCursor) {
+ private val basePath = baseUri.path?.trim('/') ?: ""
+
+ fun addPath(path: String, mode: Int = 0b110100100): PathProvider {
+ val trimmed = path.trim('/')
+ if (trimmed.startsWith(basePath)) cursor.newRow()
+ .add(PluginContract.COLUMN_PATH, trimmed)
+ .add(PluginContract.COLUMN_MODE, mode)
+ return this
+ }
+ fun addTo(file: File, to: String = "", mode: Int = 0b110100100): PathProvider {
+ var sub = to + file.name
+ if (basePath.startsWith(sub)) if (file.isDirectory) {
+ sub += '/'
+ file.listFiles()!!.forEach { addTo(it, sub, mode) }
+ } else addPath(sub, mode)
+ return this
+ }
+ fun addAt(file: File, at: String = "", mode: Int = 0b110100100): PathProvider {
+ if (basePath.startsWith(at)) {
+ if (file.isDirectory) file.listFiles()!!.forEach { addTo(it, at, mode) } else addPath(at, mode)
+ }
+ return this
+ }
+}
diff --git a/apk/app/src/main/java/io/nekohasekai/sagernet/plugin/PluginContract.kt b/apk/app/src/main/java/io/nekohasekai/sagernet/plugin/PluginContract.kt
new file mode 100644
index 0000000000..2788533292
--- /dev/null
+++ b/apk/app/src/main/java/io/nekohasekai/sagernet/plugin/PluginContract.kt
@@ -0,0 +1,34 @@
+/******************************************************************************
+ * *
+ * Copyright (C) 2021 by nekohasekai *
+ * *
+ * This program is free software: you can redistribute it and/or modify *
+ * it under the terms of the GNU General Public License as published by *
+ * the Free Software Foundation, either version 3 of the License, or *
+ * (at your option) any later version. *
+ * *
+ * This program is distributed in the hope that it will be useful, *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+ * GNU General Public License for more details. *
+ * *
+ * You should have received a copy of the GNU General Public License *
+ * along with this program. If not, see . *
+ * *
+ ******************************************************************************/
+
+package io.nekohasekai.sagernet.plugin
+
+object PluginContract {
+
+ const val ACTION_NATIVE_PLUGIN = "io.nekohasekai.sagernet.plugin.ACTION_NATIVE_PLUGIN"
+ const val EXTRA_ENTRY = "io.nekohasekai.sagernet.plugin.EXTRA_ENTRY"
+ const val METADATA_KEY_ID = "io.nekohasekai.sagernet.plugin.id"
+ const val METADATA_KEY_EXECUTABLE_PATH = "io.nekohasekai.sagernet.plguin.executable_path"
+ const val METHOD_GET_EXECUTABLE = "sagernet:getExecutable"
+
+ const val COLUMN_PATH = "path"
+ const val COLUMN_MODE = "mode"
+ const val SCHEME = "plugin"
+ const val AUTHORITY = "io.nekohasekai.sagernet"
+}
diff --git a/apk/app/src/main/java/moe/matsuri/exe/naive/BinaryProvider.kt b/apk/app/src/main/java/moe/matsuri/exe/naive/BinaryProvider.kt
new file mode 100644
index 0000000000..2776e4cd00
--- /dev/null
+++ b/apk/app/src/main/java/moe/matsuri/exe/naive/BinaryProvider.kt
@@ -0,0 +1,42 @@
+/******************************************************************************
+ * *
+ * Copyright (C) 2021 by nekohasekai *
+ * *
+ * This program is free software: you can redistribute it and/or modify *
+ * it under the terms of the GNU General Public License as published by *
+ * the Free Software Foundation, either version 3 of the License, or *
+ * (at your option) any later version. *
+ * *
+ * This program is distributed in the hope that it will be useful, *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+ * GNU General Public License for more details. *
+ * *
+ * You should have received a copy of the GNU General Public License *
+ * along with this program. If not, see . *
+ * *
+ ******************************************************************************/
+
+package moe.matsuri.exe.naive
+
+import android.net.Uri
+import android.os.ParcelFileDescriptor
+import io.nekohasekai.sagernet.plugin.NativePluginProvider
+import io.nekohasekai.sagernet.plugin.PathProvider
+import java.io.File
+import java.io.FileNotFoundException
+
+class BinaryProvider : NativePluginProvider() {
+ override fun populateFiles(provider: PathProvider) {
+ provider.addPath("naive-plugin", 0b111101101)
+ }
+
+ override fun getExecutable() = context!!.applicationInfo.nativeLibraryDir + "/libnaive.so"
+ override fun openFile(uri: Uri): ParcelFileDescriptor = when (uri.path) {
+ "/naive-plugin" -> ParcelFileDescriptor.open(
+ File(getExecutable()),
+ ParcelFileDescriptor.MODE_READ_ONLY
+ )
+ else -> throw FileNotFoundException()
+ }
+}
diff --git a/apk/app/src/main/res/drawable/ic_launcher_foreground.xml b/apk/app/src/main/res/drawable/ic_launcher_foreground.xml
new file mode 100644
index 0000000000..ef0ca55847
--- /dev/null
+++ b/apk/app/src/main/res/drawable/ic_launcher_foreground.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/apk/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/apk/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000000..7005cb8e14
--- /dev/null
+++ b/apk/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/apk/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/apk/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 0000000000..7005cb8e14
--- /dev/null
+++ b/apk/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/apk/app/src/main/res/mipmap-hdpi/ic_launcher.png b/apk/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000000..db5767033e
Binary files /dev/null and b/apk/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/apk/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/apk/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 0000000000..11b071c7d4
Binary files /dev/null and b/apk/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/apk/app/src/main/res/mipmap-mdpi/ic_launcher.png b/apk/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000000..7d028abe13
Binary files /dev/null and b/apk/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/apk/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/apk/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 0000000000..a6650975c7
Binary files /dev/null and b/apk/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/apk/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/apk/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000000..3ce8151d0b
Binary files /dev/null and b/apk/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/apk/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/apk/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 0000000000..da5b85b0d9
Binary files /dev/null and b/apk/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/apk/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/apk/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000000..5181adaae7
Binary files /dev/null and b/apk/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/apk/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/apk/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000000..4feac43c28
Binary files /dev/null and b/apk/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/apk/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/apk/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000000..b535a085dc
Binary files /dev/null and b/apk/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/apk/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/apk/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000000..4a1edea686
Binary files /dev/null and b/apk/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/apk/app/src/main/res/values/ic_launcher_background.xml b/apk/app/src/main/res/values/ic_launcher_background.xml
new file mode 100644
index 0000000000..c58f6bcbaa
--- /dev/null
+++ b/apk/app/src/main/res/values/ic_launcher_background.xml
@@ -0,0 +1,4 @@
+
+
+ #E91E63
+
\ No newline at end of file
diff --git a/apk/build.gradle b/apk/build.gradle
new file mode 100644
index 0000000000..74752cee64
--- /dev/null
+++ b/apk/build.gradle
@@ -0,0 +1,18 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+buildscript {
+ repositories {
+ google()
+ mavenCentral()
+ }
+ dependencies {
+ classpath 'com.android.tools.build:gradle:7.3.1'
+ classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.10'
+
+ // NOTE: Do not place your application dependencies here; they belong
+ // in the individual module build.gradle files
+ }
+}
+
+task clean(type: Delete) {
+ delete rootProject.buildDir
+}
\ No newline at end of file
diff --git a/apk/gradle.properties b/apk/gradle.properties
new file mode 100644
index 0000000000..98bed167dc
--- /dev/null
+++ b/apk/gradle.properties
@@ -0,0 +1,21 @@
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
+# AndroidX package structure to make it clearer which packages are bundled with the
+# Android operating system, and which are packaged with your app"s APK
+# https://developer.android.com/topic/libraries/support-library/androidx-rn
+android.useAndroidX=true
+# Automatically convert third-party libraries to use AndroidX
+android.enableJetifier=true
+# Kotlin code style for this project: "official" or "obsolete":
+kotlin.code.style=official
\ No newline at end of file
diff --git a/apk/gradle/wrapper/gradle-wrapper.jar b/apk/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000000..e708b1c023
Binary files /dev/null and b/apk/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/apk/gradle/wrapper/gradle-wrapper.properties b/apk/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000000..e89ca18b4d
--- /dev/null
+++ b/apk/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Thu Jan 27 22:42:44 HKT 2022
+distributionBase=GRADLE_USER_HOME
+distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip
+distributionPath=wrapper/dists
+zipStorePath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
diff --git a/apk/gradlew b/apk/gradlew
new file mode 100755
index 0000000000..4f906e0c81
--- /dev/null
+++ b/apk/gradlew
@@ -0,0 +1,185 @@
+#!/usr/bin/env sh
+
+#
+# Copyright 2015 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+ echo "$*"
+}
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=`expr $i + 1`
+ done
+ case $i in
+ 0) set -- ;;
+ 1) set -- "$args0" ;;
+ 2) set -- "$args0" "$args1" ;;
+ 3) set -- "$args0" "$args1" "$args2" ;;
+ 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Escape application args
+save () {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=`save "$@"`
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+exec "$JAVACMD" "$@"
diff --git a/apk/release.keystore b/apk/release.keystore
new file mode 100644
index 0000000000..bb93a21024
Binary files /dev/null and b/apk/release.keystore differ
diff --git a/apk/settings.gradle b/apk/settings.gradle
new file mode 100644
index 0000000000..aa6d172eb0
--- /dev/null
+++ b/apk/settings.gradle
@@ -0,0 +1,10 @@
+dependencyResolutionManagement {
+ repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
+ repositories {
+ google()
+ mavenCentral()
+ }
+}
+rootProject.name = "Matsuri Plugins"
+
+include ':app'