Compare commits

...

4 commits
main ... kmp

Author SHA1 Message Date
b4c7400a9b Fix KMP setup for iOS 2020-12-24 08:26:33 -07:00
f155bdfa42 Finish Android KMP migration
Signed-off-by: William Brawner <me@wbrawner.com>
2020-11-28 07:15:53 -07:00
0b336610df WIP: Move to KMP 2020-11-21 11:09:44 -07:00
414ac50234 WIP: Move to KMP 2020-11-21 11:00:57 -07:00
93 changed files with 398 additions and 437 deletions

View file

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

View file

@ -1,6 +1,13 @@
<component name="ProjectCodeStyleConfiguration"> <component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173"> <code_scheme name="Project" version="173">
<JetCodeStyleSettings> <JetCodeStyleSettings>
<option name="PACKAGES_TO_USE_STAR_IMPORTS">
<value>
<package name="java.util" withSubpackages="false" static="false" />
<package name="kotlinx.android.synthetic" withSubpackages="true" static="false" />
<package name="io.ktor" withSubpackages="true" static="false" />
</value>
</option>
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" /> <option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings> </JetCodeStyleSettings>
<codeStyleSettings language="XML"> <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="1.8" />
</component>
</project>

View file

@ -10,11 +10,12 @@
<option name="modules"> <option name="modules">
<set> <set>
<option value="$PROJECT_DIR$" /> <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> </set>
</option> </option>
<option name="resolveModulePerSourceSet" value="false" /> <option name="resolveModulePerSourceSet" value="false" />
<option name="useQualifiedModuleNames" value="true" />
</GradleProjectSettings> </GradleProjectSettings>
</option> </option>
</component> </component>

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" /> <configuration PROFILE_NAME="Debug" CONFIG_NAME="Debug" />
</configurations> </configurations>
</component> </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_1_8" default="true" project-jdk-name="1.8" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" /> <output url="file://$PROJECT_DIR$/build/classes" />
</component> </component>
<component name="ProjectType"> <component name="ProjectType">

View file

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

View file

@ -3,6 +3,7 @@
package="com.wbrawner.pihelper"> package="com.wbrawner.pihelper">
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<application <application
android:name=".PiHelperApplication" android:name=".PiHelperApplication"
@ -12,7 +13,8 @@
android:label="@string/app_name" android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/AppTheme"> android:theme="@style/AppTheme"
android:usesCleartextTraffic="true">
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:windowSoftInputMode="adjustResize"> android:windowSoftInputMode="adjustResize">

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.core.content.edit
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import com.wbrawner.piholeclient.PiHoleApiService import com.wbrawner.pihelper.shared.KtorPiHoleApiService
import com.wbrawner.piholeclient.VersionResponse import com.wbrawner.pihelper.shared.PiHoleApiService
import com.wbrawner.pihelper.shared.VersionResponse
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.net.ConnectException import java.net.ConnectException

View file

@ -20,7 +20,7 @@ import androidx.core.text.set
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.navigation.fragment.findNavController 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.android.synthetic.main.fragment_main.*
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers

View file

@ -2,7 +2,6 @@ package com.wbrawner.pihelper
import android.app.Application import android.app.Application
import android.content.Context import android.content.Context
import com.wbrawner.piholeclient.piHoleClientModule
import org.koin.android.ext.koin.androidContext import org.koin.android.ext.koin.androidContext
import org.koin.android.ext.koin.androidLogger import org.koin.android.ext.koin.androidLogger
import org.koin.core.context.startKoin import org.koin.core.context.startKoin
@ -15,7 +14,6 @@ class PiHelperApplication: Application() {
androidLogger() androidLogger()
androidContext(this@PiHelperApplication) androidContext(this@PiHelperApplication)
modules(listOf( modules(listOf(
piHoleClientModule,
piHelperModule piHelperModule
)) ))
} }

View file

@ -1,12 +1,15 @@
package com.wbrawner.pihelper package com.wbrawner.pihelper
import androidx.security.crypto.EncryptedSharedPreferences import androidx.security.crypto.EncryptedSharedPreferences
import com.wbrawner.piholeclient.NAME_BASE_URL import com.wbrawner.pihelper.shared.KtorPiHoleApiService
import com.wbrawner.pihelper.shared.PiHoleApiService
import com.wbrawner.pihelper.shared.httpClient
import org.koin.androidx.viewmodel.dsl.viewModel import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.core.qualifier.named import org.koin.core.qualifier.named
import org.koin.dsl.module import org.koin.dsl.module
const val ENCRYPTED_SHARED_PREFS_FILE_NAME = "pihelper.prefs" const val ENCRYPTED_SHARED_PREFS_FILE_NAME = "pihelper.prefs"
const val NAME_BASE_URL = "baseUrl"
val piHelperModule = module { val piHelperModule = module {
single { single {
@ -30,4 +33,8 @@ val piHelperModule = module {
single(named(NAME_BASE_URL)) { single(named(NAME_BASE_URL)) {
get<EncryptedSharedPreferences>().getString(KEY_BASE_URL, "") get<EncryptedSharedPreferences>().getString(KEY_BASE_URL, "")
} }
single<PiHoleApiService> {
KtorPiHoleApiService(httpClient())
}
} }

View file

@ -2,9 +2,9 @@ package com.wbrawner.pihelper
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import com.wbrawner.piholeclient.PiHoleApiService import com.wbrawner.pihelper.shared.PiHoleApiService
import com.wbrawner.piholeclient.Status import com.wbrawner.pihelper.shared.Status
import com.wbrawner.piholeclient.StatusProvider import com.wbrawner.pihelper.shared.StatusProvider
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
import kotlin.coroutines.coroutineContext 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,14 +1,15 @@
buildscript { buildscript {
ext.kotlin_version = '1.3.61' ext.kotlin_version = '1.4.20'
ext.coroutines_version = '1.3.2' ext.coroutines_version = '1.3.2'
ext.koin_version = '2.0.1' ext.koin_version = '2.0.1'
ext.okhttp_version = '4.2.2' ext.okhttp_version = '4.2.2'
repositories { repositories {
google() google()
jcenter() jcenter()
maven { url "https://kotlin.bintray.com/kotlinx" }
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:3.6.2' classpath 'com.android.tools.build:gradle:4.1.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
} }
} }

View file

@ -19,3 +19,6 @@ android.useAndroidX=true
android.enableJetifier=true android.enableJetifier=true
# Kotlin code style for this project: "official" or "obsolete": # Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official kotlin.code.style=official
kotlin.incremental.multiplatform=true
kotlin.mpp.stability.nowarn=true
xcodeproj=../Pi-Helper

View file

@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists 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 +0,0 @@
/build

View file

@ -1,45 +0,0 @@
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
android {
compileSdkVersion 29
defaultConfig {
minSdkVersion 23
targetSdkVersion 29
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles 'consumer-rules.pro'
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
implementation "org.koin:koin-core:$koin_version"
implementation "com.squareup.okhttp3:okhttp:${okhttp_version}"
implementation "com.squareup.okhttp3:logging-interceptor:${okhttp_version}"
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.core:core-ktx:1.1.0'
implementation "com.squareup.moshi:moshi:1.9.2"
kapt "com.squareup.moshi:moshi-kotlin-codegen:1.9.2"
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
}

View file

@ -1,67 +0,0 @@
# JSR 305 annotations are for embedding nullability information.
-dontwarn javax.annotation.**
-keepclasseswithmembers class * {
@com.squareup.moshi.* <methods>;
}
-keep @com.squareup.moshi.JsonQualifier interface *
# Enum field names are used by the integrated EnumJsonAdapter.
# values() is synthesized by the Kotlin compiler and is used by EnumJsonAdapter indirectly
# Annotate enums with @JsonClass(generateAdapter = false) to use them with Moshi.
-keepclassmembers @com.squareup.moshi.JsonClass class * extends java.lang.Enum {
<fields>;
**[] values();
}
# The name of @JsonClass types is used to look up the generated adapter.
-keepnames @com.squareup.moshi.JsonClass class *
# Retain generated target class's synthetic defaults constructor and keep DefaultConstructorMarker's
# name. We will look this up reflectively to invoke the type's constructor.
#
# We can't _just_ keep the defaults constructor because Proguard/R8's spec doesn't allow wildcard
# matching preceding parameters.
-keepnames class kotlin.jvm.internal.DefaultConstructorMarker
-keepclassmembers @com.squareup.moshi.JsonClass @kotlin.Metadata class * {
synthetic <init>(...);
}
# Retain generated JsonAdapters if annotated type is retained.
-if @com.squareup.moshi.JsonClass class *
-keep class <1>JsonAdapter {
<init>(...);
<fields>;
}
-if @com.squareup.moshi.JsonClass class **$*
-keep class <1>_<2>JsonAdapter {
<init>(...);
<fields>;
}
-if @com.squareup.moshi.JsonClass class **$*$*
-keep class <1>_<2>_<3>JsonAdapter {
<init>(...);
<fields>;
}
-if @com.squareup.moshi.JsonClass class **$*$*$*
-keep class <1>_<2>_<3>_<4>JsonAdapter {
<init>(...);
<fields>;
}
-if @com.squareup.moshi.JsonClass class **$*$*$*$*
-keep class <1>_<2>_<3>_<4>_<5>JsonAdapter {
<init>(...);
<fields>;
}
-if @com.squareup.moshi.JsonClass class **$*$*$*$*$*
-keep class <1>_<2>_<3>_<4>_<5>_<6>JsonAdapter {
<init>(...);
<fields>;
}
-keep class kotlin.reflect.jvm.internal.impl.builtins.BuiltInsLoaderImpl
-keepclassmembers class kotlin.Metadata {
public <methods>;
}

View file

@ -1,5 +0,0 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.wbrawner.piholeclient">
<uses-permission android:name="android.permission.INTERNET" />
<application android:usesCleartextTraffic="true" />
</manifest>

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

@ -1,62 +0,0 @@
package com.wbrawner.piholeclient
import com.squareup.moshi.JsonReader
import com.squareup.moshi.Moshi
import okhttp3.Cookie
import okhttp3.CookieJar
import okhttp3.HttpUrl
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
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()
}
single {
val client = OkHttpClient.Builder()
.connectTimeout(500, TimeUnit.MILLISECONDS)
.cookieJar(object : CookieJar {
val cookies = mutableMapOf<String, List<Cookie>>()
override fun loadForRequest(url: HttpUrl): List<Cookie> = cookies[url.host]
?: emptyList()
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
this.cookies[url.host] = cookies
}
})
if (BuildConfig.DEBUG) {
client.addInterceptor(HttpLoggingInterceptor(
object : HttpLoggingInterceptor.Logger {
val moshi = Moshi.Builder()
.build()
.adapter(Any::class.java)
.indent(" ")
override fun log(message: String) {
val prettyMessage = try {
val json = JsonReader.of(Buffer().writeUtf8(message))
moshi.toJson(json.readJsonValue())
} catch (ignored: Exception) {
message
}
HttpLoggingInterceptor.Logger.DEFAULT.log(prettyMessage)
}
})
.apply {
level = HttpLoggingInterceptor.Level.BODY
}
)
}
client.build()
}
single<PiHoleApiService> {
OkHttpPiHoleApiService(get(), get())
}
}

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,3 +0,0 @@
<resources>
<string name="app_name">PiHoleClient</string>
</resources>

View file

@ -1,2 +1,2 @@
include ':app', ':piholeclient' include ':android', ':shared'
rootProject.name='Pi-helper' 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()
}
kotlin {
android()
ios {
binaries {
framework {
baseName = "pihelper"
}
}
}
sourceSets {
val commonMain by getting {
dependencies {
implementation("io.ktor:ktor-client-core:1.4.2")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.0.1")
implementation("io.ktor:ktor-client-serialization:1.4.2")
}
}
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")
api("io.ktor:ktor-client-android:1.4.2")
}
}
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:1.4.2")
}
}
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)

View file

@ -1,6 +1,6 @@
# Add project specific ProGuard rules here. # Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the # You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle. # proguardFiles setting in build.gradle.kts.
# #
# For more details, see # For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html # http://developer.android.com/guide/developing/tools/proguard.html
@ -18,6 +18,4 @@
# If you keep the line number information, uncomment this to # If you keep the line number information, uncomment this to
# hide the original source file name. # hide the original source file name.
#-renamesourcefileattribute SourceFile #-renamesourcefileattribute SourceFile
-keepclassmembers @com.squareup.moshi.JsonClass class *

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,16 @@
package com.wbrawner.pihelper.shared
import io.ktor.client.*
import io.ktor.client.engine.android.*
import io.ktor.client.features.json.JsonFeature
import io.ktor.client.features.json.serializer.*
import kotlinx.serialization.json.Json
actual fun httpClient(): HttpClient = HttpClient(Android) {
install(JsonFeature) {
serializer = KotlinxSerializer(Json {
isLenient = true
ignoreUnknownKeys = true
})
}
}

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,84 @@
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? = null,
@SerialName("nx_domain_replies")
val nxDomainReplies: String? = null,
@SerialName("cname_replies")
val cnameReplies: String? = null,
@SerialName("in_replies")
val ipReplies: String? = null,
@SerialName("privacy_level")
val privacyLevel: String,
override val status: Status,
@SerialName("gravity_last_updated")
val gravity: Gravity? = null,
val type: String? = null,
val version: Int? = null
) : StatusProvider
@Serializable
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)