Migrate networking code to Kotlin Multiplatform compatible module
This commit is contained in:
parent
65838d6905
commit
09f261b034
31 changed files with 351 additions and 497 deletions
|
@ -12,7 +12,7 @@
|
||||||
<set>
|
<set>
|
||||||
<option value="$PROJECT_DIR$" />
|
<option value="$PROJECT_DIR$" />
|
||||||
<option value="$PROJECT_DIR$/app" />
|
<option value="$PROJECT_DIR$/app" />
|
||||||
<option value="$PROJECT_DIR$/piholeclient" />
|
<option value="$PROJECT_DIR$/shared" />
|
||||||
</set>
|
</set>
|
||||||
</option>
|
</option>
|
||||||
<option name="resolveModulePerSourceSet" value="false" />
|
<option name="resolveModulePerSourceSet" value="false" />
|
||||||
|
|
|
@ -4,5 +4,17 @@
|
||||||
<inspection_tool class="PreviewAnnotationInFunctionWithParameters" enabled="true" level="ERROR" enabled_by_default="true">
|
<inspection_tool class="PreviewAnnotationInFunctionWithParameters" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
<option name="previewFile" value="true" />
|
<option name="previewFile" value="true" />
|
||||||
</inspection_tool>
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="previewFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PreviewMultipleParameterProviders" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="previewFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="previewFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
|
||||||
|
<option name="previewFile" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
</profile>
|
</profile>
|
||||||
</component>
|
</component>
|
|
@ -24,6 +24,10 @@
|
||||||
<entry key="../../../../layout/compose-model-1623358815178.xml" value="1.6830769230769231" />
|
<entry key="../../../../layout/compose-model-1623358815178.xml" value="1.6830769230769231" />
|
||||||
<entry key="../../../../layout/compose-model-1623359670724.xml" value="0.11413043478260869" />
|
<entry key="../../../../layout/compose-model-1623359670724.xml" value="0.11413043478260869" />
|
||||||
<entry key="../../../../layout/compose-model-1623360495184.xml" value="0.20880752102919348" />
|
<entry key="../../../../layout/compose-model-1623360495184.xml" value="0.20880752102919348" />
|
||||||
|
<entry key="../../../../layout/compose-model-1646330539642.xml" value="0.1" />
|
||||||
|
<entry key="../../../../layout/compose-model-1646331413004.xml" value="0.4506079027355623" />
|
||||||
|
<entry key="../../../../layout/compose-model-1646332602415.xml" value="0.13775083612040134" />
|
||||||
|
<entry key="../../../../layout/compose-model-1646358533232.xml" value="0.30153061224489797" />
|
||||||
<entry key="app/src/main/res/drawable-v26/ic_shortcut_enable.xml" value="0.27447916666666666" />
|
<entry key="app/src/main/res/drawable-v26/ic_shortcut_enable.xml" value="0.27447916666666666" />
|
||||||
<entry key="app/src/main/res/drawable-v26/ic_shortcut_pause.xml" value="0.27447916666666666" />
|
<entry key="app/src/main/res/drawable-v26/ic_shortcut_pause.xml" value="0.27447916666666666" />
|
||||||
<entry key="app/src/main/res/drawable/background_splash.xml" value="0.409375" />
|
<entry key="app/src/main/res/drawable/background_splash.xml" value="0.409375" />
|
||||||
|
|
|
@ -61,10 +61,13 @@ android {
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
compose = true
|
compose = true
|
||||||
}
|
}
|
||||||
|
composeOptions {
|
||||||
|
kotlinCompilerExtensionVersion = libs.versions.compose.get()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(project(":piholeclient"))
|
implementation(project(":shared"))
|
||||||
implementation(libs.bundles.coroutines)
|
implementation(libs.bundles.coroutines)
|
||||||
implementation(libs.bundles.compose)
|
implementation(libs.bundles.compose)
|
||||||
implementation(libs.hilt.android.core)
|
implementation(libs.hilt.android.core)
|
||||||
|
|
|
@ -11,11 +11,13 @@
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
|
android:usesCleartextTraffic="true"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/AppTheme">
|
android:theme="@style/AppTheme">
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:windowSoftInputMode="adjustResize">
|
android:windowSoftInputMode="adjustResize"
|
||||||
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
|
|
@ -4,8 +4,8 @@ import android.content.SharedPreferences
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import com.wbrawner.piholeclient.PiHoleApiService
|
import com.wbrawner.pihelper.shared.PiholeAPIService
|
||||||
import com.wbrawner.piholeclient.VersionResponse
|
import com.wbrawner.pihelper.shared.VersionResponse
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
@ -22,7 +22,7 @@ const val IP_MAX = 255
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class AddPiHelperViewModel @Inject constructor(
|
class AddPiHelperViewModel @Inject constructor(
|
||||||
private val sharedPreferences: SharedPreferences,
|
private val sharedPreferences: SharedPreferences,
|
||||||
private val apiService: PiHoleApiService
|
private val apiService: PiholeAPIService
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
@Volatile
|
@Volatile
|
||||||
|
|
|
@ -19,8 +19,8 @@ import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import androidx.navigation.compose.rememberNavController
|
import androidx.navigation.compose.rememberNavController
|
||||||
|
import com.wbrawner.pihelper.shared.Status
|
||||||
import com.wbrawner.pihelper.ui.PihelperTheme
|
import com.wbrawner.pihelper.ui.PihelperTheme
|
||||||
import com.wbrawner.piholeclient.Status
|
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.math.pow
|
import kotlin.math.pow
|
||||||
import kotlin.math.roundToLong
|
import kotlin.math.roundToLong
|
||||||
|
@ -168,7 +168,7 @@ fun CustomTimeDialog(
|
||||||
onTimeSelected: (Long) -> Unit
|
onTimeSelected: (Long) -> Unit
|
||||||
) {
|
) {
|
||||||
if (!visible) return
|
if (!visible) return
|
||||||
val (time: Long, setTime: (Long) -> Unit) = remember { mutableStateOf(0L) }
|
val (time: String, setTime: (String) -> Unit) = remember { mutableStateOf("10") }
|
||||||
val (duration, selectDuration: (Duration) -> Unit) = remember {
|
val (duration, selectDuration: (Duration) -> Unit) = remember {
|
||||||
mutableStateOf(Duration.SECONDS)
|
mutableStateOf(Duration.SECONDS)
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,8 @@ package com.wbrawner.pihelper
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import androidx.security.crypto.EncryptedSharedPreferences
|
import androidx.security.crypto.EncryptedSharedPreferences
|
||||||
import com.wbrawner.piholeclient.NAME_BASE_URL
|
import com.wbrawer.pihelper.shared.create
|
||||||
|
import com.wbrawner.pihelper.shared.PiholeAPIService
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
|
@ -13,6 +14,7 @@ import javax.inject.Named
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
const val ENCRYPTED_SHARED_PREFS_FILE_NAME = "pihelper.prefs"
|
const val ENCRYPTED_SHARED_PREFS_FILE_NAME = "pihelper.prefs"
|
||||||
|
const val NAME_BASE_URL = "baseUrl"
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
@InstallIn(SingletonComponent::class)
|
@InstallIn(SingletonComponent::class)
|
||||||
|
@ -33,4 +35,8 @@ object PiHelperModule {
|
||||||
@Named(NAME_BASE_URL)
|
@Named(NAME_BASE_URL)
|
||||||
fun providesBaseUrl(sharedPreferences: SharedPreferences) = sharedPreferences
|
fun providesBaseUrl(sharedPreferences: SharedPreferences) = sharedPreferences
|
||||||
.getString(KEY_BASE_URL, "")
|
.getString(KEY_BASE_URL, "")
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun providesPiholeAPIService(): PiholeAPIService = PiholeAPIService.create()
|
||||||
}
|
}
|
|
@ -1,9 +1,10 @@
|
||||||
package com.wbrawner.pihelper
|
package com.wbrawner.pihelper
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
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 dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
@ -14,7 +15,7 @@ import kotlin.coroutines.coroutineContext
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class PiHelperViewModel @Inject constructor(
|
class PiHelperViewModel @Inject constructor(
|
||||||
private val apiService: PiHoleApiService
|
private val apiService: PiholeAPIService
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
private val _status = MutableStateFlow(Status.LOADING)
|
private val _status = MutableStateFlow(Status.LOADING)
|
||||||
val status = _status.asStateFlow()
|
val status = _status.asStateFlow()
|
||||||
|
@ -30,7 +31,7 @@ class PiHelperViewModel @Inject constructor(
|
||||||
_status.value = action!!.invoke().status
|
_status.value = action!!.invoke().status
|
||||||
action = null
|
action = null
|
||||||
} catch (ignored: Exception) {
|
} catch (ignored: Exception) {
|
||||||
break
|
_status.value = Status.UNKNOWN
|
||||||
}
|
}
|
||||||
delay(1000)
|
delay(1000)
|
||||||
}
|
}
|
||||||
|
|
|
@ -55,10 +55,6 @@ fun ScanScreen(navController: NavController, addPiHelperViewModel: AddPiHelperVi
|
||||||
?.linkAddresses
|
?.linkAddresses
|
||||||
?.filter { !it.address.isLoopbackAddress && it.address is Inet4Address }
|
?.filter { !it.address.isLoopbackAddress && it.address is Inet4Address }
|
||||||
?.forEach { address ->
|
?.forEach { address ->
|
||||||
Log.d(
|
|
||||||
"Pi-helper",
|
|
||||||
"Found link address: ${address.address.hostName}"
|
|
||||||
)
|
|
||||||
addPiHelperViewModel.beginScanning(
|
addPiHelperViewModel.beginScanning(
|
||||||
address.address.hostAddress,
|
address.address.hostAddress,
|
||||||
onSuccess,
|
onSuccess,
|
||||||
|
|
|
@ -1,13 +1,10 @@
|
||||||
buildscript {
|
buildscript {
|
||||||
repositories {
|
repositories {
|
||||||
|
gradlePluginPortal()
|
||||||
google()
|
google()
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
val hiltVersion by extra("2.36")
|
|
||||||
val kotlinVersion by extra("1.4.32")
|
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath("com.android.tools.build:gradle:7.0.0-beta03")
|
classpath(libs.bundles.plugins)
|
||||||
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinVersion}")
|
|
||||||
classpath("com.google.dagger:hilt-android-gradle-plugin:$hiltVersion")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.mpp.enableGranularSourceSetsMetadata=true
|
||||||
|
kotlin.native.enableDependencyPropagation=false
|
||||||
|
|
|
@ -1,50 +1,66 @@
|
||||||
[versions]
|
[versions]
|
||||||
androidx-core = "1.3.2"
|
androidx-core = "1.3.2"
|
||||||
androidx-appcompat = "1.2.0"
|
androidx-appcompat = "1.2.0"
|
||||||
compose = "1.0.0-beta07"
|
compose = "1.1.1"
|
||||||
coroutines = "1.4.3"
|
coroutines = "1.4.3"
|
||||||
espresso = "3.3.0"
|
espresso = "3.3.0"
|
||||||
hilt-android = "2.36"
|
hilt-android = "2.38.1"
|
||||||
kotlin = "1.4.32"
|
kotlin = "1.6.10"
|
||||||
|
kotlinx-serialization = "1.3.2"
|
||||||
|
kotlinx-coroutines = "1.6.0-native-mt"
|
||||||
|
ktor = "2.0.0-beta-1"
|
||||||
lifecycle = "2.2.0"
|
lifecycle = "2.2.0"
|
||||||
material = "1.3.0"
|
material = "1.3.0"
|
||||||
maxSdk = "30"
|
maxSdk = "31"
|
||||||
moshi = "1.9.2"
|
moshi = "1.9.2"
|
||||||
minSdk = "23"
|
minSdk = "23"
|
||||||
navigation = "2.3.2"
|
navigation = "2.4.1"
|
||||||
okhttp = "4.2.2"
|
okhttp = "4.2.2"
|
||||||
versionCode = "1"
|
versionCode = "1"
|
||||||
versionName = "1.0"
|
versionName = "1.0"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
androidx-core = { module = "androidx.core:core-ktx", version.ref = "androidx-core" }
|
android-gradle = { module = "com.android.tools.build:gradle", version = "7.1.2"}
|
||||||
androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidx-appcompat" }
|
androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidx-appcompat" }
|
||||||
compose-ui = { module = "androidx.compose.ui:ui", version.ref = "compose" }
|
androidx-core = { module = "androidx.core:core-ktx", version.ref = "androidx-core" }
|
||||||
|
compose-activity = { module = "androidx.activity:activity-compose", version = "1.4.0" }
|
||||||
compose-material = { module = "androidx.compose.material:material", version.ref = "compose" }
|
compose-material = { module = "androidx.compose.material:material", version.ref = "compose" }
|
||||||
compose-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "compose" }
|
|
||||||
compose-activity = { module = "androidx.activity:activity-compose", version = "1.3.0-alpha07" }
|
|
||||||
compose-test = { module = "androidx.compose.ui:ui-test-junit4", version.ref = "compose" }
|
compose-test = { module = "androidx.compose.ui:ui-test-junit4", version.ref = "compose" }
|
||||||
|
compose-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "compose" }
|
||||||
|
compose-ui = { module = "androidx.compose.ui:ui", version.ref = "compose" }
|
||||||
|
coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" }
|
||||||
|
coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
|
||||||
|
dagger-hilt = { module = "com.google.dagger:hilt-android-gradle-plugin", version.ref = "hilt-android" }
|
||||||
|
espresso = { module = "androidx.test.espresso:espresso-core", version.ref = "espresso" }
|
||||||
hilt-android-core = { module = "com.google.dagger:hilt-android", version.ref = "hilt-android" }
|
hilt-android-core = { module = "com.google.dagger:hilt-android", version.ref = "hilt-android" }
|
||||||
hilt-android-kapt = { module = "com.google.dagger:hilt-compiler", version.ref = "hilt-android" }
|
hilt-android-kapt = { module = "com.google.dagger:hilt-compiler", version.ref = "hilt-android" }
|
||||||
hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version = "1.0.0-alpha02" }
|
hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version = "1.0.0" }
|
||||||
navigation-compose = { module = "androidx.navigation:navigation-compose", version = "2.4.0-alpha01" }
|
|
||||||
material = { module = "com.google.android.material:material", version.ref = "material" }
|
|
||||||
moshi-core = { module = "com.squareup.moshi:moshi", version.ref = "moshi" }
|
|
||||||
moshi-codegen = { module = "com.squareup.moshi:moshi-kotlin-codegen", version.ref = "moshi" }
|
|
||||||
navigation-fragment = { module = "androidx.navigation:navigation-fragment-ktx", version.ref = "navigation" }
|
|
||||||
navigation-ui = { module = "androidx.navigation:navigation-ui-ktx", version.ref = "navigation" }
|
|
||||||
coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
|
|
||||||
coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" }
|
|
||||||
preference = { module = "androidx.preference:preference-ktx", version = "1.1.1" }
|
|
||||||
junit = { module = "junit:junit", version = "4.12" }
|
junit = { module = "junit:junit", version = "4.12" }
|
||||||
kotlin-gradle = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
|
kotlin-gradle = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
|
||||||
kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" }
|
|
||||||
kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin" }
|
kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin" }
|
||||||
|
kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" }
|
||||||
|
ktor-client-ios = { module = "io.ktor:ktor-client-ios", version.ref = "ktor" }
|
||||||
|
ktor-client-js = { module = "io.ktor:ktor-client-js", version.ref = "ktor" }
|
||||||
|
ktor-client-android = { module = "io.ktor:ktor-client-android", version.ref = "ktor" }
|
||||||
|
ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" }
|
||||||
|
ktor-client-serialization = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" }
|
||||||
|
ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
|
||||||
|
ktor-client-logging = { module = "io.ktor:ktor-client-logging", version.ref = "ktor" }
|
||||||
|
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" }
|
||||||
|
kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinx-coroutines" }
|
||||||
|
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" }
|
||||||
|
material = { module = "com.google.android.material:material", version.ref = "material" }
|
||||||
|
moshi-codegen = { module = "com.squareup.moshi:moshi-kotlin-codegen", version.ref = "moshi" }
|
||||||
|
moshi-core = { module = "com.squareup.moshi:moshi", version.ref = "moshi" }
|
||||||
|
navigation-compose = { module = "androidx.navigation:navigation-compose", version = "navigation" }
|
||||||
|
navigation-fragment = { module = "androidx.navigation:navigation-fragment-ktx", version.ref = "navigation" }
|
||||||
|
navigation-ui = { module = "androidx.navigation:navigation-ui-ktx", version.ref = "navigation" }
|
||||||
|
preference = { module = "androidx.preference:preference-ktx", version = "1.1.1" }
|
||||||
test-ext = { module = "androidx.test.ext:junit", version = "1.1.2" }
|
test-ext = { module = "androidx.test.ext:junit", version = "1.1.2" }
|
||||||
espresso = { module = "androidx.test.espresso:espresso-core", version.ref = "espresso" }
|
|
||||||
|
|
||||||
[bundles]
|
[bundles]
|
||||||
compose = ["compose-ui", "compose-material", "compose-tooling", "compose-activity", "navigation-compose"]
|
compose = ["compose-ui", "compose-material", "compose-tooling", "compose-activity", "navigation-compose"]
|
||||||
coroutines = ["coroutines-core", "coroutines-android"]
|
coroutines = ["coroutines-core", "coroutines-android"]
|
||||||
|
plugins = ["android-gradle", "kotlin-gradle", "dagger-hilt"]
|
||||||
|
|
||||||
|
|
||||||
|
|
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
|
@ -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-7.0.2-all.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip
|
||||||
|
|
6
gradlew
vendored
6
gradlew
vendored
|
@ -82,7 +82,7 @@ location of your Java installation."
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
JAVACMD="java"
|
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.
|
which kotlin >/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
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
location of your Java installation."
|
location of your Java installation."
|
||||||
|
@ -109,7 +109,7 @@ if $darwin; then
|
||||||
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# For Cygwin, switch paths to Windows format before running java
|
# For Cygwin, switch paths to Windows format before running kotlin
|
||||||
if $cygwin ; then
|
if $cygwin ; then
|
||||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||||
|
@ -161,7 +161,7 @@ save () {
|
||||||
}
|
}
|
||||||
APP_ARGS=$(save "$@")
|
APP_ARGS=$(save "$@")
|
||||||
|
|
||||||
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
# Collect all arguments for the kotlin 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"
|
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
||||||
|
|
||||||
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
|
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
|
||||||
|
|
1
piholeclient/.gitignore
vendored
1
piholeclient/.gitignore
vendored
|
@ -1 +0,0 @@
|
||||||
/build
|
|
|
@ -1,45 +0,0 @@
|
||||||
plugins {
|
|
||||||
id("com.android.library")
|
|
||||||
id("kotlin-android")
|
|
||||||
id("kotlin-kapt")
|
|
||||||
id("dagger.hilt.android.plugin")
|
|
||||||
}
|
|
||||||
|
|
||||||
android {
|
|
||||||
compileSdk = libs.versions.maxSdk.get().toInt()
|
|
||||||
defaultConfig {
|
|
||||||
minSdk = libs.versions.minSdk.get().toInt()
|
|
||||||
targetSdk = libs.versions.maxSdk.get().toInt()
|
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
|
||||||
consumerProguardFiles("consumer-rules.pro")
|
|
||||||
}
|
|
||||||
|
|
||||||
buildTypes {
|
|
||||||
release {
|
|
||||||
isMinifyEnabled = false
|
|
||||||
proguardFiles(
|
|
||||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
|
||||||
"proguard-rules.pro"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
compileOptions {
|
|
||||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
|
||||||
targetCompatibility = JavaVersion.VERSION_1_8
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
api("com.squareup.okhttp3:okhttp:${libs.versions.okhttp.get()}")
|
|
||||||
implementation("com.squareup.okhttp3:logging-interceptor:${libs.versions.okhttp.get()}")
|
|
||||||
implementation(libs.androidx.core)
|
|
||||||
implementation(libs.androidx.appcompat)
|
|
||||||
implementation(libs.bundles.coroutines)
|
|
||||||
implementation(libs.hilt.android.core)
|
|
||||||
kapt(libs.hilt.android.kapt)
|
|
||||||
implementation(libs.kotlin.reflect)
|
|
||||||
api(libs.moshi.core)
|
|
||||||
kapt(libs.moshi.codegen)
|
|
||||||
testImplementation(libs.junit)
|
|
||||||
}
|
|
|
@ -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>;
|
|
||||||
}
|
|
23
piholeclient/proguard-rules.pro
vendored
23
piholeclient/proguard-rules.pro
vendored
|
@ -1,23 +0,0 @@
|
||||||
# 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
|
|
||||||
|
|
||||||
-keepclassmembers @com.squareup.moshi.JsonClass class *
|
|
|
@ -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())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,71 +0,0 @@
|
||||||
package com.wbrawner.piholeclient
|
|
||||||
|
|
||||||
import com.squareup.moshi.JsonReader
|
|
||||||
import com.squareup.moshi.Moshi
|
|
||||||
import dagger.Module
|
|
||||||
import dagger.Provides
|
|
||||||
import dagger.hilt.InstallIn
|
|
||||||
import dagger.hilt.components.SingletonComponent
|
|
||||||
import okhttp3.Cookie
|
|
||||||
import okhttp3.CookieJar
|
|
||||||
import okhttp3.HttpUrl
|
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
import okhttp3.logging.HttpLoggingInterceptor
|
|
||||||
import okio.Buffer
|
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
import javax.inject.Singleton
|
|
||||||
|
|
||||||
const val NAME_BASE_URL = "baseUrl"
|
|
||||||
|
|
||||||
@Module
|
|
||||||
@InstallIn(SingletonComponent::class)
|
|
||||||
object PiHoleClientModule {
|
|
||||||
@Provides
|
|
||||||
@Singleton
|
|
||||||
fun providesMoshi(): Moshi = Moshi.Builder().build()
|
|
||||||
|
|
||||||
@Provides
|
|
||||||
@Singleton
|
|
||||||
fun providesOkHttpClient(): OkHttpClient {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return client.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Provides
|
|
||||||
@Singleton
|
|
||||||
fun providesPiHoleApiService(okHttpClient: OkHttpClient, moshi: Moshi): PiHoleApiService =
|
|
||||||
OkHttpPiHoleApiService(okHttpClient, moshi)
|
|
||||||
}
|
|
|
@ -1,89 +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,
|
|
||||||
@Transient
|
|
||||||
LOADING,
|
|
||||||
@Transient
|
|
||||||
UNKNOWN
|
|
||||||
}
|
|
||||||
|
|
||||||
@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
|
|
||||||
}
|
|
|
@ -1,3 +0,0 @@
|
||||||
<resources>
|
|
||||||
<string name="app_name">PiHoleClient</string>
|
|
||||||
</resources>
|
|
|
@ -8,4 +8,4 @@ dependencyResolutionManagement {
|
||||||
}
|
}
|
||||||
rootProject.name = "Pi-helper"
|
rootProject.name = "Pi-helper"
|
||||||
include(":app")
|
include(":app")
|
||||||
include(":piholeclient")
|
include(":shared")
|
||||||
|
|
1
shared/.gitignore
vendored
Normal file
1
shared/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
/build
|
63
shared/build.gradle.kts
Normal file
63
shared/build.gradle.kts
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
plugins {
|
||||||
|
kotlin("multiplatform")
|
||||||
|
id("com.android.library")
|
||||||
|
kotlin("plugin.serialization") version "1.6.10"
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
android()
|
||||||
|
listOf(
|
||||||
|
iosX64(),
|
||||||
|
iosArm64(),
|
||||||
|
iosSimulatorArm64()
|
||||||
|
).forEach {
|
||||||
|
it.binaries.framework {
|
||||||
|
baseName = "Pihelper"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceSets {
|
||||||
|
val commonMain by getting {
|
||||||
|
dependencies {
|
||||||
|
implementation(libs.ktor.client.core)
|
||||||
|
implementation(libs.ktor.client.logging)
|
||||||
|
implementation(libs.ktor.client.serialization)
|
||||||
|
implementation(libs.ktor.client.content.negotiation)
|
||||||
|
implementation(libs.kotlinx.coroutines.core)
|
||||||
|
implementation(libs.kotlinx.serialization.json)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val androidMain by getting {
|
||||||
|
dependencies {
|
||||||
|
implementation(libs.ktor.client.android)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val iosX64Main by getting
|
||||||
|
val iosArm64Main by getting
|
||||||
|
val iosSimulatorArm64Main by getting
|
||||||
|
val iosMain by creating {
|
||||||
|
dependsOn(commonMain)
|
||||||
|
iosX64Main.dependsOn(this)
|
||||||
|
iosArm64Main.dependsOn(this)
|
||||||
|
iosSimulatorArm64Main.dependsOn(this)
|
||||||
|
dependencies {
|
||||||
|
implementation(libs.ktor.client.ios)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml")
|
||||||
|
compileSdk = libs.versions.maxSdk.get().toInt()
|
||||||
|
defaultConfig {
|
||||||
|
minSdk = libs.versions.minSdk.get().toInt()
|
||||||
|
targetSdk = libs.versions.maxSdk.get().toInt()
|
||||||
|
}
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||||
|
targetCompatibility = JavaVersion.VERSION_1_8
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
package="com.wbrawner.piholeclient">
|
package="com.wbrawner.pihelper">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<application android:usesCleartextTraffic="true" />
|
</manifest>
|
||||||
</manifest>
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
package com.wbrawer.pihelper.shared
|
||||||
|
|
||||||
|
import com.wbrawner.pihelper.shared.KtorPiholeAPIService
|
||||||
|
import com.wbrawner.pihelper.shared.PiholeAPIService
|
||||||
|
import com.wbrawner.pihelper.shared.commonConfig
|
||||||
|
import io.ktor.client.*
|
||||||
|
import io.ktor.client.engine.android.*
|
||||||
|
|
||||||
|
fun PiholeAPIService.Companion.create() = KtorPiholeAPIService(HttpClient(Android) {
|
||||||
|
commonConfig()
|
||||||
|
})
|
|
@ -0,0 +1,82 @@
|
||||||
|
package com.wbrawner.pihelper.shared
|
||||||
|
|
||||||
|
import io.ktor.client.*
|
||||||
|
import io.ktor.client.call.*
|
||||||
|
import io.ktor.client.engine.*
|
||||||
|
import io.ktor.client.plugins.*
|
||||||
|
import io.ktor.client.request.*
|
||||||
|
import io.ktor.http.*
|
||||||
|
import io.ktor.serialization.kotlinx.json.*
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
|
||||||
|
const val BASE_PATH = "/admin/api.php"
|
||||||
|
|
||||||
|
abstract class PiholeAPIService {
|
||||||
|
abstract var baseUrl: String?
|
||||||
|
abstract var apiKey: String?
|
||||||
|
|
||||||
|
abstract suspend fun getSummary(): Summary
|
||||||
|
|
||||||
|
abstract suspend fun getVersion(): VersionResponse
|
||||||
|
abstract suspend fun getTopItems(): TopItemsResponse
|
||||||
|
abstract suspend fun enable(): StatusResponse
|
||||||
|
abstract suspend fun disable(duration: Long? = null): StatusResponse
|
||||||
|
|
||||||
|
companion object
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T: HttpClientEngineConfig> HttpClientConfig<T>.commonConfig() {
|
||||||
|
install(ContentNegotiation) {
|
||||||
|
json(Json {
|
||||||
|
ignoreUnknownKeys = true
|
||||||
|
isLenient = true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class KtorPiholeAPIService(val httpClient: HttpClient) : PiholeAPIService() {
|
||||||
|
override var baseUrl: String? = null
|
||||||
|
override var apiKey: String? = null
|
||||||
|
|
||||||
|
override suspend fun getSummary(): Summary = httpClient.get {
|
||||||
|
url {
|
||||||
|
host = baseUrl ?: error("baseUrl not set")
|
||||||
|
encodedPath = BASE_PATH
|
||||||
|
}
|
||||||
|
}.body()
|
||||||
|
|
||||||
|
override suspend fun getVersion(): VersionResponse = httpClient.get {
|
||||||
|
url {
|
||||||
|
host = baseUrl ?: error("baseUrl not set")
|
||||||
|
encodedPath = BASE_PATH
|
||||||
|
parameter("version", "")
|
||||||
|
}
|
||||||
|
}.body()
|
||||||
|
|
||||||
|
override suspend fun getTopItems(): TopItemsResponse = httpClient.get {
|
||||||
|
url {
|
||||||
|
host = baseUrl ?: error("baseUrl not set")
|
||||||
|
encodedPath = BASE_PATH
|
||||||
|
parameter("topItems", "25")
|
||||||
|
parameter("auth", apiKey)
|
||||||
|
}
|
||||||
|
}.body()
|
||||||
|
|
||||||
|
override suspend fun enable(): StatusResponse = httpClient.get {
|
||||||
|
url {
|
||||||
|
host = baseUrl ?: error("baseUrl not set")
|
||||||
|
encodedPath = BASE_PATH
|
||||||
|
parameter("enable", "")
|
||||||
|
parameter("auth", apiKey)
|
||||||
|
}
|
||||||
|
}.body()
|
||||||
|
|
||||||
|
override suspend fun disable(duration: Long?): StatusResponse = httpClient.get {
|
||||||
|
url {
|
||||||
|
host = baseUrl ?: error("baseUrl not set")
|
||||||
|
encodedPath = BASE_PATH
|
||||||
|
parameter("disable", duration?.toString()?: "")
|
||||||
|
parameter("auth", apiKey)
|
||||||
|
}
|
||||||
|
}.body()
|
||||||
|
}
|
|
@ -0,0 +1,90 @@
|
||||||
|
package com.wbrawner.pihelper.shared
|
||||||
|
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable()
|
||||||
|
data class Summary(
|
||||||
|
@SerialName("domains_being_blocked")
|
||||||
|
val domainsBeingBlocked: String? = null,
|
||||||
|
@SerialName("dns_queries_today")
|
||||||
|
val dnsQueriesToday: String? = null,
|
||||||
|
@SerialName("ads_blocked_today")
|
||||||
|
val adsBlockedToday: String? = null,
|
||||||
|
@SerialName("ads_percentage_today")
|
||||||
|
val adsPercentageToday: String? = null,
|
||||||
|
@SerialName("unique_domains")
|
||||||
|
val uniqueDomains: String? = null,
|
||||||
|
@SerialName("queries_forwarded")
|
||||||
|
val queriesForwarded: String? = null,
|
||||||
|
@SerialName("clients_ever_seen")
|
||||||
|
val clientsEverSeen: String? = null,
|
||||||
|
@SerialName("unique_clients")
|
||||||
|
val uniqueClients: String? = null,
|
||||||
|
@SerialName("dns_queries_all_types")
|
||||||
|
val dnsQueriesAllTypes: String? = null,
|
||||||
|
@SerialName("queries_cached")
|
||||||
|
val queriesCached: String? = null,
|
||||||
|
@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,
|
||||||
|
@kotlinx.serialization.Transient
|
||||||
|
LOADING,
|
||||||
|
@kotlinx.serialization.Transient
|
||||||
|
UNKNOWN,
|
||||||
|
@kotlinx.serialization.Transient
|
||||||
|
ERROR
|
||||||
|
}
|
||||||
|
|
||||||
|
@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
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
package com.wbrawer.pihelper.shared
|
||||||
|
|
||||||
|
import com.wbrawner.pihelper.shared.KtorPiholeAPIService
|
||||||
|
import com.wbrawner.pihelper.shared.PiholeAPIService
|
||||||
|
import io.ktor.client.*
|
||||||
|
import com.wbrawner.pihelper.shared.commonConfig
|
||||||
|
import io.ktor.client.engine.darwin.*
|
||||||
|
|
||||||
|
fun PiholeAPIService.Companion.create() = KtorPiholeAPIService(HttpClient(Darwin) {
|
||||||
|
commonConfig()
|
||||||
|
})
|
Loading…
Reference in a new issue