WIP: Move to KMP

This commit is contained in:
William Brawner 2020-11-21 11:00:57 -07:00
parent 3164bee01b
commit 414ac50234
86 changed files with 399 additions and 246 deletions

View file

@ -1 +1 @@
Pi-Helper
Pi-helper

View file

@ -1,6 +1,22 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<JetCodeStyleSettings>
<option name="PACKAGES_TO_USE_STAR_IMPORTS">
<value>
<package name="java.util" alias="false" withSubpackages="false" />
<package name="kotlinx.android.synthetic" alias="false" withSubpackages="true" />
<package name="io.ktor" alias="false" withSubpackages="true" />
</value>
</option>
<option name="PACKAGES_IMPORT_LAYOUT">
<value>
<package name="" alias="false" withSubpackages="true" />
<package name="java" alias="false" withSubpackages="true" />
<package name="javax" alias="false" withSubpackages="true" />
<package name="kotlin" alias="false" withSubpackages="true" />
<package name="" alias="true" withSubpackages="true" />
</value>
</option>
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings>
<codeStyleSettings language="XML">

6
.idea/compiler.xml Normal file
View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="11" />
</component>
</project>

View file

@ -10,8 +10,9 @@
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
<option value="$PROJECT_DIR$/android" />
<option value="$PROJECT_DIR$/piholeclient" />
<option value="$PROJECT_DIR$/shared" />
</set>
</option>
<option name="resolveModulePerSourceSet" value="false" />

35
.idea/jarRepositories.xml Normal file
View file

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RemoteRepositoriesConfiguration">
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Maven Central repository" />
<option name="url" value="https://repo1.maven.org/maven2" />
</remote-repository>
<remote-repository>
<option name="id" value="jboss.community" />
<option name="name" value="JBoss Community repository" />
<option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
</remote-repository>
<remote-repository>
<option name="id" value="BintrayJCenter" />
<option name="name" value="BintrayJCenter" />
<option name="url" value="https://jcenter.bintray.com/" />
</remote-repository>
<remote-repository>
<option name="id" value="Google" />
<option name="name" value="Google" />
<option name="url" value="https://dl.google.com/dl/android/maven2/" />
</remote-repository>
<remote-repository>
<option name="id" value="MavenRepo" />
<option name="name" value="MavenRepo" />
<option name="url" value="https://repo.maven.apache.org/maven2/" />
</remote-repository>
<remote-repository>
<option name="id" value="Gradle Central Plugin Repository" />
<option name="name" value="Gradle Central Plugin Repository" />
<option name="url" value="https://plugins.gradle.org/m2" />
</remote-repository>
</component>
</project>

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="KotlinScriptingSettings">
<scriptDefinition className="org.jetbrains.kotlin.scripting.resolve.KotlinScriptDefinitionFromAnnotatedTemplate" definitionName="KotlinBuildScript">
<order>2147483647</order>
<autoReloadConfigurations>true</autoReloadConfigurations>
</scriptDefinition>
</component>
</project>

View file

@ -5,7 +5,7 @@
<configuration PROFILE_NAME="Debug" CONFIG_NAME="Debug" />
</configurations>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" project-jdk-name="1.8" project-jdk-type="JavaSDK">
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" default="false" project-jdk-name="1.8" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">

View file

@ -50,7 +50,7 @@ android {
}
dependencies {
implementation project(':piholeclient')
implementation project(':shared')
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
implementation "org.koin:koin-androidx-viewmodel:$koin_version"

View file

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View file

@ -5,8 +5,9 @@ import android.util.Log
import androidx.core.content.edit
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.wbrawner.piholeclient.PiHoleApiService
import com.wbrawner.piholeclient.VersionResponse
import com.wbrawner.pihelper.shared.KtorPiHoleApiService
import com.wbrawner.pihelper.shared.PiHoleApiService
import com.wbrawner.pihelper.shared.VersionResponse
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.net.ConnectException
@ -19,7 +20,7 @@ const val IP_MAX = 255
class AddPiHelperViewModel(
private val sharedPreferences: SharedPreferences,
private val apiService: PiHoleApiService
private val apiService: PiHoleApiService = KtorPiHoleApiService()
) : ViewModel() {
@Volatile

View file

@ -20,7 +20,7 @@ import androidx.core.text.set
import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer
import androidx.navigation.fragment.findNavController
import com.wbrawner.piholeclient.Status
import com.wbrawner.pihelper.shared.Status
import kotlinx.android.synthetic.main.fragment_main.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers

View file

@ -3,8 +3,8 @@ package com.wbrawner.pihelper
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.wbrawner.piholeclient.PiHoleApiService
import com.wbrawner.piholeclient.Status
import com.wbrawner.piholeclient.StatusProvider
import com.wbrawner.pihelper.shared.Status
import com.wbrawner.pihelper.shared.StatusProvider
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlin.coroutines.coroutineContext

View file

Before

Width:  |  Height:  |  Size: 561 B

After

Width:  |  Height:  |  Size: 561 B

View file

Before

Width:  |  Height:  |  Size: 395 B

After

Width:  |  Height:  |  Size: 395 B

View file

Before

Width:  |  Height:  |  Size: 741 B

After

Width:  |  Height:  |  Size: 741 B

View file

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

View file

Before

Width:  |  Height:  |  Size: 4 KiB

After

Width:  |  Height:  |  Size: 4 KiB

View file

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View file

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

View file

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

View file

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

View file

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

View file

Before

Width:  |  Height:  |  Size: 8.6 KiB

After

Width:  |  Height:  |  Size: 8.6 KiB

View file

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB

View file

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View file

@ -1,11 +1,12 @@
buildscript {
ext.kotlin_version = '1.3.61'
ext.kotlin_version = '1.4.10'
ext.coroutines_version = '1.3.2'
ext.koin_version = '2.0.1'
ext.okhttp_version = '4.2.2'
repositories {
google()
jcenter()
maven { url "https://kotlin.bintray.com/kotlinx" }
}
dependencies {
classpath 'com.android.tools.build:gradle:3.6.2'

View file

@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip

View file

@ -1,146 +0,0 @@
package com.wbrawner.piholeclient
import com.squareup.moshi.Moshi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.HttpUrl
import okhttp3.OkHttpClient
import okhttp3.Request
import kotlin.reflect.KClass
interface PiHoleApiService {
var baseUrl: String?
var apiKey: String?
suspend fun getSummary(
version: Boolean = false,
type: Boolean = false
): Summary
suspend fun getVersion(): VersionResponse
suspend fun getTopItems(): TopItemsResponse
suspend fun enable(): StatusResponse
suspend fun disable(duration: Long? = null): StatusResponse
/**
@Query("overTimeData10mins") overTimeData10mins: Boolean = true,
@Query("topItems") topItems: Int? = null,
@Query("topClients") topClients: Int? = null,
@Query("getForwardDestinations") getForwardDestinations: Boolean = true,
@Query("getQueryTypes") getQueryTypes: Boolean = true,
@Query("getAllQueries") getAllQueries: Boolean = true
*/
// suspend fun login(password: String): Response<String>
//
// @GET("/admin/scripts/pi-hole/php/api_token.php")
// suspend fun apiKey(phpSession: String): Response<String>
}
const val BASE_PATH = "/admin/api.php"
class OkHttpPiHoleApiService(
private val okHttpClient: OkHttpClient,
private val moshi: Moshi
) : PiHoleApiService {
override var baseUrl: String? = null
@Synchronized
get
@Synchronized
set
override var apiKey: String? = null
@Synchronized
get
@Synchronized
set
private val urlBuilder: HttpUrl.Builder
get() {
val host = baseUrl ?: throw IllegalStateException("No base URL defined")
return HttpUrl.Builder()
.scheme("http")
.host(host)
.addPathSegments(BASE_PATH)
}
override suspend fun getSummary(version: Boolean, type: Boolean): Summary {
val url = urlBuilder
.addQueryParameter("summary", "")
if (version) {
url.addQueryParameter("version", "")
}
if (type) {
url.addQueryParameter("type", "")
}
val request = Request.Builder()
.get()
.url(url.build())
return sendRequest(request.build(), Summary::class)!!
}
override suspend fun getVersion(): VersionResponse {
val url = urlBuilder
.addQueryParameter("version", "")
val request = Request.Builder()
.get()
.url(url.build())
return sendRequest(request.build(), VersionResponse::class)!!
}
override suspend fun getTopItems(): TopItemsResponse {
val apiToken = this.apiKey ?: throw java.lang.IllegalStateException("No API Token provided")
val url = urlBuilder
.addQueryParameter("topItems", "25")
.addQueryParameter("auth", apiToken)
val request = Request.Builder()
.get()
.url(url.build())
return sendRequest(request.build(), TopItemsResponse::class)!!
}
override suspend fun enable(): StatusResponse {
val apiToken = this.apiKey ?: throw java.lang.IllegalStateException("No API Token provided")
val url = urlBuilder
.addQueryParameter("enable", "")
.addQueryParameter("auth", apiToken)
val request = Request.Builder()
.get()
.url(url.build())
return sendRequest(request.build(), StatusResponse::class)!!
}
override suspend fun disable(duration: Long?): StatusResponse {
val apiToken = this.apiKey ?: throw java.lang.IllegalStateException("No API Token provided")
val url = urlBuilder
.addQueryParameter("disable", duration?.toString()?: "")
.addQueryParameter("auth", apiToken)
val request = Request.Builder()
.get()
.url(url.build())
return sendRequest(request.build(), StatusResponse::class)!!
}
private suspend fun <T : Any> sendRequest(request: Request, responseType: KClass<T>?): T? {
return withContext(Dispatchers.IO) {
val response = okHttpClient.newCall(request).execute()
if (!response.isSuccessful) {
null
} else {
@Suppress("UNCHECKED_CAST")
when (responseType) {
null -> null
String::class -> response.body?.string() as T
else -> response.body?.let {
moshi
.adapter(responseType.javaObjectType)
.fromJson(it.source())
}
}
}
}
}
}

View file

@ -11,8 +11,6 @@ import okio.Buffer
import org.koin.dsl.module
import java.util.concurrent.TimeUnit
const val NAME_BASE_URL = "baseUrl"
val piHoleClientModule = module {
single {
Moshi.Builder().build()

View file

@ -1,85 +0,0 @@
package com.wbrawner.piholeclient
import androidx.annotation.Keep
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class Summary(
@Json(name = "domains_being_blocked")
val domainsBeingBlocked: String,
@Json(name = "dns_queries_today")
val dnsQueriesToday: String,
@Json(name = "ads_blocked_today")
val adsBlockedToday: String,
@Json(name = "ads_percentage_today")
val adsPercentageToday: String,
@Json(name = "unique_domains")
val uniqueDomains: String,
@Json(name = "queries_forwarded")
val queriesForwarded: String,
@Json(name = "clients_ever_seen")
val clientsEverSeen: String,
@Json(name = "unique_clients")
val uniqueClients: String,
@Json(name = "dns_queries_all_types")
val dnsQueriesAllTypes: String,
@Json(name = "queries_cached")
val queriesCached: String,
@Json(name = "no_data_replies")
val noDataReplies: String?,
@Json(name = "nx_domain_replies")
val nxDomainReplies: String?,
@Json(name = "cname_replies")
val cnameReplies: String?,
@Json(name = "in_replies")
val ipReplies: String?,
@Json(name = "privacy_level")
val privacyLevel: String,
override val status: Status,
@Json(name = "gravity_last_updated")
val gravity: Gravity?,
val type: String?,
val version: Int?
) : StatusProvider
@Keep
enum class Status {
@Json(name = "enabled")
ENABLED,
@Json(name = "disabled")
DISABLED
}
@JsonClass(generateAdapter = true)
data class Gravity(
@Json(name = "file_exists")
val fileExists: Boolean,
val absolute: Int,
val relative: Relative
)
@JsonClass(generateAdapter = true)
data class Relative(
val days: String,
val hours: String,
val minutes: String
)
@JsonClass(generateAdapter = true)
data class VersionResponse(val version: Int)
@JsonClass(generateAdapter = true)
data class TopItemsResponse(
@Json(name = "top_queries") val topQueries: Map<String, String>,
@Json(name = "top_ads") val topAds: Map<String, Double>
)
@JsonClass(generateAdapter = true)
data class StatusResponse(
override val status: Status
) : StatusProvider
interface StatusProvider {
val status: Status
}

View file

@ -1,2 +1,2 @@
include ':app', ':piholeclient'
include ':android', ':piholeclient', ':shared'
rootProject.name='Pi-helper'

1
shared/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

88
shared/build.gradle.kts Normal file
View file

@ -0,0 +1,88 @@
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget
plugins {
kotlin("multiplatform")
id("com.android.library")
id("kotlin-android-extensions")
kotlin("plugin.serialization") version "1.4.10"
}
group = "com.wbrawner.pihelper"
version = "1.0-SNAPSHOT"
repositories {
gradlePluginPortal()
google()
jcenter()
mavenCentral()
}
val ktor_version = "+"
kotlin {
android()
ios {
binaries {
framework {
baseName = "shared"
}
}
}
sourceSets {
val commonMain by getting {
dependencies {
implementation("io.ktor:ktor-client-core:$ktor_version")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:+")
}
}
val commonTest by getting {
dependencies {
implementation(kotlin("test-common"))
implementation(kotlin("test-annotations-common"))
}
}
val androidMain by getting {
dependencies {
implementation("com.google.android.material:material:1.2.0")
implementation("io.ktor:ktor-client-android:$ktor_version")
}
}
val androidTest by getting {
dependencies {
implementation(kotlin("test-junit"))
implementation("junit:junit:4.12")
}
}
val iosMain by getting {
dependencies {
implementation("io.ktor:ktor-client-ios:$ktor_version")
}
}
val iosTest by getting
}
}
android {
compileSdkVersion(30)
sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml")
defaultConfig {
minSdkVersion(23)
targetSdkVersion(30)
versionCode = 1
versionName = "1.0"
}
buildTypes {
getByName("release") {
isMinifyEnabled = false
}
}
}
val packForXcode by tasks.creating(Sync::class) {
group = "build"
val mode = System.getenv("CONFIGURATION") ?: "DEBUG"
val sdkName = System.getenv("SDK_NAME") ?: "iphonesimulator"
val targetName = "ios" + if (sdkName.startsWith("iphoneos")) "Arm64" else "X64"
val framework = kotlin.targets.getByName<KotlinNativeTarget>(targetName).binaries.getFramework(mode)
inputs.property("mode", mode)
dependsOn(framework.linkTask)
val targetDir = File(buildDir, "xcode-frameworks")
from({ framework.outputDirectory })
into(targetDir)
}
tasks.getByName("build").dependsOn(packForXcode)

21
shared/proguard-rules.pro vendored Normal file
View file

@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.kts.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View file

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.wbrawner.pihelper.android"/>

View file

@ -0,0 +1,6 @@
package com.wbrawner.pihelper.shared
import io.ktor.client.*
import io.ktor.client.engine.android.*
actual fun httpClient(): HttpClient = HttpClient(Android)

View file

@ -0,0 +1,110 @@
package com.wbrawner.pihelper.shared
import io.ktor.client.*
import io.ktor.client.request.*
import io.ktor.http.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlin.jvm.Synchronized
import kotlin.reflect.KClass
interface PiHoleApiService {
var baseUrl: String?
var apiKey: String?
suspend fun getSummary(
version: Boolean = false,
type: Boolean = false
): Summary
suspend fun getVersion(): VersionResponse
suspend fun getTopItems(): TopItemsResponse
suspend fun enable(): StatusResponse
suspend fun disable(duration: Long? = null): StatusResponse
/**
@Query("overTimeData10mins") overTimeData10mins: Boolean = true,
@Query("topItems") topItems: Int? = null,
@Query("topClients") topClients: Int? = null,
@Query("getForwardDestinations") getForwardDestinations: Boolean = true,
@Query("getQueryTypes") getQueryTypes: Boolean = true,
@Query("getAllQueries") getAllQueries: Boolean = true
*/
// suspend fun login(password: String): Response<String>
//
// @GET("/admin/scripts/pi-hole/php/api_token.php")
// suspend fun apiKey(phpSession: String): Response<String>
}
const val BASE_PATH = "/admin/api.php"
expect fun httpClient(): HttpClient
class KtorPiHoleApiService(private val httpClient: HttpClient = httpClient()) : PiHoleApiService {
override var baseUrl: String? = null
@Synchronized
get
@Synchronized
set
override var apiKey: String? = null
@Synchronized
get
@Synchronized
set
private fun urlBuilder(configuration: URLBuilder.() -> Unit): Url {
val host = baseUrl ?: throw IllegalStateException("No base URL defined")
return URLBuilder(
protocol = URLProtocol.HTTP,
host = host,
encodedPath = BASE_PATH
).run {
configuration()
build()
}
}
override suspend fun getSummary(version: Boolean, type: Boolean): Summary = httpClient.get {
url(urlBuilder {
parameters["summary"] = ""
if (type) {
parameters["type"] = ""
}
})
}
override suspend fun getVersion(): VersionResponse = httpClient.get {
url(urlBuilder {
parameters["version"] = ""
})
}
override suspend fun getTopItems(): TopItemsResponse = httpClient.get {
val apiToken = apiKey ?: throw Error("No API Token provided")
url(urlBuilder {
parameters["topItems"] = "25"
parameters["auth"] = apiToken
})
}
override suspend fun enable(): StatusResponse = httpClient.get {
val apiToken = apiKey ?: throw Error("No API Token provided")
url(urlBuilder {
parameters["enable"] = ""
parameters["auth"] = apiToken
})
}
override suspend fun disable(duration: Long?): StatusResponse = httpClient.get {
val apiToken = apiKey ?: throw Error("No API Token provided")
url(urlBuilder {
parameters["disable"] = duration?.toString() ?: ""
parameters["auth"] = apiToken
})
}
}

View file

@ -0,0 +1,83 @@
package com.wbrawner.pihelper.shared
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class Summary(
@SerialName("domains_being_blocked")
val domainsBeingBlocked: String,
@SerialName("dns_queries_today")
val dnsQueriesToday: String,
@SerialName("ads_blocked_today")
val adsBlockedToday: String,
@SerialName("ads_percentage_today")
val adsPercentageToday: String,
@SerialName("unique_domains")
val uniqueDomains: String,
@SerialName("queries_forwarded")
val queriesForwarded: String,
@SerialName("clients_ever_seen")
val clientsEverSeen: String,
@SerialName("unique_clients")
val uniqueClients: String,
@SerialName("dns_queries_all_types")
val dnsQueriesAllTypes: String,
@SerialName("queries_cached")
val queriesCached: String,
@SerialName("no_data_replies")
val noDataReplies: String?,
@SerialName("nx_domain_replies")
val nxDomainReplies: String?,
@SerialName("cname_replies")
val cnameReplies: String?,
@SerialName("in_replies")
val ipReplies: String?,
@SerialName("privacy_level")
val privacyLevel: String,
override val status: Status,
@SerialName("gravity_last_updated")
val gravity: Gravity?,
val type: String?,
val version: Int?
) : StatusProvider
enum class Status {
@SerialName("enabled")
ENABLED,
@SerialName("disabled")
DISABLED
}
@Serializable
data class Gravity(
@SerialName("file_exists")
val fileExists: Boolean,
val absolute: Int,
val relative: Relative
)
@Serializable
data class Relative(
val days: String,
val hours: String,
val minutes: String
)
@Serializable
data class VersionResponse(val version: Int)
@Serializable
data class TopItemsResponse(
@SerialName("top_queries") val topQueries: Map<String, String>,
@SerialName("top_ads") val topAds: Map<String, Double>
)
@Serializable
data class StatusResponse(
override val status: Status
) : StatusProvider
interface StatusProvider {
val status: Status
}

View file

@ -0,0 +1,6 @@
package com.wbrawner.pihelper.shared
import io.ktor.client.*
import io.ktor.client.engine.ios.*
actual fun httpClient(): HttpClient = HttpClient(Ios)