From 1347b9ea1021842b2a3220497512037dca44583d Mon Sep 17 00:00:00 2001 From: William Brawner Date: Wed, 1 Jan 2020 14:21:33 -0600 Subject: [PATCH] Implement working Pi-hole connections and management Signed-off-by: William Brawner --- .gitignore | 14 ++ .idea/.name | 1 + .idea/codeStyles/Project.xml | 126 ++++++++++ .idea/codeStyles/codeStyleConfig.xml | 5 + .idea/gradle.xml | 20 ++ .idea/misc.xml | 14 ++ .idea/runConfigurations.xml | 12 + .idea/vcs.xml | 6 + app/.gitignore | 1 + app/build.gradle | 53 +++++ app/google-services.json | 48 ++++ app/proguard-rules.pro | 21 ++ .../pihelper/ExampleInstrumentedTest.kt | 24 ++ app/src/main/AndroidManifest.xml | 60 +++++ app/src/main/ic_launcher-web.png | Bin 0 -> 18535 bytes .../wbrawner/pihelper/AddPiHelperViewModel.kt | 146 ++++++++++++ .../wbrawner/pihelper/AddPiHoleFragment.kt | 65 ++++++ .../java/com/wbrawner/pihelper/Extensions.kt | 3 + .../com/wbrawner/pihelper/InfoFragment.kt | 82 +++++++ .../com/wbrawner/pihelper/MainActivity.kt | 84 +++++++ .../com/wbrawner/pihelper/MainFragment.kt | 218 ++++++++++++++++++ .../wbrawner/pihelper/PiHelperApplication.kt | 39 ++++ .../com/wbrawner/pihelper/PiHelperModule.kt | 33 +++ .../wbrawner/pihelper/PiHelperViewModel.kt | 41 ++++ .../pihelper/RetrieveApiKeyFragment.kt | 68 ++++++ .../wbrawner/pihelper/ScanNetworkFragment.kt | 165 +++++++++++++ .../drawable-anydpi-v24/ic_notification.xml | 19 ++ .../res/drawable-hdpi/ic_notification.png | Bin 0 -> 610 bytes .../res/drawable-mdpi/ic_notification.png | Bin 0 -> 403 bytes .../res/drawable-xhdpi/ic_notification.png | Bin 0 -> 791 bytes .../res/drawable-xxhdpi/ic_notification.png | Bin 0 -> 1149 bytes .../main/res/drawable/background_splash.xml | 12 + app/src/main/res/drawable/horizontal_rule.xml | 11 + app/src/main/res/drawable/ic_app_logo.xml | 18 ++ .../res/drawable/ic_launcher_background.xml | 6 + .../res/drawable/ic_launcher_foreground.xml | 18 ++ app/src/main/res/drawable/ic_pause.xml | 9 + app/src/main/res/drawable/ic_play_arrow.xml | 9 + app/src/main/res/drawable/ic_settings.xml | 9 + .../main/res/drawable/ic_shortcut_enable.xml | 16 ++ .../main/res/drawable/ic_shortcut_pause.xml | 16 ++ app/src/main/res/layout/activity_main.xml | 17 ++ .../res/layout/dialog_disable_custom_time.xml | 44 ++++ .../main/res/layout/fragment_add_pi_hole.xml | 67 ++++++ app/src/main/res/layout/fragment_info.xml | 53 +++++ app/src/main/res/layout/fragment_main.xml | 95 ++++++++ .../res/layout/fragment_retrieve_api_key.xml | 82 +++++++ .../main/res/layout/fragment_scan_network.xml | 35 +++ app/src/main/res/layout/or_divider.xml | 34 +++ app/src/main/res/menu/main.xml | 9 + .../res/mipmap-anydpi-v26/ic_launcher.xml | 5 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 5 + app/src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 2472 bytes .../res/mipmap-hdpi/ic_launcher_round.png | Bin 0 -> 4110 bytes app/src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 1863 bytes .../res/mipmap-mdpi/ic_launcher_round.png | Bin 0 -> 2645 bytes app/src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 3490 bytes .../res/mipmap-xhdpi/ic_launcher_round.png | Bin 0 -> 5889 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 4917 bytes .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin 0 -> 8796 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 6598 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 0 -> 12599 bytes app/src/main/res/navigation/nav_graph.xml | 77 +++++++ app/src/main/res/values-night/colors.xml | 7 + app/src/main/res/values-night/styles.xml | 13 ++ app/src/main/res/values/colors.xml | 17 ++ app/src/main/res/values/strings.xml | 33 +++ app/src/main/res/values/styles.xml | 28 +++ app/src/main/res/xml/backup_descriptor.xml | 4 + app/src/main/res/xml/shortcuts.xml | 62 +++++ .../com/wbrawner/pihelper/ExampleUnitTest.kt | 17 ++ build.gradle | 28 +++ gradle.properties | 21 ++ gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 54329 bytes gradle/wrapper/gradle-wrapper.properties | 6 + gradlew | 172 ++++++++++++++ gradlew.bat | 84 +++++++ piholeclient/.gitignore | 1 + piholeclient/build.gradle | 45 ++++ piholeclient/consumer-rules.pro | 67 ++++++ piholeclient/proguard-rules.pro | 21 ++ .../piholeclient/ExampleInstrumentedTest.kt | 24 ++ piholeclient/src/main/AndroidManifest.xml | 5 + .../wbrawner/piholeclient/PiHoleApiService.kt | 174 ++++++++++++++ .../piholeclient/PiHoleClientModule.kt | 62 +++++ .../com/wbrawner/piholeclient/Responses.kt | 79 +++++++ piholeclient/src/main/res/values/strings.xml | 3 + .../wbrawner/piholeclient/ExampleUnitTest.kt | 17 ++ settings.gradle | 2 + 89 files changed, 3007 insertions(+) create mode 100644 .gitignore create mode 100644 .idea/.name create mode 100644 .idea/codeStyles/Project.xml create mode 100644 .idea/codeStyles/codeStyleConfig.xml create mode 100644 .idea/gradle.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/runConfigurations.xml create mode 100644 .idea/vcs.xml create mode 100644 app/.gitignore create mode 100644 app/build.gradle create mode 100644 app/google-services.json create mode 100644 app/proguard-rules.pro create mode 100644 app/src/androidTest/java/com/wbrawner/pihelper/ExampleInstrumentedTest.kt create mode 100644 app/src/main/AndroidManifest.xml create mode 100644 app/src/main/ic_launcher-web.png create mode 100644 app/src/main/java/com/wbrawner/pihelper/AddPiHelperViewModel.kt create mode 100644 app/src/main/java/com/wbrawner/pihelper/AddPiHoleFragment.kt create mode 100644 app/src/main/java/com/wbrawner/pihelper/Extensions.kt create mode 100644 app/src/main/java/com/wbrawner/pihelper/InfoFragment.kt create mode 100644 app/src/main/java/com/wbrawner/pihelper/MainActivity.kt create mode 100644 app/src/main/java/com/wbrawner/pihelper/MainFragment.kt create mode 100644 app/src/main/java/com/wbrawner/pihelper/PiHelperApplication.kt create mode 100644 app/src/main/java/com/wbrawner/pihelper/PiHelperModule.kt create mode 100644 app/src/main/java/com/wbrawner/pihelper/PiHelperViewModel.kt create mode 100644 app/src/main/java/com/wbrawner/pihelper/RetrieveApiKeyFragment.kt create mode 100644 app/src/main/java/com/wbrawner/pihelper/ScanNetworkFragment.kt create mode 100644 app/src/main/res/drawable-anydpi-v24/ic_notification.xml create mode 100644 app/src/main/res/drawable-hdpi/ic_notification.png create mode 100644 app/src/main/res/drawable-mdpi/ic_notification.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_notification.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_notification.png create mode 100644 app/src/main/res/drawable/background_splash.xml create mode 100644 app/src/main/res/drawable/horizontal_rule.xml create mode 100644 app/src/main/res/drawable/ic_app_logo.xml create mode 100644 app/src/main/res/drawable/ic_launcher_background.xml create mode 100644 app/src/main/res/drawable/ic_launcher_foreground.xml create mode 100644 app/src/main/res/drawable/ic_pause.xml create mode 100644 app/src/main/res/drawable/ic_play_arrow.xml create mode 100644 app/src/main/res/drawable/ic_settings.xml create mode 100644 app/src/main/res/drawable/ic_shortcut_enable.xml create mode 100644 app/src/main/res/drawable/ic_shortcut_pause.xml create mode 100644 app/src/main/res/layout/activity_main.xml create mode 100644 app/src/main/res/layout/dialog_disable_custom_time.xml create mode 100644 app/src/main/res/layout/fragment_add_pi_hole.xml create mode 100644 app/src/main/res/layout/fragment_info.xml create mode 100644 app/src/main/res/layout/fragment_main.xml create mode 100644 app/src/main/res/layout/fragment_retrieve_api_key.xml create mode 100644 app/src/main/res/layout/fragment_scan_network.xml create mode 100644 app/src/main/res/layout/or_divider.xml create mode 100644 app/src/main/res/menu/main.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png create mode 100644 app/src/main/res/navigation/nav_graph.xml create mode 100644 app/src/main/res/values-night/colors.xml create mode 100644 app/src/main/res/values-night/styles.xml create mode 100644 app/src/main/res/values/colors.xml create mode 100644 app/src/main/res/values/strings.xml create mode 100644 app/src/main/res/values/styles.xml create mode 100644 app/src/main/res/xml/backup_descriptor.xml create mode 100644 app/src/main/res/xml/shortcuts.xml create mode 100644 app/src/test/java/com/wbrawner/pihelper/ExampleUnitTest.kt create mode 100644 build.gradle create mode 100644 gradle.properties create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 gradlew.bat create mode 100644 piholeclient/.gitignore create mode 100644 piholeclient/build.gradle create mode 100644 piholeclient/consumer-rules.pro create mode 100644 piholeclient/proguard-rules.pro create mode 100644 piholeclient/src/androidTest/java/com/wbrawner/piholeclient/ExampleInstrumentedTest.kt create mode 100644 piholeclient/src/main/AndroidManifest.xml create mode 100644 piholeclient/src/main/java/com/wbrawner/piholeclient/PiHoleApiService.kt create mode 100644 piholeclient/src/main/java/com/wbrawner/piholeclient/PiHoleClientModule.kt create mode 100644 piholeclient/src/main/java/com/wbrawner/piholeclient/Responses.kt create mode 100644 piholeclient/src/main/res/values/strings.xml create mode 100644 piholeclient/src/test/java/com/wbrawner/piholeclient/ExampleUnitTest.kt create mode 100644 settings.gradle diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..603b140 --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 0000000..db548ff --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +Pi-Helper \ No newline at end of file diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 0000000..6d29e86 --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,126 @@ + + + + + + + + + + + +
+ + + + xmlns:android + + ^$ + + + +
+
+ + + + xmlns:.* + + ^$ + + + BY_NAME + +
+
+ + + + .*:id + + http://schemas.android.com/apk/res/android + + + +
+
+ + + + .*:name + + http://schemas.android.com/apk/res/android + + + +
+
+ + + + name + + ^$ + + + +
+
+ + + + style + + ^$ + + + +
+
+ + + + .* + + ^$ + + + BY_NAME + +
+
+ + + + .* + + http://schemas.android.com/apk/res/android + + + ANDROID_ATTRIBUTE_ORDER + +
+
+ + + + .* + + .* + + + BY_NAME + +
+
+
+
+ + +
+
\ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 0000000..79ee123 --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..da811ea --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,20 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..703e5d4 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml new file mode 100644 index 0000000..7f68460 --- /dev/null +++ b/.idea/runConfigurations.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..850bdaf --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,53 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-android-extensions' + +android { + compileSdkVersion 29 + defaultConfig { + applicationId "com.wbrawner.pihelper" + minSdkVersion 23 + targetSdkVersion 29 + versionCode 1 + versionName "1.0" + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + signingConfig signingConfigs.debug + } + buildTypes { + release { + minifyEnabled true + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } +} + +dependencies { + implementation project(':piholeclient') + 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" + implementation 'androidx.appcompat:appcompat:1.1.0' + implementation 'androidx.core:core-ktx:1.1.0' + implementation 'androidx.constraintlayout:constraintlayout:1.1.3' + implementation 'com.google.android.material:material:1.2.0-alpha02' + implementation 'androidx.legacy:legacy-support-v4:1.0.0' + implementation 'androidx.security:security-crypto:1.0.0-alpha02' + implementation 'androidx.preference:preference:1.1.0' + testImplementation 'junit:junit:4.12' + androidTestImplementation 'androidx.test.ext:junit:1.1.1' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' + def navigation_version = '2.1.0' + implementation "androidx.navigation:navigation-fragment-ktx:$navigation_version" + implementation "androidx.navigation:navigation-ui-ktx:$navigation_version" + def lifecycle_version = '2.1.0' + implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version" + implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version" + def acraVersion = '5.1.3' + implementation "ch.acra:acra-mail:$acraVersion" + implementation "ch.acra:acra-notification:$acraVersion" + implementation "ch.acra:acra-limiter:$acraVersion" +} diff --git a/app/google-services.json b/app/google-services.json new file mode 100644 index 0000000..2a61ab2 --- /dev/null +++ b/app/google-services.json @@ -0,0 +1,48 @@ +{ + "project_info": { + "project_number": "511023365625", + "firebase_url": "https://pi-helper-d3885.firebaseio.com", + "project_id": "pi-helper-d3885", + "storage_bucket": "pi-helper-d3885.appspot.com" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:511023365625:android:f8d76275e55f6db4efd128", + "android_client_info": { + "package_name": "com.wbrawner.pihelper" + } + }, + "oauth_client": [ + { + "client_id": "511023365625-d5fi76hu5mu838tblaahrfdudjdlels8.apps.googleusercontent.com", + "client_type": 1, + "android_info": { + "package_name": "com.wbrawner.pihelper", + "certificate_hash": "e705f9dfdbd6ab7a79c902bc94c8448030f52c89" + } + }, + { + "client_id": "511023365625-k0t6fbagmsgdveg6e673detjf75fmi2l.apps.googleusercontent.com", + "client_type": 3 + } + ], + "api_key": [ + { + "current_key": "AIzaSyDv9-WRa4xWFswXr5qBWVaAzA-UOraAsHc" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [ + { + "client_id": "511023365625-k0t6fbagmsgdveg6e673detjf75fmi2l.apps.googleusercontent.com", + "client_type": 3 + } + ] + } + } + } + ], + "configuration_version": "1" +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..462dd20 --- /dev/null +++ b/app/proguard-rules.pro @@ -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. +# +# 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 diff --git a/app/src/androidTest/java/com/wbrawner/pihelper/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/wbrawner/pihelper/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..aea9a83 --- /dev/null +++ b/app/src/androidTest/java/com/wbrawner/pihelper/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.wbrawner.pihelper + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.wbrawner.pihelper", appContext.packageName) + } +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..fb11a14 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/ic_launcher-web.png b/app/src/main/ic_launcher-web.png new file mode 100644 index 0000000000000000000000000000000000000000..dc8c839591685f7df2f3091cf9e9991fcfb82f74 GIT binary patch literal 18535 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 literal 0 HcmV?d00001 diff --git a/app/src/main/java/com/wbrawner/pihelper/AddPiHelperViewModel.kt b/app/src/main/java/com/wbrawner/pihelper/AddPiHelperViewModel.kt new file mode 100644 index 0000000..1bbf755 --- /dev/null +++ b/app/src/main/java/com/wbrawner/pihelper/AddPiHelperViewModel.kt @@ -0,0 +1,146 @@ +package com.wbrawner.pihelper + +import android.content.SharedPreferences +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.Summary +import com.wbrawner.piholeclient.VersionResponse +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.withContext +import java.net.ConnectException +import java.net.SocketTimeoutException +import java.util.regex.Pattern +import java.util.regex.Pattern.DOTALL + +const val KEY_BASE_URL = "baseUrl" +const val KEY_API_KEY = "apiKey" +const val IP_MIN = 0 +const val IP_MAX = 255 + +class AddPiHelperViewModel( + private val sharedPreferences: SharedPreferences, + private val apiService: PiHoleApiService +) : ViewModel() { + + @Volatile + var baseUrl: String? = sharedPreferences.getString(KEY_BASE_URL, null) + set(value) { + sharedPreferences.edit { + putString(KEY_BASE_URL, value) + } + field = value + } + + @Volatile + var apiKey: String? = sharedPreferences.getString(KEY_API_KEY, null) + set(value) { + sharedPreferences.edit { + putString(KEY_API_KEY, value) + } + field = value + } + + init { + apiService.baseUrl = this.baseUrl + apiService.apiKey = this.apiKey + } + + val piHoleIpAddress = MutableLiveData() + val scanningIp = MutableLiveData() + val authenticated = MutableLiveData() + + suspend fun beginScanning(deviceIpAddress: String) { + val addressParts = deviceIpAddress.split(".").toMutableList() + var chunks = 1 + val ipAddresses = mutableListOf() + while (chunks <= IP_MAX) { + val chunkSize = (IP_MAX - IP_MIN + 1) / chunks + if (chunkSize == 1) { + return + } + for (chunk in 0 until chunks) { + val chunkStart = IP_MIN + (chunk * chunkSize) + val chunkEnd = IP_MIN + ((chunk + 1) * chunkSize) + addressParts[3] = (((chunkEnd - chunkStart) / 2) + chunkStart).toString() + ipAddresses.add(addressParts.joinToString(".")) + } + chunks *= 2 + } + scan(ipAddresses) + } + + private suspend fun scan(ipAddresses: MutableList) { + if (ipAddresses.isEmpty()) { + scanningIp.postValue(null) + piHoleIpAddress.postValue(null) + return + } + + val ipAddress = ipAddresses.removeAt(0) + scanningIp.postValue(ipAddress) + if (!connectToIpAddress(ipAddress)) { + scan(ipAddresses) + } + } + + suspend fun connectToIpAddress(ipAddress: String): Boolean { + val version: VersionResponse? = withContext(Dispatchers.IO) { + try { + apiService.baseUrl = ipAddress + apiService.getVersion() + } catch (ignored: ConnectException) { + null + } catch (ignored: SocketTimeoutException) { + null + } catch (e: Exception) { + Log.e("Pi-Helper", "Failed to load Pi-Hole version at $ipAddress", e) + null + } + } + return if (version == null) { + false + } else { + piHoleIpAddress.postValue(ipAddress) + baseUrl = ipAddress + true + } + } + + suspend fun authenticateWithPassword(password: String) { + // Perform the login to get the PHPSESSID cookie set + apiService.login(password) + val html = apiService.getApiToken() + val matcher = Pattern.compile(".*Raw API Token: ([a-z0-9]+).*", DOTALL) + .matcher(html) + if (!matcher.matches()) { + throw RuntimeException("Unable to retrieve API token from password") + } + val apiToken = matcher.group(1)!! + authenticateWithApiKey(apiToken) + } + + suspend fun authenticateWithApiKey(apiKey: String) { + // This uses the topItems endpoint to test that the API key is working since it requires + // authentication and is fairly simple to determine whether or not the request was + // successful + apiService.apiKey = apiKey + try { + apiService.getTopItems() + this.apiKey = apiKey + authenticated.postValue(true) + } catch (e: Exception) { + Log.e("Pi-Helper", "Unable to authenticate with API key", e) + authenticated.postValue(false) + throw e + } + } + + fun forgetPihole() { + baseUrl = null + apiKey = null + } +} \ No newline at end of file diff --git a/app/src/main/java/com/wbrawner/pihelper/AddPiHoleFragment.kt b/app/src/main/java/com/wbrawner/pihelper/AddPiHoleFragment.kt new file mode 100644 index 0000000..1c703c8 --- /dev/null +++ b/app/src/main/java/com/wbrawner/pihelper/AddPiHoleFragment.kt @@ -0,0 +1,65 @@ +package com.wbrawner.pihelper + + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.app.AlertDialog +import androidx.fragment.app.Fragment +import androidx.navigation.NavOptionsBuilder +import androidx.navigation.fragment.FragmentNavigatorExtras +import androidx.navigation.fragment.findNavController +import kotlinx.android.synthetic.main.fragment_add_pi_hole.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import org.koin.android.ext.android.inject +import kotlin.coroutines.CoroutineContext + +class AddPiHoleFragment : Fragment(), CoroutineScope { + + override val coroutineContext: CoroutineContext = Dispatchers.Main + private val viewModel: AddPiHelperViewModel by inject() + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? = inflater.inflate(R.layout.fragment_add_pi_hole, container, false) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + val navController = findNavController() + scanNetworkButton.setOnClickListener { + navController.navigate( + R.id.action_addPiHoleFragment_to_scanNetworkFragment, + null, + null, + FragmentNavigatorExtras(piHelperLogo to "piHelperLogo") + ) + } + connectButton.setOnClickListener { + launch { + if (viewModel.connectToIpAddress(ipAddress.text.toString())) { + navController.navigate( + R.id.action_addPiHoleFragment_to_retrieveApiKeyFragment, + null, + null, + FragmentNavigatorExtras(piHelperLogo to "piHelperLogo") + ) + } else { + AlertDialog.Builder(view.context) + .setTitle(R.string.connection_failed_title) + .setMessage(R.string.connection_failed) + .setPositiveButton(android.R.string.ok) { _, _ -> } + .show() + } + } + } + } + + override fun onDestroyView() { + coroutineContext[Job]?.cancel() + super.onDestroyView() + } +} diff --git a/app/src/main/java/com/wbrawner/pihelper/Extensions.kt b/app/src/main/java/com/wbrawner/pihelper/Extensions.kt new file mode 100644 index 0000000..ab54674 --- /dev/null +++ b/app/src/main/java/com/wbrawner/pihelper/Extensions.kt @@ -0,0 +1,3 @@ +package com.wbrawner.pihelper + +fun R.transform(block: (R) -> T): T = block(this) \ No newline at end of file diff --git a/app/src/main/java/com/wbrawner/pihelper/InfoFragment.kt b/app/src/main/java/com/wbrawner/pihelper/InfoFragment.kt new file mode 100644 index 0000000..e27795b --- /dev/null +++ b/app/src/main/java/com/wbrawner/pihelper/InfoFragment.kt @@ -0,0 +1,82 @@ +package com.wbrawner.pihelper + + +import android.content.Intent +import android.os.Build +import android.os.Bundle +import android.text.Html +import android.text.method.LinkMovementMethod +import android.view.LayoutInflater +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.Fragment +import androidx.navigation.fragment.findNavController +import com.wbrawner.pihelper.MainActivity.Companion.ACTION_FORGET_PIHOLE +import kotlinx.android.synthetic.main.fragment_main.toolbar +import kotlinx.android.synthetic.main.fragment_info.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import org.koin.android.ext.android.inject +import java.lang.RuntimeException +import kotlin.coroutines.CoroutineContext + +class InfoFragment : Fragment(), CoroutineScope { + override val coroutineContext: CoroutineContext = Dispatchers.Main + private val viewModel: AddPiHelperViewModel by inject() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setHasOptionsMenu(true) + throw RuntimeException("I crashed!") + } + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? = inflater.inflate(R.layout.fragment_info, container, false) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + (activity as? AppCompatActivity)?.setSupportActionBar(toolbar) + (activity as? AppCompatActivity)?.supportActionBar?.setDisplayHomeAsUpEnabled(true) + (activity as? AppCompatActivity)?.supportActionBar?.setTitle(R.string.action_settings) + val html = getString(R.string.content_info) + @Suppress("DEPRECATION") + infoContent?.text = if (Build.VERSION.SDK_INT < 24) + Html.fromHtml(html) + else + Html.fromHtml(html, 0) + infoContent.movementMethod = LinkMovementMethod.getInstance() + forgetPiHoleButton?.setOnClickListener { + AlertDialog.Builder(view.context) + .setTitle(R.string.confirm_forget_pihole) + .setMessage(R.string.warning_cannot_be_undone) + .setNegativeButton(android.R.string.cancel) { _, _ -> } + .setPositiveButton(R.string.action_forget_pihole) { _, _ -> + viewModel.forgetPihole() + val refreshIntent = Intent( + view.context.applicationContext, + MainActivity::class.java + ).apply { + action = ACTION_FORGET_PIHOLE + addFlags( + Intent.FLAG_ACTIVITY_CLEAR_TASK + and Intent.FLAG_ACTIVITY_NEW_TASK + ) + } + activity?.startActivity(refreshIntent) + } + .show() + } + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + if (item.itemId == android.R.id.home) { + findNavController().navigateUp() + return true + } + return super.onOptionsItemSelected(item) + } +} diff --git a/app/src/main/java/com/wbrawner/pihelper/MainActivity.kt b/app/src/main/java/com/wbrawner/pihelper/MainActivity.kt new file mode 100644 index 0000000..6b6629f --- /dev/null +++ b/app/src/main/java/com/wbrawner/pihelper/MainActivity.kt @@ -0,0 +1,84 @@ +package com.wbrawner.pihelper + +import android.graphics.drawable.ColorDrawable +import android.os.Bundle +import android.widget.Toast +import androidx.appcompat.app.AppCompatActivity +import androidx.navigation.findNavController +import com.wbrawner.pihelper.MainFragment.Companion.ACTION_DISABLE +import com.wbrawner.pihelper.MainFragment.Companion.ACTION_ENABLE +import com.wbrawner.pihelper.MainFragment.Companion.EXTRA_DURATION +import org.koin.android.ext.android.inject + +class MainActivity : AppCompatActivity() { + private val addPiHoleViewModel: AddPiHelperViewModel by inject() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + window.setBackgroundDrawable(ColorDrawable(getColor(R.color.colorSurface))) + val navController = findNavController(R.id.content_main) + val analyticsBundle = Bundle() + analyticsBundle.putString("intent_action", intent.action) + val args = when (intent.action) { + ACTION_ENABLE -> { + if (addPiHoleViewModel.apiKey == null) { + Toast.makeText(this, R.string.configure_pihelper, Toast.LENGTH_SHORT).show() + null + } else { + Bundle().apply { putBoolean(ACTION_ENABLE, true) } + } + } + ACTION_DISABLE -> { + if (addPiHoleViewModel.apiKey == null) { + Toast.makeText(this, R.string.configure_pihelper, Toast.LENGTH_SHORT).show() + null + } else { + Bundle().apply { + putBoolean(ACTION_DISABLE, true) + putLong(EXTRA_DURATION, intent.getIntExtra(EXTRA_DURATION, 10).toLong()) + } + } + } + ACTION_FORGET_PIHOLE -> { + if (intent.component?.packageName == packageName) { + while (navController.popBackStack()) { + // Do nothing, just pop all the items off the back stack + } + // Just return an empty bundle so that the navigation branch below will load + // the correct screen + Bundle() + } else { + null + } + } + else -> null + } + val navDestination = when { + navController.currentDestination?.id != R.id.placeholder && args == null -> { + return + } + addPiHoleViewModel.baseUrl.isNullOrBlank() -> { + R.id.addPiHoleFragment + } + addPiHoleViewModel.apiKey.isNullOrBlank() -> { + R.id.retrieveApiKeyFragment + } + else -> { + R.id.mainFragment + } + } + navController.navigate(navDestination, args) + } + + override fun onBackPressed() { + when (findNavController(R.id.content_main).currentDestination?.id) { + R.id.addPiHoleFragment, R.id.mainFragment -> finish() + else -> super.onBackPressed() + } + } + + companion object { + const val ACTION_FORGET_PIHOLE = "com.wbrawner.pihelper.ACTION_FORGET_PIHOLE" + } +} diff --git a/app/src/main/java/com/wbrawner/pihelper/MainFragment.kt b/app/src/main/java/com/wbrawner/pihelper/MainFragment.kt new file mode 100644 index 0000000..1f877b4 --- /dev/null +++ b/app/src/main/java/com/wbrawner/pihelper/MainFragment.kt @@ -0,0 +1,218 @@ +package com.wbrawner.pihelper + + +import android.graphics.Typeface +import android.os.Bundle +import android.text.SpannableString +import android.text.style.ForegroundColorSpan +import android.text.style.StyleSpan +import android.util.Log +import android.view.* +import android.view.animation.Animation +import android.view.animation.LinearInterpolator +import android.view.animation.RotateAnimation +import android.widget.EditText +import android.widget.RadioGroup +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.app.AppCompatActivity +import androidx.core.content.ContextCompat.getColor +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 kotlinx.android.synthetic.main.fragment_main.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import org.koin.android.ext.android.inject +import kotlin.coroutines.CoroutineContext + +class MainFragment : Fragment(), CoroutineScope { + override val coroutineContext: CoroutineContext = Dispatchers.Main + private val viewModel: PiHelperViewModel by inject() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setHasOptionsMenu(true) + launch { + if (arguments?.getBoolean(ACTION_ENABLE) == true) { + viewModel.enablePiHole() + } else if (arguments?.getBoolean(ACTION_DISABLE) == true) { + viewModel.disablePiHole(arguments?.getLong(EXTRA_DURATION)) + } + viewModel.monitorSummary() + } + viewModel.summary.observe(this, Observer { summary -> + showProgress(false) + val (statusColor, statusText) = if ( + summary.status == Status.DISABLED + ) { + enableButton?.visibility = View.VISIBLE + disableButtons?.visibility = View.GONE + Pair(R.color.colorDisabled, R.string.status_disabled) + } else { + enableButton?.visibility = View.GONE + disableButtons?.visibility = View.VISIBLE + Pair(R.color.colorEnabled, R.string.status_enabled) + } + status?.let { + val status = getString(statusText) + val statusLabel = getString(R.string.label_status, status) + val start = statusLabel.indexOf(status) + val end = start + status.length + val statusSpan = SpannableString(statusLabel) + statusSpan[start, end] = StyleSpan(Typeface.BOLD) + statusSpan[start, end] = ForegroundColorSpan(getColor(it.context, statusColor)) + it.text = statusSpan + } + }) + } + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? = inflater.inflate(R.layout.fragment_main, container, false) + + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + inflater.inflate(R.menu.main, menu) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + (activity as? AppCompatActivity)?.setSupportActionBar(toolbar) + showProgress(true) + enableButton?.setOnClickListener { + launch { + showProgress(true) + try { + viewModel.enablePiHole() + } catch (ignored: Exception) { + Log.e("Pi-Helper", "Failed to enable Pi-Hole", ignored) + } + } + } + disable10SecondsButton?.setOnClickListener { + launch { + showProgress(true) + try { + viewModel.disablePiHole(10) + } catch (ignored: Exception) { + Log.e("Pi-Helper", "Failed to disable Pi-Hole", ignored) + } + } + } + disable30SecondsButton?.setOnClickListener { + launch { + showProgress(true) + try { + viewModel.disablePiHole(30) + } catch (ignored: Exception) { + Log.e("Pi-Helper", "Failed to disable Pi-Hole", ignored) + } + } + } + disable5MinutesButton?.setOnClickListener { + launch { + showProgress(true) + try { + viewModel.disablePiHole(300) + } catch (ignored: Exception) { + Log.e("Pi-Helper", "Failed to disable Pi-Hole", ignored) + } + } + } + disableCustomTimeButton?.setOnClickListener { + val dialogView = LayoutInflater.from(it.context) + .inflate(R.layout.dialog_disable_custom_time, null, false) + AlertDialog.Builder(it.context) + .setTitle(R.string.action_disable_custom) + .setNegativeButton(android.R.string.cancel) { _, _ -> } + .setPositiveButton(R.string.action_disable, null) + .setView(dialogView) + .create() + .apply { + setOnShowListener { + getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener { + launch { + try { + val rawTime = dialogView.findViewById(R.id.time) + .text + .toString() + .toLong() + val checkedId = + dialogView.findViewById(R.id.timeUnit) + .checkedRadioButtonId + val computedTime = if (checkedId == R.id.seconds) rawTime + else rawTime * 60 + viewModel.disablePiHole(computedTime) + dismiss() + } catch (e: Exception) { + dialogView.findViewById(R.id.time) + .error = "Failed to disable Pi-hole" + } + } + } + } + } + .show() + } + disablePermanentlyButton?.setOnClickListener { + launch { + showProgress(true) + try { + viewModel.disablePiHole() + } catch (ignored: Exception) { + Log.e("Pi-Helper", "Failed to disable Pi-Hole", ignored) + } + } + } + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + if (item.itemId == R.id.settings) { + findNavController().navigate(R.id.action_mainFragment_to_settingsFragment) + return true + } + return super.onOptionsItemSelected(item) + } + + private fun showProgress(show: Boolean) { + progressBar?.visibility = if (show) { + progressBar?.startAnimation(RotateAnimation( + 0f, + 360f, + Animation.RELATIVE_TO_SELF, + 0.5f, + Animation.RELATIVE_TO_SELF, + 0.5f + ).apply { + duration = resources.getInteger(android.R.integer.config_longAnimTime).toLong() * 2 + repeatMode = Animation.RESTART + repeatCount = Animation.INFINITE + interpolator = LinearInterpolator() + fillAfter = true + }) + View.VISIBLE + } else { + progressBar?.clearAnimation() + View.GONE + } + statusContent?.visibility = if (show) { + View.GONE + } else { + View.VISIBLE + } + } + + override fun onDestroyView() { + coroutineContext[Job]?.cancel() + super.onDestroyView() + } + + companion object { + const val ACTION_DISABLE = "com.wbrawner.pihelper.MainFragment.ACTION_DISABLE" + const val ACTION_ENABLE = "com.wbrawner.pihelper.MainFragment.ACTION_ENABLE" + const val EXTRA_DURATION = "com.wbrawner.pihelper.MainFragment.EXTRA_DURATION" + } +} diff --git a/app/src/main/java/com/wbrawner/pihelper/PiHelperApplication.kt b/app/src/main/java/com/wbrawner/pihelper/PiHelperApplication.kt new file mode 100644 index 0000000..efb4cea --- /dev/null +++ b/app/src/main/java/com/wbrawner/pihelper/PiHelperApplication.kt @@ -0,0 +1,39 @@ +package com.wbrawner.pihelper + +import android.app.Application +import android.content.Context +import com.wbrawner.piholeclient.piHoleClientModule +import org.acra.ACRA +import org.acra.annotation.AcraCore +import org.acra.annotation.AcraMailSender +import org.acra.annotation.AcraNotification +import org.koin.android.ext.koin.androidContext +import org.koin.android.ext.koin.androidLogger +import org.koin.core.context.startKoin + +@AcraCore(buildConfigClass = BuildConfig::class) +@AcraMailSender(mailTo = "pihelper@wbrawner.com") +@AcraNotification( + resIcon = R.drawable.ic_notification, + resTitle = R.string.title_crash_notification, + resText = R.string.text_crash_notification, + resChannelName = R.string.channel_crash_notification +) +class PiHelperApplication: Application() { + override fun onCreate() { + super.onCreate() + startKoin{ + androidLogger() + androidContext(this@PiHelperApplication) + modules(listOf( + piHoleClientModule, + piHelperModule + )) + } + } + + override fun attachBaseContext(base: Context?) { + super.attachBaseContext(base) + ACRA.init(this) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/wbrawner/pihelper/PiHelperModule.kt b/app/src/main/java/com/wbrawner/pihelper/PiHelperModule.kt new file mode 100644 index 0000000..adf00c6 --- /dev/null +++ b/app/src/main/java/com/wbrawner/pihelper/PiHelperModule.kt @@ -0,0 +1,33 @@ +package com.wbrawner.pihelper + +import androidx.security.crypto.EncryptedSharedPreferences +import com.wbrawner.piholeclient.NAME_BASE_URL +import org.koin.androidx.viewmodel.dsl.viewModel +import org.koin.core.qualifier.named +import org.koin.dsl.module + +const val ENCRYPTED_SHARED_PREFS_FILE_NAME = "pihelper.prefs" + +val piHelperModule = module { + single { + EncryptedSharedPreferences.create( + ENCRYPTED_SHARED_PREFS_FILE_NAME, + "pihelper", + get(), + EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, + EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM + ) + } + + viewModel { + AddPiHelperViewModel(get(), get()) + } + + viewModel { + PiHelperViewModel(get()) + } + + single(named(NAME_BASE_URL)) { + get().getString(KEY_BASE_URL, "") + } +} \ No newline at end of file diff --git a/app/src/main/java/com/wbrawner/pihelper/PiHelperViewModel.kt b/app/src/main/java/com/wbrawner/pihelper/PiHelperViewModel.kt new file mode 100644 index 0000000..fd4a1da --- /dev/null +++ b/app/src/main/java/com/wbrawner/pihelper/PiHelperViewModel.kt @@ -0,0 +1,41 @@ +package com.wbrawner.pihelper + +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import com.wbrawner.piholeclient.PiHoleApiService +import com.wbrawner.piholeclient.Summary +import kotlinx.coroutines.NonCancellable.isActive +import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive +import kotlinx.coroutines.yield +import java.lang.Exception +import kotlin.coroutines.coroutineContext + +class PiHelperViewModel( + private val apiService: PiHoleApiService +) : ViewModel() { + val summary = MutableLiveData() + + suspend fun monitorSummary() { + while (coroutineContext.isActive) { + try { + loadSummary() + } catch (ignored: Exception) { + break + } + delay(1000) + } + } + + suspend fun loadSummary() { + summary.postValue(apiService.getSummary()) + } + + suspend fun enablePiHole() { + apiService.enable() + } + + suspend fun disablePiHole(duration: Long? = null) { + apiService.disable(duration) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/wbrawner/pihelper/RetrieveApiKeyFragment.kt b/app/src/main/java/com/wbrawner/pihelper/RetrieveApiKeyFragment.kt new file mode 100644 index 0000000..a6d2c3b --- /dev/null +++ b/app/src/main/java/com/wbrawner/pihelper/RetrieveApiKeyFragment.kt @@ -0,0 +1,68 @@ +package com.wbrawner.pihelper + + +import android.os.Bundle +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.lifecycle.Observer +import androidx.navigation.fragment.findNavController +import androidx.transition.TransitionInflater +import kotlinx.android.synthetic.main.fragment_retrieve_api_key.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import org.koin.android.ext.android.inject +import kotlin.coroutines.CoroutineContext + +class RetrieveApiKeyFragment : Fragment(), CoroutineScope { + override val coroutineContext: CoroutineContext = Dispatchers.Main + private val viewModel: AddPiHelperViewModel by inject() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + sharedElementEnterTransition = TransitionInflater.from(context) + .inflateTransition(android.R.transition.move) + viewModel.authenticated.observe(this, Observer { + findNavController().navigate(R.id.action_retrieveApiKeyFragment_to_mainFragment) + }) + } + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? = inflater.inflate(R.layout.fragment_retrieve_api_key, container, false) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + connectWithPasswordButton.setOnClickListener { + launch { + try { + viewModel.authenticateWithPassword(password.text.toString()) + } catch (ignored: Exception) { + Log.e("Pi-Helper", "Failed to authenticate with password", ignored) + password.error = "Failed to authenticate with given password. Please verify " + + "you've entered it correctly and try again." + } + } + } + + connectWithApiKeyButton.setOnClickListener { + launch { + try { + viewModel.authenticateWithApiKey(apiKey.text.toString()) + } catch (ignored: Exception) { + apiKey.error = "Failed to authenticate with given API key. Please verify " + + "you've entered it correctly and try again." + } + } + } + } + + override fun onDestroyView() { + coroutineContext[Job]?.cancel() + super.onDestroyView() + } +} diff --git a/app/src/main/java/com/wbrawner/pihelper/ScanNetworkFragment.kt b/app/src/main/java/com/wbrawner/pihelper/ScanNetworkFragment.kt new file mode 100644 index 0000000..e3b71b2 --- /dev/null +++ b/app/src/main/java/com/wbrawner/pihelper/ScanNetworkFragment.kt @@ -0,0 +1,165 @@ +package com.wbrawner.pihelper + + +import android.content.Context +import android.net.ConnectivityManager +import android.net.NetworkCapabilities +import android.os.Build +import android.os.Bundle +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.view.animation.Animation +import android.view.animation.LinearInterpolator +import android.view.animation.RotateAnimation +import androidx.appcompat.app.AlertDialog +import androidx.fragment.app.Fragment +import androidx.lifecycle.Observer +import androidx.navigation.fragment.FragmentNavigatorExtras +import androidx.navigation.fragment.findNavController +import androidx.transition.Transition +import androidx.transition.TransitionInflater +import kotlinx.android.synthetic.main.fragment_scan_network.* +import kotlinx.coroutines.* +import org.koin.android.ext.android.inject +import java.net.Inet4Address +import kotlin.coroutines.CoroutineContext + +class ScanNetworkFragment : Fragment(), CoroutineScope { + + override val coroutineContext: CoroutineContext = Dispatchers.Main + private val viewModel: AddPiHelperViewModel by inject() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + sharedElementEnterTransition = TransitionInflater.from(context) + .inflateTransition(android.R.transition.move) + .addListener(object : Transition.TransitionListener { + override fun onTransitionEnd(transition: Transition) { + animatePiHelperLogo() + } + + override fun onTransitionResume(transition: Transition) { + } + + override fun onTransitionPause(transition: Transition) { + } + + override fun onTransitionCancel(transition: Transition) { + } + + override fun onTransitionStart(transition: Transition) { + } + }) + } + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? = inflater.inflate(R.layout.fragment_scan_network, container, false) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + viewModel.scanningIp.observe(this, Observer { + ipAddress?.text = it + }) + viewModel.piHoleIpAddress.observe(this, Observer { ipAddress -> + if (ipAddress == null) { + AlertDialog.Builder(view.context) + .setTitle(R.string.scan_failed_title) + .setMessage(R.string.scan_failed) + .setPositiveButton(android.R.string.ok) { _, _ -> + findNavController().navigateUp() + } + .show() + return@Observer + } + piHelperLogo?.animation?.let { + it.setAnimationListener(object : Animation.AnimationListener { + override fun onAnimationRepeat(animation: Animation?) { + } + + override fun onAnimationEnd(animation: Animation?) { + navigateToApiKeyScreen() + } + + override fun onAnimationStart(animation: Animation?) { + } + }) + it.repeatCount = 0 + } ?: navigateToApiKeyScreen() + }) + launch(Dispatchers.IO) { + if (BuildConfig.DEBUG && Build.MODEL == "Android SDK built for x86") { + // For emulators, just begin scanning the host machine directly + viewModel.beginScanning("10.0.2.2") + return@launch + } + (view.context.getSystemService(Context.CONNECTIVITY_SERVICE) as? ConnectivityManager)?.let { connectivityManager -> + connectivityManager.allNetworks + .filter { + connectivityManager.getNetworkCapabilities(it) + ?.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) + ?: false + } + .forEach { network -> + connectivityManager.getLinkProperties(network) + ?.linkAddresses + ?.filter { !it.address.isLoopbackAddress && it.address is Inet4Address } + ?.forEach { address -> + Log.d( + "Pi-Helper", + "Found link address: ${address.address.hostName}" + ) + viewModel.beginScanning(address.address.hostAddress) + } + } + } + } + launch { + delay(500) + if (piHelperLogo?.animation == null) { + animatePiHelperLogo() + } + } + } + + private fun navigateToApiKeyScreen() { + val extras = FragmentNavigatorExtras( + piHelperLogo to "piHelperLogo" + ) + + findNavController().navigate( + R.id.action_scanNetworkFragment_to_retrieveApiKeyFragment, + null, + null, + extras + ) + } + + override fun onDestroyView() { + piHelperLogo.clearAnimation() + coroutineContext[Job]?.cancel() + super.onDestroyView() + } + + private fun animatePiHelperLogo() { + piHelperLogo?.startAnimation( + RotateAnimation( + 0f, + 360f, + Animation.RELATIVE_TO_SELF, + 0.5f, + Animation.RELATIVE_TO_SELF, + 0.5f + ).apply { + duration = resources.getInteger(android.R.integer.config_longAnimTime).toLong() * 2 + repeatMode = Animation.RESTART + repeatCount = Animation.INFINITE + interpolator = LinearInterpolator() + fillAfter = true + } + ) + } +} diff --git a/app/src/main/res/drawable-anydpi-v24/ic_notification.xml b/app/src/main/res/drawable-anydpi-v24/ic_notification.xml new file mode 100644 index 0000000..387fdb5 --- /dev/null +++ b/app/src/main/res/drawable-anydpi-v24/ic_notification.xml @@ -0,0 +1,19 @@ + + + + + diff --git a/app/src/main/res/drawable-hdpi/ic_notification.png b/app/src/main/res/drawable-hdpi/ic_notification.png new file mode 100644 index 0000000000000000000000000000000000000000..d7b4a7aa6790b95eb5a86c86b23c2c425a3db886 GIT binary patch literal 610 zcmV-o0-gPdP)hy)fNI(G5=|aV8Ze@)WrNp zB!U6UYp93$cS!{GOt4}h&0sS)$jdef)T_*DE`nDjIAmiVL8Ch)p^ILU{)4QtH&DI<@40K0pNptkc zt_*L0Lbtu5P5D=spj+H;A}qpN4t6LU?*IS*07*qoM6N<$g8z9K3;+NC literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-mdpi/ic_notification.png b/app/src/main/res/drawable-mdpi/ic_notification.png new file mode 100644 index 0000000000000000000000000000000000000000..e6c7273b004b6ef21349d4aaa95f15a37f9c5a84 GIT binary patch literal 403 zcmV;E0c`$>P)Y6h$}DWS3y!53my(3rRY$Q?L+{LeVrLXe+iMf5|2wqNQjPNhLO7AsQ@#MJxmr zKa=r3xZ)ZzFO%6$ADm|QymxnYnB5@w(`AfFfmN^zTIQ!(ye9WjzQgn2%>1rf5-)M` z!wxTkCk|+UWsr;A&+%G=j<`kF;RWyvK0(#NuhNyC!XP;f&iPc8Ay!;rkMyQBu}rb< zv1fJmmNltHspJt*VsA~8+TayL9sv=1+cqHtUC>N$Zkn7C+5$QW1Fjhm*#tSEZy@V2 zJmf37*QEXLO$HNS8e|*;me{+eNh_2-VxNLLu;mbNOn+UI=8e5JmA)N17hpsSI8_2< ze@6TCY;(mgU{)KR^=B~MXhaLw6+<>bd?^1p8o{$6MRuXUsHu}9b xEnw2Jig&O8#=%(9-{cHe^1xGh0_K8$TVJn@!#dN&lUV=&002ovPDHLkV1l$Est^DG literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xhdpi/ic_notification.png b/app/src/main/res/drawable-xhdpi/ic_notification.png new file mode 100644 index 0000000000000000000000000000000000000000..d16b224acb2ee8d5b3eab2d24523e2e3a36cdfaa GIT binary patch literal 791 zcmV+y1L*vTP)&Lickv^ znsj*5Pcq*vDk5`Mi=jqm4L*sW1J79uM`aoxV2u+Ibl^k4{}$LQdm;vttHlU9 z5IjUzbzdSe0+kMfKf)5bPbwn!*fQ#K*nt7&`>u+}MJDnFlz=vH8C(VD!9fchc*}gZ zs+LUF**zc`as~VY9)pvHf5 zG;5x%S$v!U??c*&o$i$GPEc#0Xxkg2TLrF!)R}_!-_s`c;%WmO5ZmE>@Q@0;^OTZg zdj7D<@y#(7QTXgloB5YjpDZ# zg;803Mk4qh;Dnb;MxT_P?jZP&;Fy=IhTzVDN8mSm5nqE#PS&*HS*h^ymbcw+{{V{k V*gHSBGQ9u*002ovPDHLkV1m81Zzuo& literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxhdpi/ic_notification.png b/app/src/main/res/drawable-xxhdpi/ic_notification.png new file mode 100644 index 0000000000000000000000000000000000000000..d722b14bf041678f481f54c086f1f3b71cdd0631 GIT binary patch literal 1149 zcmV-@1cLjCP)Wenzc?gCI#3AoQ<;eM}cABX?(s7{|@8- zqUnXR_ywzYXEDRxse0R}KQn0Y0)2^dK+*Oo%}%0m~S) zpvPj$vD=&s;P5{J9RpTN(5}X^d0??+*tz=Uwd4%G7Aca)y%1{gdTy#62DNj7`U0#` z7)uRmQfOCgP90~q`T#7mp~1>fo0{F~R0Wa=e9!4Qr8bUR@ z4RFS!4%y-bz+Z%T&_V06AQ2;ry+EdL4r+8Y zB&-fz8G=nji}X8qf7nmN+$2M6ln|?QutAkX%%h@(@(H1F4US*NiP-Z+iqsP#EgBfq zOvGL1|0-u3N8W@y>u|{G=O+yrdJ8G~hKoWxA9 z5*y7aCSraSOO8OaYl#U9DJn0qt1nP&5H)rllp(f)bLC?dtmQT;GG`?b?~JsoA1Js_ zKqlCX&;gQ;^#-9ImMK~%FlBD1#5lKebjxIkvK941fd;YeUnL>V65@c5;k?Y=6O@p6 z`2sjkV2nPTG^oT1uPk%2h;v;LXcQD9%gKJu(P@y`d-h38-+dw=zSW$b!_X&tMi?~Y zqA-TowgSKL8bE5EA{!%sKO`HpWe#z%+umyy)?HrNy~n2@Oa4%l#&la~wAWH|fh?R! zi$te|M%o6vX|X((Xt7k)e#WoBpnJH{Z9pSh@dK61_?kq6i){0(a>JKp=z*{7^uVrT zg9(S56SnocF%UoDP-Ct*BK?mYQ8>sHmhXIwEe>_KI#Gdlz^iyWy533Iw2=m_6>wk{ ziQ~8s_2DkO>M#0j`%*{34Z&P6tojI6l}Vif%wfCk`1}SQP1;3VM_c_|a*-$Af!96P zqnv#jgLc(H+pPoVdtCf6Y&p(KOI$ZxxNzaZg;e|oBXuF58 + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/horizontal_rule.xml b/app/src/main/res/drawable/horizontal_rule.xml new file mode 100644 index 0000000..7f702b0 --- /dev/null +++ b/app/src/main/res/drawable/horizontal_rule.xml @@ -0,0 +1,11 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_app_logo.xml b/app/src/main/res/drawable/ic_app_logo.xml new file mode 100644 index 0000000..4152a11 --- /dev/null +++ b/app/src/main/res/drawable/ic_app_logo.xml @@ -0,0 +1,18 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..b0f73c8 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..958ce44 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,18 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_pause.xml b/app/src/main/res/drawable/ic_pause.xml new file mode 100644 index 0000000..8d33792 --- /dev/null +++ b/app/src/main/res/drawable/ic_pause.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_play_arrow.xml b/app/src/main/res/drawable/ic_play_arrow.xml new file mode 100644 index 0000000..13af6d8 --- /dev/null +++ b/app/src/main/res/drawable/ic_play_arrow.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_settings.xml b/app/src/main/res/drawable/ic_settings.xml new file mode 100644 index 0000000..f673399 --- /dev/null +++ b/app/src/main/res/drawable/ic_settings.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_shortcut_enable.xml b/app/src/main/res/drawable/ic_shortcut_enable.xml new file mode 100644 index 0000000..8ccd9f2 --- /dev/null +++ b/app/src/main/res/drawable/ic_shortcut_enable.xml @@ -0,0 +1,16 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_shortcut_pause.xml b/app/src/main/res/drawable/ic_shortcut_pause.xml new file mode 100644 index 0000000..3b52750 --- /dev/null +++ b/app/src/main/res/drawable/ic_shortcut_pause.xml @@ -0,0 +1,16 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..eba10ca --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,17 @@ + + + + + diff --git a/app/src/main/res/layout/dialog_disable_custom_time.xml b/app/src/main/res/layout/dialog_disable_custom_time.xml new file mode 100644 index 0000000..240d4a5 --- /dev/null +++ b/app/src/main/res/layout/dialog_disable_custom_time.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_add_pi_hole.xml b/app/src/main/res/layout/fragment_add_pi_hole.xml new file mode 100644 index 0000000..731f381 --- /dev/null +++ b/app/src/main/res/layout/fragment_add_pi_hole.xml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_info.xml b/app/src/main/res/layout/fragment_info.xml new file mode 100644 index 0000000..7f7bbb9 --- /dev/null +++ b/app/src/main/res/layout/fragment_info.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_main.xml b/app/src/main/res/layout/fragment_main.xml new file mode 100644 index 0000000..897e9c7 --- /dev/null +++ b/app/src/main/res/layout/fragment_main.xml @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_retrieve_api_key.xml b/app/src/main/res/layout/fragment_retrieve_api_key.xml new file mode 100644 index 0000000..5a496db --- /dev/null +++ b/app/src/main/res/layout/fragment_retrieve_api_key.xml @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_scan_network.xml b/app/src/main/res/layout/fragment_scan_network.xml new file mode 100644 index 0000000..408f2cb --- /dev/null +++ b/app/src/main/res/layout/fragment_scan_network.xml @@ -0,0 +1,35 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/or_divider.xml b/app/src/main/res/layout/or_divider.xml new file mode 100644 index 0000000..3c066f7 --- /dev/null +++ b/app/src/main/res/layout/or_divider.xml @@ -0,0 +1,34 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/main.xml b/app/src/main/res/menu/main.xml new file mode 100644 index 0000000..9cc65f6 --- /dev/null +++ b/app/src/main/res/menu/main.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..bbd3e02 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..bbd3e02 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..2f5ce022e3121355a47e716f4ad309481968cb07 GIT binary patch literal 2472 zcmV;Z30L-sP) zJ0e|w?}&1}+7UHp3-rnq(-vRfQ3Kav8q#QDM^sWgOg*1Ox`=hf0@7>oG@~{{kOJ>d z7#58fJB796qUK0vu}z1gG$-*S#<4|nJ>CwZb{MuHT1fyTE=D@(ctq?Ifisu z&cUP!fH+!Aq6sm38qog`kQad1q!ECce4v7;=(*Ig`Z4;?rr%TN&Nrzy?h?r#tPzeAvI1=bA0C5b4&MkPrS9o7(8QtoM z&*5`9hjW9~4@jUc$}|gzP3m}9q;e9`O8 z0xAX|HmTFijH(e={U-NtX8J`oF!yfF# zo@V;Q7ribRfaNoYBoxb$0#(cflFFbeY=LZ=(9R`O-doF6VIWcu3ra zk-qUoM_rU*77&}%=w>Raot;S6a4Q5B^m%y|H6^bkRj*NT?&sx|V?4PHZp(n5s4)wu zP>XffXz^67#S4xqYI*9Hm@oRqz^O}GOsYQP#U6o>xD9U0fR{jB^nn&_9MA~>Vv{P} zOjdRCmrx&`IHN@j;MQnJSwSiuU#5oS6+X{Ve{PH0RP+&5!2rZ2mAaX%E^Ydq|4}Y5 zxwfv~NGkap_GKx|T>o5`pO^TEy1Q~s-av$&M1G0Dp2%Zfd3 z;_2pZ2W@iO3LXM=(SEal*rZ%HlTpK}H8jL4WPP2}^2{cZ%~ch9kFJxr&4yLqSIHOU zW&srd&}o=p(<(U1sAVH=FMazkaJh|Ym#!q)TvgSktn{_vyBKJb+g8XIsEhWQ0hA9w zWm+7a(&7Y18FlP%`-goGR>P-fxkxrwRqWkU?DeSi+P@s{~JeC3sGB%5bc+=hR|qHTp-fx74& zGk^{Q5Sx_fW-{t7@s0r`n`cz*TgC!X$Q7uI_L>4@k9C(s#8Ghs%n)UC{YbvQ!&h;O z$>tf=)!lb)vCiBAm0W?kXs_J_pd0{VlasocjIR9qjXRr3jHZ$(C&}ijswB!q-2s#I zCzzxa@&)RmbTfdm?J@46h*F`7l*|4-2_vtEnVuA$f5B z%X&_vCTuqX7mAgu9#zQ~g=PU|0Z^e1NRN}sGs{U`0b7lJY$|(e>uIQ~o5a8X4PL)a zJ=l$3{L^2lbm4;}>pNaWv2&^GWQo5WZqtKxk%EUff%tYX0G&_(#A@2G`L3@@@1^e3 z6;!c$jUhmd>rw+YtAo_E@j0b@uIad0K$!r?Ah z*GsQ&r{kFS{x5(s?6chk5iyh>5hFOts6B0u@^4$dH50wYJZPxlhnl#95-6rk|g?mT-8<=R0TQATZVr5ge?`A^S-)Q!o%`e#p5K1P>p ze_nXX)77#U$I0$;36~ zAZ|AU=sf^BY>%cKd$iyvqx^*V)Kys}4`O(jx@&6bYHmK=z{0W9d&>8^b^9)=P2Ef< zzxxQ~V#LU{FDtI;BSh1HU_>wyIU}Z0^8Vm)8o2hE+(NPW0u^GRsNx?CaZTA~0lf=A zOgUSpt)j9Y{e=43+P{3Offcalp@&J;J4g~)i2pJJXfFU|>wxqqFMgaZPN8Nzm3kVQ z{J(VYhPA7@hR*)+FO)lXAu0O?Net)^;#RYOm~xg*+laCqadaf{J}S6Bg$h@!rh+9) z>9A`7WofZw)H4|3i=Jr~&>jF{%7=8?RuW(IgJuEk1|T*$sGC_y7(F5b@ijAm(!!&5 z9k9Ok#1rHMY*vbBS`4cN)p&gVFWphkD#~0XKiK5AL_KNBnGq} z@ul!cw_!kk3w5mBA3mF~CllCq(0+^PeZ8ji@L9AuR7>SrciC*As(;k`H7Id=Xk^WM zx}lZCHN6}fS%(_uajnY#XyC~aK+!8hLLU7GjNS%qtgLCD1(b$bUJ8w@|5ixIV_f4J zmD?-uxX)&@BP2oJUK^&p@O)Urz8Ax^gD-?cX8bu!J1~{>VpwDb_nm9tTDT^z4Z|nG z%s*e40XiDu9O!;18A_SrO4e&}xokFvEg1j9V>u=;;ErBmv&Ap6*_=~ZTm#p#TxC*L m8BrF|^f7%*AJfN_IQ|RrE*LXmor-e+0000hGEP|L?u` z^?$s-yJtA=2EKsfS}{b`1Mm0T|I3R7+!_&0rG|D_+x+GPV?BX_iwgFA9@mvGH6_HB z7!ul*S)5%6bWESocj0i}EW&uty6N0p3hnJ7Iy*AKR!0>C`;(CpGTx5(Nf{v#Y`-tKbr2KQ8P~k#W#SIWEg=`1&=|)e$ z-d^Lf#C0k%I+v-U6e^$|;wh|qA8B})4extJEax)vCD6ZSf9>ZuoqAj-#l4QW?2_W( zq!i~txLFC!1V5YTOEviv%-3cWC*NwdpeXB@FyNA$waq9>P^dn*6s#v7n^h#$5O(K2 z=zxoyDL|(Rq&T_2d)y)*GoMH_SH2Y2w^>9=V-sJVaReT_kF(>J4)PTN zxrlwD!9*=%MccQIq`Ty{2YHXLUs z_K@f-TxYRZePe8tKu0CVx?@O;Wo-_fg_{^^412}c4IH}`)u^F$#ORpxTM@?xwTC4K zY?osH64p>tH+z{vd`A@L(ouy}KI#=JGRU#Eg}gokz%~34D4p~cl+T(C6$=+bWzssR z_I8@!%RY8qUsL z0GD%e!F}m6$g*57=zFS1^{E}TkAxo-(;@0bqJ1SBYOOU5tqh}M@KJ@#6A@uz+e6{l zXW`=h43KMU8x*BpU^{A0JjB;npD;d8h7=1)lI?+zhnhNRWl4_Bq;R(6a391nY#5zz ztXF?HyJ!hqsi>$o@Y<>>xbjOm)Rdj^mh`?pA&H0hh?n@o^-Ya!n`BQ7X~Irq-E5!1 zKJbu;1c-y8!;{U9!$ICX!{K69Hpp)GwSktOg&^qxMa z@2TE3LLndV5`Vb9QEU|FPhu=A6=DYaYS8fT5*>4}F=UAjcVomki9?~ZdcqW~}Iq-A(p6XG3YDevT`G}wFgzKXk<7R_>VS~d=?Or>K@eXWI zo~01TSqh0ZP*R3xHc*_}%5zZm`B!kX&p>Y-t(?@3+7nNpNMuhlCyS9f!!FT&_9KJs zmU@#%Rk?K0F(JD#;vkOp><q>UeTVEiIoqCvF;2kwIa*j4$3i)@tt!hnByZJgG^H#gu@vK;vqgikY8~-y-hR6Y_{zeV^1AW8p;Qh#+qbWJF=@Ta5c^}e z0%i5al`H4iRVQmOxyxwZy-CGUL3V0g_IMdaMT1|?k z8;wq!q{{Z-M3pYuWh%8Gf6|+N^MDJt3F){I)XEbM$c_t#v+w*z{%GRBm@&nHjiU9; z(*{Fb!l(-LnGn+vv&nP2l?F1ET2M0c>$>F@H=4(ud=|8FGzQ0TyFqqUAIKjq?I~sy z2~rHUs@Hi__iF_DH*e~;Q?y^&r8MwRsRd=DXF*p)12NmHQq@_K@j3U80gZ0c~ zoY2vC74r%di%VOoV9yIzpxl?=@LR?5aaGxY%Vx6(gm&@+RmsZOsaQEpX%sT{^t=PkAIb5_q|0Gf`);6>Cmif- zcdK6}eI7IE^j>#p;!tD$mbWY?E&MKdu#K61fna`zDs9EesY;`ek!FsA;<@wu_C%)^ zC&KB(m2k1(BwRUn4lWfHLwQOnqnTNI%}#-0I#yhBOL&A^(GPMpb`CsE~1sA`I$Zb(RVwujOs%fh;2dOjx?(h|CaY-I@Pk7TZik)fD;H_uz6bTu0&w#n8_IOT1Q;r)`JSgihCpl46O2 z)OKAVXUaQJNvl=BomCPiW6aZ?k}&5V6;+trWjr|!_S`cJ((iu=GRBUBoT=}^Da??o>$x{i!)iD7oKXA= z1~s-XdGn7La~l?W!!M?w=_S44iM0X_GX@c zcsHK)%!ym~#7 zAZn8p(t44J6K7)t3Q0s1tOE0#{ZHVKvL?Xwd};_36Lo-(^(MNTn!sWX?e&B*7nz1c zM-NKW$5fF>>jbOJ#0rYkUUx!O?(v9=LK{ySelDEj(nmfH$(S!S(CMQyi}-8b3{MoUuxFv2|fFhWATq z*6plLj_Ws^lHEh75x>xRNx)87Zi~f5n>$ zVmYoq#n!*EKS_8Pa|0~mhPLKM&c`uJ2Jj5HS1SgCVq=GHaQN-%aN%SjTseQiZ`jeC zaIa{u)y9Qf1AkkY3=B%v_ZYac1BVkh)F-Yk#h#TPM_ zb}Xid{YgP9+@!={*>M7Id~F_z#EjRjR2;Z2l5q7&!pwM8i1i}@=9_oU!6`x>$4iqb zhQ2toxF+}qhbTte`6)qsVUFH%PIX`fbEYL>1@n!LXY0*Xqm`c1xwmpbzR7;*8`Xp3 z=${Wd4d%s1X7T2NrHV81a>NSe*o_ABMNY)yPjNyXq&GZHJ~Q8&{8!LX>@0M^8E(M) zI%)F)-i(e}2(`-ui)T3=-w2Rx@_~FIpWf3+TQI>HNWM08&$(t!!sU^EP23jI=M}y2 z{cm`4<&rvQYcUb?EK{6rawIAzrIu`y4_~7%FY2Wa(Kq@rZ(HVTv;S*Ms{~voF`uA^ z9=rA9`3I)yrFA$NdYvT4mkJg)LQYhIh&YIwY%p6i5yLxrX?<^=ryuZkBi|B~rTm8e zrA{;(+wBp(N9m;J-s6qiG2f6E@fe5#c?oZEFHuU*Qv56&P+P{~CQjo10YSFN=EFMi zdGdjLVLr8(KRfaECd|0$-&i^$Zo5w}JTpZnCC=oHr|03B{lPy__hJV8TqZ0IfY0aN zdela3Ph7-F++^ck<#Gi*veKD+XbHWhzE|t6n1HciM?&@P5G@QHuQ$FsT`z6=oHrHE z!NBGtsCO+^#A*;!hw4%r8bV(n2XbvBPU2=grZd^>sw96m#eWI4rcGccrAM_x&=XsI z+eAm-jlCO8$T|x3EJ)LdJNib5U#BuSQG8}JVlY9q;_UyAZ zREO$P8`f6;TN0(giJbzaODc59fqFwn&imw&K*#hMyWA3WBPZPd0hlz2trhNwh5!Hn M07*qoM6N<$f^w+js{jB1 literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..5b4f5f31189b30865a3c5fd8829d6ebf1e83aa92 GIT binary patch literal 1863 zcmV-N2e|l&P)qD0ZH~Hny?woVJ z^Zzb0OV(m}@*2Z0vRJ}Ec!&vmoW`GK#?VBX?`LFScl3ByuRZmd9((G9P{3nz4|=`% zZNAr%8g<>7vaQ#da-+wY(icjU9=35$0ZphEuR4>rcRQ2m+8CnM>E4Ofo-#C`9+tM~ z)I=D#3FwM5*$Wj{LWDB5*JmK2m==#NfJyD5Q>Hp zaS7TC16!@}(?|AdXrRsEmSBQ_P&AZ?cIX*LqIm*xNIZOq28ZcRRW)7zW(##? zencH_z9TEY=g;{%$8kL81**+G0IChfKx0&*)D-2WGf{5A>W{9>)pY;XZFzR@Ha1b` ziVvw7`@V?}-pE+I=g;{%$8kL8alXI$+ykJRs2~iSf`QY%0)o|u^Rr%};cGo|Gt!|F zI+s3&{Kczq9LIAW=W`vS*d}EXtAjDn00S^VSP8+Z3zpTBlP_-vp3$?@GsrMjhqLJ! zzLh{e*Wr44aohu-lV%OG8W1QlJ|7`iVw&r&L5n|`CO-E&O+T7 z?g3C03}gjkpw3r7u#N(=)xGgMc?%5SU^XN@N4jwV;JksVlX4xd$91(a^~xkx24kQG z1_0F}nN>?&Tq;RrN9BR9;_g!8aFbqKK#<4z@)<%suB(mV9spGYV*tw~VbAfw5v*Ej z6Vqr24<{bD^}SW37h@LA=Q>=E>uRra4}i*|63iPo4g)8A1q7>>YWFiVj3+D)-2BGd zq!(kB=EX~WcO%r}y4vd}l}R*h;Aa>(W_L-)?Jlaay9BG26RtEGY`-KA-12TF>BX4k zoU#IgSOKo9z0N%VI$}@gUt`w5Q5b*;szfrYmde-^>MJdm2kuw4}cEaF&jL z>i9Z~DzF`la#fm_zyIhtP@D0xQJg%9hk`MH{U*T#?>oV&$Fa1@6p-#8!KyGNZ{ zzQ@6Hkx~AMX|riSc@p2q$uo>qdnhz(;0G8efdM4)z7t1$MgBvn!$lofa(`c&y&ex$ z&s#)#`Nz{|)6GN0zT4-uZaOh*o^IU89tvRKbF&7p-=t!DJTIGI)lwQij{22vl!jnt zTfbWSD5oR}GwPl|jmuX)${ohH(bJi$NN)@kwTC==oOuISE(s>62+6EkjwFt!q3$04 z20(9iF4ZnxLXEgpUR=M4`fy;?{|9*H8_~R8{gJ3W91O-lL1esCWQ(W2Z1I9sOL1HZ zdFmPh8Zi2gtQUL!*zzaC#rbl+Ze1PJ9uAl_un7k8BIBe2TO8%t;smQ6MXvF5oA2S+4x^eM z8lsjj*HTf!I602BFeZr)A{o^)LhWI{HMaj#vj%cuARkG&NM<$S@Px^9Y5PtZ#C~QY z{2b`3tEW>Re?kQ@Zt|CF7PW_c!5GMa0VGlml5>4U49SajQ}N{KR6chBmCl+=g{hM$ z7c(6w&n#*WdxJ5MZHx0Dkq#g^$5$j|s6G4%1D{!)>46Pcqo?kVjFk@9Vric(R~ Bc?JLg literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..758f3c69d527c3cefb371bc87750914feeb9c2df GIT binary patch literal 2645 zcmV-b3aa&qP)ct7@l7x>J~$1w31FMkW_R}Wy_tWO!JgS&gCUkaz27tM zeeZk!IkUSK%U%9~#gc$XL?n}q!$0Cnv6ipu50iflkz{o5yw4|nDS8LBIRg(7s7=>Q zc#??gD;3^iAM(gqd+IOGNr9{uDRm#B5z&5D@^=uZ&93buAM&M``(8)|4!ypYTWtQ9 z&q#sYh${#<-4C#s-Nexr2-u8r3}mk-X!D!eEt3D0lal`!2|0BO;geE8jtAtciv!{! zPHj!F(MT95@sWjj#t|5M(iSLHlPW+l{vZExhgc-N;e}fpvy5l1ByFsJPzaEK6 zxZJ1$?lEMR3%;=VKepUj7m0B%Sgl@MD<4za_Z%#jlCMVeuhJC}dtn!_ zJ-*)Ndz}#~WcAp-pV0E+C_shH`y0KsnEU(@A$1IrgpOh`LS{BDeBnrihP20^>B*;| zdCUX_)TV26uRmNzSpkP_zEGjy9~bqU(9ioLqQA5b1CQ8zL9+4Ss67=n4JpI1O_x^^LR6`Y(pQ}KvZ7}Up~ zuF*a6Az!Uv;vlY=`A}0O+W%SIvu+y&{nk_=`gW6Zwde~*Bdc`|hSr5kBI2)B*FwX@ zX;5Pzybq`l^raZBh}MPgDxA^cG);wqH&3?~5-gk80a3{NhE)mPD^;QwoxL(yGc-Pz z9+@^Iq#k=Vn!gb!Mk|82h%=f$X{tec(CWFemUBN&nicKZwp1v(XEEn+C0bKRez;sx z8ksir6MqCz{iA?lwdq8h#2w9_v>igbRdl@?*;2I;*e`llm*dR}tdy)7>Zi<5b`oXi zaXwez-X6_A8Yq@Hl&OfE^;4dY?$5Mki=MS~I&acFUL+I=-l9XQqfFKa<@NxyF3(bW ztQ&Wfy7801%r{=tjsJndMV!PCyGc7);SnxKMsg5#PBUC;(0=n=( zqGX-8EtX*qn)${Jx|?TPoUTX#{}DVxibYS`LD2)HqDLlc zgz6{8!1bo%O0si!s+#%r7sN%J#7#NO{74%D_KKeN3~NeSq=2=&^Wh@Va|w%p5_BSK zgzBfpLpMGeP_h$m&j&NV{(`uaX^GsF!_1Ggl_&+cI~H;F;gJHec*n5)Sik|*Q6_7I zs&79F-E8}8{@tIz%&))D7f^)-nE4g6&dtLD=5o^GkpliExQ6Z%Jni_b0Y!pGCToQ9 zkx#+3re-DCnWf9Y%&)&_VN>WDPNDLVV@&->8-f<``8haiA_csN3nJI*YTPHd@uYLh zWQ|ZV^ijCP7Tl$RLMXI(!OS;a6x#jJvHyU=*?}7a<)OnKtdwjY;0{pt${g&m=r1lk9i32tQ_7Hop$t#puId_fI%0=ZPX9?%e-tk7 zrTNdhF8?Z7c$ze4&A)51-k`TkZf<{4|y2A%*&7c)k{uuAga% ztANg`YS`-^8i~!7@KJdDL^!c*MP%BL_~ZXv26^5Gpm#1kP!moaS2$ZpfqClmNxJQ| zGK0M!;Kus(tyX6P6|qZjg>uy32#Q9Ji|iP&a2jrez)PY{ch>G`;5Dua(v%oc*nAs|#PGK7x0j3#x1jC(sbA?FQb_^J1cqPTE$*`T~R%k&?kl3%996;m@oo@(yfkDEjUcxKv(g*z@hUOXUrJ z67>Fe0?J9bw^*I5Dor2C#~P663RrlUF{XvP75pbK`8&lEI74p z1GId)1DaNT00(~fJY>5ELB#J4!8%OFrvq4&ENvt$>HYauldT6Ds)yd=96L6vq=1B> zPto26Eao%ZBKuswLr@ONML8F9_H5Mf4bqa{-|q<63C+{)`WH_6_!AsTN`NC0#3bQu zYJ0%IID?d9Bj%c~_WU+J#gIl=z{o#(#1aVlL#00000NkvXXu0mjf DBy$mL literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..f2b86ce411a79826df010cb0727c14d0113648df GIT binary patch literal 3490 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 Q4*&oF07*qoM6N<$f_0OfNB{r; literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..97a8a9d55a07a692f1f426b59b73fb7873751248 GIT binary patch literal 5889 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 XZgJAho|(Ln00000NkvXXu0mjf9!fhY literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..883fcea297507f6a6e059af2362726c1b335aa13 GIT binary patch literal 4917 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..44fcbc23ba3e6c7288e69d6b1785da318f8d6a66 GIT binary patch literal 8796 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 Zh)*s=5_h+#0P}waP*>4Zu2QrN`5%HCG`#=- literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..00749a4f24ae749a95d8d4b6794f1092f18ecd83 GIT binary patch literal 12599 zcmZvDWl$YWv@H$?3+@DWcXzj-!QI_GxNC5C4est7B>2H0xCD21&dqmky??K2dgjmG zyH{^pUDeakD&J&K5D5?=ARtiWWF^)9eFy#<2yp-2Y8&?O5D=`Na*|>i-fI^I@ZMNU zk4DB`r)}rdIbnuSMWMzL{uAn0ggw8G?}Q432|-p`iKdWJjbQ8oT4NSvNGZt^+ENut zG}5ryA77XjRZ=#Ez;2e818C4wkRxqx?Kf<-Md^F&z6#5um+WfRO(=~ppt(A6K1@fZrR8b2D_$YgZvhc(K>D22t0u-)wXMcK!j_ANay1xey zsyLQ7oaP7w8@Nqqr>sS!=}oDJxtH~KZuo|yoRChLQ+dyiFB zezM0(hk-WZoC*hA+kTJ5IOFBIFIc!{g`Gp##~}evvwnOoWFd##pCkD7=fxp96v*I(-1GyVTx}Hi1$1xt;5j*0+ zw3tvCn&tDf^>bR{7NUq`BPBs5VWoi9-NuCeoWm(U_Pw)Wd3M?nT!M2j|h z>rKOb@QoLqNdKx&E0y>N%fV*MSpn6Tp_|0!dOvY%?d@Seny`7?;=HdX-^CPHH$w%^ zm*qZx>HS&zJP@WCn8Wd6d){EA03l)x*>5koI^q6n;i$gU%#M>4{uu;e6jL87@qln% zsNOY09_s|z2^AKPkZq_;*Cm~ov5dQ{I-(hvLe$|^o!fyAn~7C|gtZk{z*MOMNOAP= z-BRpomkyBT@Y@Mj=Z`vuDc63Xt8GbvjmnT<(aIP3*W4#sj≺o$z(JV#;h)(wG-Ok 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 S|GN+YAt&`svQFGM + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml new file mode 100644 index 0000000..24d1d8d --- /dev/null +++ b/app/src/main/res/values-night/colors.xml @@ -0,0 +1,7 @@ + + + #333333 + #f1f1f1 + @color/colorGreenLight + @color/colorRedLight + \ No newline at end of file diff --git a/app/src/main/res/values-night/styles.xml b/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..c754cb1 --- /dev/null +++ b/app/src/main/res/values-night/styles.xml @@ -0,0 +1,13 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..22e9af2 --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,17 @@ + + + @color/colorRedLight + @color/colorRedDark + @color/colorRedLight + #f60d1a + #96060c + #30d158 + #34c759 + #000000 + #ffffff + @color/colorGreenDark + @color/colorRedDark + @color/colorWhite + #00000000 + #666666 + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..61c2747 --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,33 @@ + + Pi-Helper + Scanning IP Address: + Pi-Helper Logo + Status: %1$s + Enable + Disable + Disable for 10 seconds + Disable for 30 seconds + Disable for 5 minutes + Disable for custom time + Disable Permanently + Disabled + Enabled + Please ensure you are connected to the same Wi-Fi network that the Pi-Hole is running on and try again, or enter the Pi-Hole\'s IP address manually. + Pi-Helper failed to find your Pi-Hole + Please ensure you are connected to the same Wi-Fi network that the Pi-Hole is running on, and that you\'re using the correct IP address and try again. + Pi-Helper failed to connect to your Pi-Hole + Please configure Pi-Helper before using shortcuts + or + Sorry, but no + Yes, I\'ll help! + Would you like to help me make Pi-Helper better by automatically sending crash reports and usage details via Firebase? These are immensely helpful in tracking down issues that may arise or optimizing the app for only the features that are used. You can change your mind at any time from the settings. + Help make Pi-Helper better + Settings + Forget Pi-hole + William Brawner. You can find the source code or report issues on the GitHub page for the project.]]> + Are you sure you want to forget your Pi-hole? + This cannot be undone. + Pi-Helper Crashed! + Would you please consider sending the crash report to me? + Crash Reports + diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..9b4ff21 --- /dev/null +++ b/app/src/main/res/values/styles.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + diff --git a/app/src/main/res/xml/backup_descriptor.xml b/app/src/main/res/xml/backup_descriptor.xml new file mode 100644 index 0000000..6fd6103 --- /dev/null +++ b/app/src/main/res/xml/backup_descriptor.xml @@ -0,0 +1,4 @@ + + + + diff --git a/app/src/main/res/xml/shortcuts.xml b/app/src/main/res/xml/shortcuts.xml new file mode 100644 index 0000000..a8ff49e --- /dev/null +++ b/app/src/main/res/xml/shortcuts.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/test/java/com/wbrawner/pihelper/ExampleUnitTest.kt b/app/src/test/java/com/wbrawner/pihelper/ExampleUnitTest.kt new file mode 100644 index 0000000..bc95806 --- /dev/null +++ b/app/src/test/java/com/wbrawner/pihelper/ExampleUnitTest.kt @@ -0,0 +1,17 @@ +package com.wbrawner.pihelper + +import org.junit.Test + +import org.junit.Assert.* + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..4bd2e0a --- /dev/null +++ b/build.gradle @@ -0,0 +1,28 @@ +buildscript { + ext.kotlin_version = '1.3.61' + ext.coroutines_version = '1.3.2' + ext.koin_version = '2.0.1' + ext.okhttp_version = '4.2.2' + repositories { + google() + jcenter() + maven { + url 'https://maven.fabric.io/public' + } + } + dependencies { + classpath 'com.android.tools.build:gradle:3.5.2' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +allprojects { + repositories { + google() + jcenter() + } +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..23339e0 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,21 @@ +# Project-wide Gradle settings. +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx1536m +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app's APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true +# Automatically convert third-party libraries to use AndroidX +android.enableJetifier=true +# Kotlin code style for this project: "official" or "obsolete": +kotlin.code.style=official diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..f6b961fd5a86aa5fbfe90f707c3138408be7c718 GIT binary patch literal 54329 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

<5G2M1WZP5 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 F{|7XC?P&l2 literal 0 HcmV?d00001 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..52a0619 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Sat Nov 16 20:08:04 MST 2019 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..cccdd3d --- /dev/null +++ b/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..f955316 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/piholeclient/.gitignore b/piholeclient/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/piholeclient/.gitignore @@ -0,0 +1 @@ +/build diff --git a/piholeclient/build.gradle b/piholeclient/build.gradle new file mode 100644 index 0000000..53b7c9c --- /dev/null +++ b/piholeclient/build.gradle @@ -0,0 +1,45 @@ +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' +} diff --git a/piholeclient/consumer-rules.pro b/piholeclient/consumer-rules.pro new file mode 100644 index 0000000..b10b8cf --- /dev/null +++ b/piholeclient/consumer-rules.pro @@ -0,0 +1,67 @@ +# JSR 305 annotations are for embedding nullability information. +-dontwarn javax.annotation.** + +-keepclasseswithmembers class * { + @com.squareup.moshi.* ; +} + +-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 { + ; + **[] 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 (...); +} + +# Retain generated JsonAdapters if annotated type is retained. +-if @com.squareup.moshi.JsonClass class * +-keep class <1>JsonAdapter { + (...); + ; +} +-if @com.squareup.moshi.JsonClass class **$* +-keep class <1>_<2>JsonAdapter { + (...); + ; +} +-if @com.squareup.moshi.JsonClass class **$*$* +-keep class <1>_<2>_<3>JsonAdapter { + (...); + ; +} +-if @com.squareup.moshi.JsonClass class **$*$*$* +-keep class <1>_<2>_<3>_<4>JsonAdapter { + (...); + ; +} +-if @com.squareup.moshi.JsonClass class **$*$*$*$* +-keep class <1>_<2>_<3>_<4>_<5>JsonAdapter { + (...); + ; +} +-if @com.squareup.moshi.JsonClass class **$*$*$*$*$* +-keep class <1>_<2>_<3>_<4>_<5>_<6>JsonAdapter { + (...); + ; +} + +-keep class kotlin.reflect.jvm.internal.impl.builtins.BuiltInsLoaderImpl + +-keepclassmembers class kotlin.Metadata { + public ; +} diff --git a/piholeclient/proguard-rules.pro b/piholeclient/proguard-rules.pro new file mode 100644 index 0000000..f1b4245 --- /dev/null +++ b/piholeclient/proguard-rules.pro @@ -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. +# +# 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 diff --git a/piholeclient/src/androidTest/java/com/wbrawner/piholeclient/ExampleInstrumentedTest.kt b/piholeclient/src/androidTest/java/com/wbrawner/piholeclient/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..8ab2bd2 --- /dev/null +++ b/piholeclient/src/androidTest/java/com/wbrawner/piholeclient/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.wbrawner.piholeclient + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.wbrawner.piholeclient.test", appContext.packageName) + } +} diff --git a/piholeclient/src/main/AndroidManifest.xml b/piholeclient/src/main/AndroidManifest.xml new file mode 100644 index 0000000..3dc5fd8 --- /dev/null +++ b/piholeclient/src/main/AndroidManifest.xml @@ -0,0 +1,5 @@ + + + + diff --git a/piholeclient/src/main/java/com/wbrawner/piholeclient/PiHoleApiService.kt b/piholeclient/src/main/java/com/wbrawner/piholeclient/PiHoleApiService.kt new file mode 100644 index 0000000..1d5027c --- /dev/null +++ b/piholeclient/src/main/java/com/wbrawner/piholeclient/PiHoleApiService.kt @@ -0,0 +1,174 @@ +package com.wbrawner.piholeclient + +import com.squareup.moshi.Moshi +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import okhttp3.HttpUrl +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody.Companion.toRequestBody +import kotlin.reflect.KClass + +interface PiHoleApiService { + var baseUrl: String? + var apiKey: String? + + suspend fun login(password: String): String + + suspend fun getApiToken(): 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 +// +// @GET("/admin/scripts/pi-hole/php/api_token.php") +// suspend fun apiKey(phpSession: String): Response +} + +const val BASE_PATH = "/admin/api.php" +const val INDEX_PATH = "/admin/index.php" +const val API_TOKEN_PATH = "/admin/scripts/pi-hole/php/api_token.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 login(password: String): String { + val url = urlBuilder + .encodedPath(INDEX_PATH) + .addQueryParameter("login", "") + val body = "pw=$password".toRequestBody("application/x-www-form-urlencoded".toMediaType()) + val request = Request.Builder() + .post(body) + .url(url.build()) + return sendRequest(request.build(), String::class)!! + } + + override suspend fun getApiToken(): String { + val url = urlBuilder + .encodedPath(API_TOKEN_PATH) + val request = Request.Builder() + .get() + .url(url.build()) + return sendRequest(request.build(), String::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 sendRequest(request: Request, responseType: KClass?): 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()) + } + } + } + } + } +} \ No newline at end of file diff --git a/piholeclient/src/main/java/com/wbrawner/piholeclient/PiHoleClientModule.kt b/piholeclient/src/main/java/com/wbrawner/piholeclient/PiHoleClientModule.kt new file mode 100644 index 0000000..832f7e3 --- /dev/null +++ b/piholeclient/src/main/java/com/wbrawner/piholeclient/PiHoleClientModule.kt @@ -0,0 +1,62 @@ +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>() + override fun loadForRequest(url: HttpUrl): List = cookies[url.host] + ?: emptyList() + + override fun saveFromResponse(url: HttpUrl, cookies: List) { + 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 { + OkHttpPiHoleApiService(get(), get()) + } +} \ No newline at end of file diff --git a/piholeclient/src/main/java/com/wbrawner/piholeclient/Responses.kt b/piholeclient/src/main/java/com/wbrawner/piholeclient/Responses.kt new file mode 100644 index 0000000..8119f98 --- /dev/null +++ b/piholeclient/src/main/java/com/wbrawner/piholeclient/Responses.kt @@ -0,0 +1,79 @@ +package com.wbrawner.piholeclient + +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, + val status: Status, + @Json(name = "gravity_last_updated") + val gravity: Gravity?, + val type: String?, + val version: Int? +) + +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: List, + @Json(name = "top_ads") val topAds: List +) + +@JsonClass(generateAdapter = true) +data class StatusResponse( + val status: Status +) diff --git a/piholeclient/src/main/res/values/strings.xml b/piholeclient/src/main/res/values/strings.xml new file mode 100644 index 0000000..9c995ef --- /dev/null +++ b/piholeclient/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + PiHoleClient + diff --git a/piholeclient/src/test/java/com/wbrawner/piholeclient/ExampleUnitTest.kt b/piholeclient/src/test/java/com/wbrawner/piholeclient/ExampleUnitTest.kt new file mode 100644 index 0000000..0f592fe --- /dev/null +++ b/piholeclient/src/test/java/com/wbrawner/piholeclient/ExampleUnitTest.kt @@ -0,0 +1,17 @@ +package com.wbrawner.piholeclient + +import org.junit.Test + +import org.junit.Assert.* + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..1728ee1 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,2 @@ +include ':app', ':piholeclient' +rootProject.name='Pi-Helper'