source import
authorDa Risk <da_risk@geekorum.com>
Fri, 08 May 2020 21:33:19 -0400
changeset 1 831cffa9c991
parent 0 fef46dce2812
child 2 99455c223e8f
source import
.gitignore
.hgignore
README.md
build.gradle.kts
buildSrc/build.gradle.kts
buildSrc/src/main/kotlin/AndroidJavaVersion.kt
buildSrc/src/main/kotlin/AndroidPlayStorePublisher.kt
buildSrc/src/main/kotlin/AndroidSigning.kt
buildSrc/src/main/kotlin/AndroidTests.kt
buildSrc/src/main/kotlin/Avdl.kt
buildSrc/src/main/kotlin/Repositories.kt
buildSrc/src/main/kotlin/RepositoryChangeset.kt
buildSrc/src/main/kotlin/SourceLicenseChecker.kt
buildSrc/src/main/kotlin/VersionAlignment.kt
buildSrc/src/main/kotlin/android-avdl.gradle.kts
buildSrc/src/main/kotlin/android-signing.gradle.kts
buildSrc/src/main/kotlin/android-tests.gradle.kts
buildSrc/src/main/kotlin/play-store-publish.gradle.kts
buildSrc/src/main/kotlin/source-license-checker.gradle.kts
config/README.md
config/android-checkstyle.gradle
config/android-maven-publication.gradle
config/checkstyle/checkstyle.xml
config/java-checkstyle.gradle
config/license/header.txt
config/source-archive.gradle
geekdroid-firebase/build.gradle.kts
geekdroid-firebase/proguard-rules.pro
geekdroid-firebase/src/androidTest/java/com/geekorum/geekdroid/firebase/ExampleInstrumentedTest.java
geekdroid-firebase/src/main/AndroidManifest.xml
geekdroid-firebase/src/main/java/com/geekorum/geekdroid/firebase/CurrentUserLiveData.kt
geekdroid-firebase/src/main/java/com/geekorum/geekdroid/firebase/Firestore.kt
geekdroid-firebase/src/main/java/com/geekorum/geekdroid/firebase/Tasks.kt
geekdroid-firebase/src/main/java/com/geekorum/geekdroid/firebase/logging/CrashlyticsLoggingTree.kt
geekdroid-firebase/src/main/java/com/geekorum/geekdroid/gms/LocationServices.kt
geekdroid-firebase/src/main/java/com/geekorum/geekdroid/gms/Tasks.kt
geekdroid-firebase/src/test/java/com/geekorum/geekdroid/firebase/ExampleUnitTest.java
geekdroid/.gitignore
geekdroid/build.gradle.kts
geekdroid/proguard-rules.pro
geekdroid/src/androidTest/java/com/geekorum/geekdroid/ExampleInstrumentedTest.java
geekdroid/src/main/AndroidManifest.xml
geekdroid/src/main/java/com/geekorum/geekdroid/RoomExt.kt
geekdroid/src/main/java/com/geekorum/geekdroid/accounts/AccountAuthenticatorAppCompatActivity.kt
geekdroid/src/main/java/com/geekorum/geekdroid/accounts/AccountSelector.kt
geekdroid/src/main/java/com/geekorum/geekdroid/accounts/AccountTokenRetriever.kt
geekdroid/src/main/java/com/geekorum/geekdroid/accounts/AccountsListViewModel.kt
geekdroid/src/main/java/com/geekorum/geekdroid/accounts/AccountsLiveData.kt
geekdroid/src/main/java/com/geekorum/geekdroid/accounts/CancellableSyncAdapter.kt
geekdroid/src/main/java/com/geekorum/geekdroid/accounts/SyncInProgressLiveData.kt
geekdroid/src/main/java/com/geekorum/geekdroid/app/AppCompatPreferenceActivity.java
geekdroid/src/main/java/com/geekorum/geekdroid/app/BottomSheetDialogActivity.kt
geekdroid/src/main/java/com/geekorum/geekdroid/app/lifecycle/EventObserver.kt
geekdroid/src/main/java/com/geekorum/geekdroid/app/lifecycle/Transformations.kt
geekdroid/src/main/java/com/geekorum/geekdroid/arch/PagingRequestHelper.java
geekdroid/src/main/java/com/geekorum/geekdroid/battery/LiveData.kt
geekdroid/src/main/java/com/geekorum/geekdroid/bindings/BindingAdapters.kt
geekdroid/src/main/java/com/geekorum/geekdroid/bindings/Converters.java
geekdroid/src/main/java/com/geekorum/geekdroid/dagger/AndroidFrameworkModule.kt
geekdroid/src/main/java/com/geekorum/geekdroid/dagger/AndroidxCoreModule.kt
geekdroid/src/main/java/com/geekorum/geekdroid/dagger/AppInitializers.kt
geekdroid/src/main/java/com/geekorum/geekdroid/dagger/AssistedInjection.kt
geekdroid/src/main/java/com/geekorum/geekdroid/dagger/FragmentFactory.kt
geekdroid/src/main/java/com/geekorum/geekdroid/dagger/ViewModels.kt
geekdroid/src/main/java/com/geekorum/geekdroid/dagger/WorkerInjection.kt
geekdroid/src/main/java/com/geekorum/geekdroid/jobs/JobThread.java
geekdroid/src/main/java/com/geekorum/geekdroid/jobs/ThreadedJobService.java
geekdroid/src/main/java/com/geekorum/geekdroid/loaders/ObjectCursorLoader.java
geekdroid/src/main/java/com/geekorum/geekdroid/network/BrowserLauncher.kt
geekdroid/src/main/java/com/geekorum/geekdroid/network/OkHttpWebViewClient.java
geekdroid/src/main/java/com/geekorum/geekdroid/network/PicassoOkHttp3Downloader.kt
geekdroid/src/main/java/com/geekorum/geekdroid/network/Socket.kt
geekdroid/src/main/java/com/geekorum/geekdroid/network/TokenRetriever.kt
geekdroid/src/main/java/com/geekorum/geekdroid/preferences/PreferenceSummaryBinder.java
geekdroid/src/main/java/com/geekorum/geekdroid/preferences/RingtonePreferenceSummaryBinder.kt
geekdroid/src/main/java/com/geekorum/geekdroid/security/SimpleEncryption.kt
geekdroid/src/main/java/com/geekorum/geekdroid/utils/PriorityRunnable.java
geekdroid/src/main/java/com/geekorum/geekdroid/utils/ProcessPriority.java
geekdroid/src/main/java/com/geekorum/geekdroid/views/AccountMenuLineView.kt
geekdroid/src/main/java/com/geekorum/geekdroid/views/CheckableImageView.java
geekdroid/src/main/java/com/geekorum/geekdroid/views/EdgeToEdge.kt
geekdroid/src/main/java/com/geekorum/geekdroid/views/MultipleLongClickGestureDetector.java
geekdroid/src/main/java/com/geekorum/geekdroid/views/ReturningBottomAppBar.kt
geekdroid/src/main/java/com/geekorum/geekdroid/views/banners/Banners.kt
geekdroid/src/main/java/com/geekorum/geekdroid/views/banners/Builders.kt
geekdroid/src/main/java/com/geekorum/geekdroid/views/behaviors/NestedCoordinatorLayout.java
geekdroid/src/main/java/com/geekorum/geekdroid/views/behaviors/ScrollAwareFABBehavior.java
geekdroid/src/main/java/com/geekorum/geekdroid/views/behaviors/SwingBottomItemAnimator.java
geekdroid/src/main/java/com/geekorum/geekdroid/views/recyclerview/FirstLayoutItemAnimator.java
geekdroid/src/main/java/com/geekorum/geekdroid/views/recyclerview/ItemSwiper.java
geekdroid/src/main/java/com/geekorum/geekdroid/views/recyclerview/ScrollFromBottomAppearanceItemAnimator.java
geekdroid/src/main/java/com/geekorum/geekdroid/views/recyclerview/SingleItemSwipedCallback.java
geekdroid/src/main/java/com/geekorum/geekdroid/views/recyclerview/SpacingItemDecoration.java
geekdroid/src/main/java/com/geekorum/geekdroid/views/recyclerview/ViewItemDecoration.java
geekdroid/src/main/res/drawable/view_account_menu_line_handle_closed.xml
geekdroid/src/main/res/drawable/view_account_menu_line_handle_open.xml
geekdroid/src/main/res/drawable/view_account_menu_line_handle_selector.xml
geekdroid/src/main/res/layout/activity_bottom_sheet_dialog.xml
geekdroid/src/main/res/layout/view_account_menu_line.xml
geekdroid/src/main/res/layout/view_banner_extended.xml
geekdroid/src/main/res/layout/view_banner_simple.xml
geekdroid/src/main/res/values/attrs.xml
geekdroid/src/main/res/values/ids.xml
geekdroid/src/main/res/values/strings.xml
geekdroid/src/main/res/values/styles.xml
geekdroid/src/test/java/com/geekorum/geekdroid/accounts/AccountsLiveDataTest.java
geekdroid/src/test/java/com/geekorum/geekdroid/accounts/CancellableSyncAdapterTest.kt
geekdroid/src/test/java/com/geekorum/geekdroid/accounts/SyncInProgressLiveDataTest.java
geekdroid/src/test/java/com/geekorum/geekdroid/battery/LiveDataTest.kt
geekdroid/src/test/java/com/geekorum/geekdroid/shadows/ShadowAccountManager.java
geekdroid/src/test/java/com/geekorum/geekdroid/shadows/ShadowContentResolver.java
geekdroid/src/test/java/com/geekorum/geekdroid/utils/LifecycleMock.java
gradle.properties
gradle/wrapper/gradle-wrapper.jar
gradle/wrapper/gradle-wrapper.properties
gradlew
gradlew.bat
settings.gradle.kts
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/.gitignore	Fri May 08 21:33:19 2020 -0400
@@ -0,0 +1,9 @@
+*.iml
+.gradle
+/local.properties
+/.idea/workspace.xml
+/.idea/libraries
+.DS_Store
+/build
+/captures
+.externalNativeBuild
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/.hgignore	Fri May 08 21:33:19 2020 -0400
@@ -0,0 +1,57 @@
+syntax: glob
+# generated files
+bin/
+gen/
+doc/javadoc
+
+# Local configuration file (sdk path, etc)
+local.properties
+
+# built application files
+*.apk
+*.ap_
+
+# keystore
+*.keystore
+*.jks
+
+# files for the dex VM
+*.dex
+
+# Java class files
+*.class
+
+# maven output folder
+target
+
+# Eclipse project files
+.classpath
+.project
+.metadata
+.settings
+
+# IntelliJ files
+.idea
+*.iml
+
+# OSX files
+.DS_Store
+
+# Windows files
+Thumbs.db
+
+# vi swap files
+.*.sw?
+
+# backup files
+*.bak
+*~
+
+# maven eclipse files
+.externalToolBuilders
+maven-eclipse.xml
+
+# gradle generated files
+build/
+.gradle/
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/README.md	Fri May 08 21:33:19 2020 -0400
@@ -0,0 +1,26 @@
+Geekdroid
+==========
+
+Geekdroid is an Android library used in various Android projects.
+
+Geekdroid is an open source library and is licensed under the GNU General Public License 3 and any later version.
+This means that you can get Geekdroid's code and modify it to suit your needs, as long as you publish the changes
+you make for everyone to benefit from as well.
+
+Geekdroid is built and maintained by community volunteers.
+
+Modules
+=======
+
+The project is composed of 2 modules:
+
+    * geekdroid is the main library
+    * geekdroid-firebase contains utilities to work with Firebase and Google Play services
+
+Build instructions
+==================
+
+Just use Gradle to build
+
+    ./gradlew build
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/build.gradle.kts	Fri May 08 21:33:19 2020 -0400
@@ -0,0 +1,36 @@
+import com.geekorum.build.SourceLicenseCheckerPlugin
+import com.geekorum.build.configureAnnotationProcessorDeps
+import com.geekorum.build.setupGoogleContent
+
+plugins {
+    id("com.android.library") apply false
+    kotlin("android") apply false
+    kotlin("kapt") apply false
+}
+
+
+// some extra properties
+extra["compileSdkVersion"] = "android-29"
+
+allprojects {
+    repositories {
+        google().setupGoogleContent()
+        jcenter()
+    }
+    apply<SourceLicenseCheckerPlugin>()
+}
+
+subprojects {
+    group = "com.geekorum"
+    version = "0.0.1"
+
+    configureAnnotationProcessorDeps()
+}
+
+task("clean", type = Delete::class) {
+    doLast {
+        delete(buildDir)
+    }
+}
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/buildSrc/build.gradle.kts	Fri May 08 21:33:19 2020 -0400
@@ -0,0 +1,37 @@
+import java.net.URI
+
+plugins {
+    `kotlin-dsl`
+}
+
+
+version = "1.0"
+
+kotlinDslPluginOptions {
+    experimentalWarning.set(false)
+}
+
+repositories {
+    gradlePluginPortal()
+    jcenter()
+    google()
+    maven {
+        // Workaround for genymotion plugin not working on gradle 5.0
+        // we publish 1.4.2 version with fixes
+        url = uri("https://raw.githubusercontent.com/fbarthelery/genymotion-gradle-plugin/master/repo/")
+    }
+    maven {
+        url = uri("https://kotlin.bintray.com/kotlinx")
+    }
+}
+
+dependencies {
+    implementation("com.android.tools.build:gradle:4.1.0-alpha06")
+    implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.3.71")
+    implementation("com.genymotion:plugin:1.4.2")
+    implementation("gradle.plugin.com.hierynomus.gradle.plugins:license-gradle-plugin:0.15.0")
+    implementation("com.github.triplet.gradle:play-publisher:2.7.2")
+
+    implementation("com.geekorum.gradle.avdl:plugin:0.0.2")
+    implementation("com.geekorum.gradle.avdl:flydroid:0.0.2")
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/buildSrc/src/main/kotlin/AndroidJavaVersion.kt	Fri May 08 21:33:19 2020 -0400
@@ -0,0 +1,48 @@
+package com.geekorum.build
+
+import com.android.build.gradle.BaseExtension
+import org.gradle.api.JavaVersion
+import org.gradle.api.Project
+import org.gradle.api.plugins.ExtensionAware
+import org.gradle.kotlin.dsl.dependencies
+import org.jetbrains.kotlin.gradle.dsl.KotlinJvmOptions
+
+/**
+ * Configure java version compile options based on minSdkVersion value
+ */
+fun BaseExtension.configureJavaVersion() {
+    val api = defaultConfig.minSdkVersion?.apiLevel ?: 0
+    val version = when {
+        api >= 24 -> JavaVersion.VERSION_1_8
+        api >= 19 -> JavaVersion.VERSION_1_7
+        else -> JavaVersion.VERSION_1_6
+    }
+    compileOptions {
+        sourceCompatibility = version
+        targetCompatibility = version
+    }
+
+    (this as ExtensionAware).extensions.findByType(KotlinJvmOptions::class.java)?.apply {
+        if (version >= JavaVersion.VERSION_1_8) {
+            jvmTarget = "1.8"
+        }
+    }
+}
+
+/**
+ * Add missing annotation processord dependencies to build on Java 11
+ */
+fun Project.configureAnnotationProcessorDeps() {
+    dependencies {
+        configurations.whenObjectAdded {
+            when (name) {
+                "kapt" -> {
+                    add(name,"javax.xml.bind:jaxb-api:2.3.1")
+                    add(name, "com.sun.xml.bind:jaxb-core:2.3.0.1")
+                    add(name, "com.sun.xml.bind:jaxb-impl:2.3.2")
+                }
+                "annotationProcessor" -> add(name, "javax.xml.bind:jaxb-api:2.3.1")
+            }
+        }
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/buildSrc/src/main/kotlin/AndroidPlayStorePublisher.kt	Fri May 08 21:33:19 2020 -0400
@@ -0,0 +1,46 @@
+package com.geekorum.build
+
+import com.android.build.gradle.AppExtension
+import com.github.triplet.gradle.play.PlayPublisherExtension
+import org.gradle.api.NamedDomainObjectContainer
+import org.gradle.api.Project
+import org.gradle.api.plugins.ExtensionAware
+import org.gradle.kotlin.dsl.apply
+import org.gradle.kotlin.dsl.configure
+import org.gradle.kotlin.dsl.getByType
+import org.gradle.kotlin.dsl.the
+
+
+// Configuration for "com.github.triplet.play" plugin
+// This configuration expects the given properties
+// PLAY_STORE_JSON_KEY_FILE: google play console service credentials json file to use
+// PLAY_STORE_TRACK: track to publish the build, default to internal but can be set to alpha, beta or production
+// PLAY_STORE_FROM_TRACK: track from which to promote a build, default to internal but can be set to alpha, beta or production
+
+internal fun Project.configureAndroidPlayStorePublisher(): Unit {
+    apply(plugin = "com.github.triplet.play")
+    configure<PlayPublisherExtension> {
+        defaultToAppBundles = true
+        serviceAccountCredentials = file(properties["PLAY_STORE_JSON_KEY_FILE"]!!)
+        track = properties.getOrDefault("PLAY_STORE_TRACK", "internal") as String
+        fromTrack = properties.getOrDefault("PLAY_STORE_FROM_TRACK", "internal") as String
+    }
+
+    val android = the<AppExtension>() as ExtensionAware
+
+    tasks.apply {
+        register("publishToGooglePlayStore") {
+            group = "Continuous Delivery"
+            description = "Publish project to Google play store"
+            dependsOn(named("publish"))
+        }
+
+        // only there for consistent naming scheme
+        register("promoteOnGooglePlayStore") {
+            group = "Continuous Delivery"
+            description = "Promote project Google play store"
+            dependsOn(named("promoteArtifact"))
+        }
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/buildSrc/src/main/kotlin/AndroidSigning.kt	Fri May 08 21:33:19 2020 -0400
@@ -0,0 +1,29 @@
+package com.geekorum.build
+
+import com.android.build.gradle.BaseExtension
+import org.gradle.api.Project
+
+internal fun Project.configureReleaseSigningConfig() {
+    val releaseStoreFile = findProperty("RELEASE_STORE_FILE") as? String ?: ""
+    val releaseStorePassword = findProperty("RELEASE_STORE_PASSWORD") as? String ?: ""
+    val releaseKeyAlias= findProperty("RELEASE_KEY_ALIAS") as? String ?: ""
+    val releaseKeyPassword= findProperty("RELEASE_KEY_PASSWORD") as? String ?: ""
+
+    extensions.configure<BaseExtension>("android") {
+        signingConfigs {
+            register("release") {
+                storeFile =  file(releaseStoreFile)
+                storePassword = releaseStorePassword
+                keyAlias = releaseKeyAlias
+                keyPassword = releaseKeyPassword
+            }
+        }
+
+        buildTypes {
+            named("release") {
+                signingConfig = signingConfigs.getByName("release")
+            }
+        }
+    }
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/buildSrc/src/main/kotlin/AndroidTests.kt	Fri May 08 21:33:19 2020 -0400
@@ -0,0 +1,112 @@
+package com.geekorum.build
+
+import com.android.build.gradle.BaseExtension
+import com.android.build.gradle.internal.dsl.TestOptions
+import org.gradle.api.Project
+import org.gradle.api.artifacts.Dependency
+import org.gradle.api.artifacts.DependencyConstraint
+import org.gradle.api.artifacts.ExternalModuleDependency
+import org.gradle.api.artifacts.dsl.DependencyConstraintHandler
+import org.gradle.api.artifacts.dsl.DependencyHandler
+import org.gradle.kotlin.dsl.closureOf
+import org.gradle.kotlin.dsl.configure
+import org.gradle.kotlin.dsl.dependencies
+import org.gradle.kotlin.dsl.kotlin
+
+const val espressoVersion = "3.2.0"
+const val androidxTestRunnerVersion = "1.3.0-alpha05"
+const val androidxTestCoreVersion = "1.3.0-alpha05"
+const val robolectricVersion = "4.3.1"
+
+
+/*
+ * Configuration for espresso and robolectric usage in an Android project
+ */
+internal fun Project.configureTests() {
+    extensions.configure<BaseExtension> {
+        defaultConfig {
+            testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+            testInstrumentationRunnerArgument("clearPackageData", "true")
+            testInstrumentationRunnerArgument("disableAnalytics", "true")
+        }
+
+        testOptions {
+            execution = "ANDROIDX_TEST_ORCHESTRATOR"
+            animationsDisabled = true
+
+            unitTests(closureOf<TestOptions.UnitTestOptions> {
+                isIncludeAndroidResources = true
+            })
+        }
+    }
+
+
+    dependencies {
+        dualTestImplementation(kotlin("test-junit"))
+
+        androidTestUtil("androidx.test:orchestrator:$androidxTestRunnerVersion")
+        androidTestImplementation("androidx.test:runner:$androidxTestRunnerVersion")
+        dualTestImplementation("androidx.test.ext:junit-ktx:1.1.1")
+
+        dualTestImplementation("androidx.test:core-ktx:$androidxTestCoreVersion")
+        dualTestImplementation("androidx.test:rules:$androidxTestRunnerVersion")
+        // fragment testing is usually declared on debugImplementation configuration and need these dependencies
+        constraints {
+            debugImplementation("androidx.test:core:$androidxTestCoreVersion")
+            debugImplementation("androidx.test:monitor:$androidxTestRunnerVersion")
+        }
+
+        dualTestImplementation("androidx.test.espresso:espresso-core:$espressoVersion")
+        dualTestImplementation("androidx.test.espresso:espresso-contrib:$espressoVersion")
+        dualTestImplementation("androidx.test.espresso:espresso-intents:$espressoVersion")
+
+        // assertions
+        dualTestImplementation("com.google.truth:truth:1.0")
+        dualTestImplementation("androidx.test.ext:truth:1.3.0-alpha01")
+
+        // mock
+        testImplementation("io.mockk:mockk:1.9.3")
+        androidTestImplementation("io.mockk:mockk-android:1.9.3")
+        testImplementation("org.robolectric:robolectric:$robolectricVersion")
+
+        constraints {
+            dualTestImplementation(kotlin("reflect")) {
+                because("Use the kotlin version that we use")
+            }
+            androidTestImplementation("org.objenesis:objenesis") {
+                because("3.x version use instructions only available with minSdk 26 (Android O)")
+                version {
+                    strictly("2.6")
+                }
+            }
+        }
+    }
+}
+
+fun DependencyHandler.dualTestImplementation(dependencyNotation: Any) {
+    add("androidTestImplementation", dependencyNotation)
+    add("testImplementation", dependencyNotation)
+}
+
+fun DependencyHandler.dualTestImplementation(dependencyNotation: Any, action: ExternalModuleDependency.() -> Unit) {
+    val closure = closureOf(action)
+    add("androidTestImplementation", dependencyNotation, closure)
+    add("testImplementation", dependencyNotation, closure)
+}
+
+internal fun DependencyHandler.androidTestImplementation(dependencyNotation: Any): Dependency? =
+    add("androidTestImplementation", dependencyNotation)
+
+internal fun DependencyHandler.androidTestImplementation(dependencyNotation: Any, action: ExternalModuleDependency.() -> Unit) {
+    val closure = closureOf(action)
+    add("androidTestImplementation", dependencyNotation, closure)
+}
+
+internal fun DependencyHandler.androidTestUtil(dependencyNotation: Any): Dependency? =
+    add("androidTestUtil", dependencyNotation)
+
+internal fun DependencyHandler.testImplementation(dependencyNotation: Any): Dependency? =
+    add("testImplementation", dependencyNotation)
+
+internal fun DependencyConstraintHandler.debugImplementation(dependencyConstraintNotation: Any): DependencyConstraint? =
+    add("debugImplementation", dependencyConstraintNotation)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/buildSrc/src/main/kotlin/Avdl.kt	Fri May 08 21:33:19 2020 -0400
@@ -0,0 +1,129 @@
+/*
+ * Geekttrss is a RSS feed reader application on the Android Platform.
+ *
+ * Copyright (C) 2017-2020 by Frederic-Charles Barthelery.
+ *
+ * This file is part of Geekttrss.
+ *
+ * Geekttrss is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Geekttrss is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Geekttrss.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.geekorum.build
+
+import com.android.build.gradle.AppPlugin
+import com.android.build.gradle.DynamicFeaturePlugin
+import com.android.build.gradle.LibraryPlugin
+import com.android.build.gradle.TestedExtension
+import com.android.build.gradle.api.TestVariant
+import com.geekorum.gradle.avdl.AvdlExtension
+import com.geekorum.gradle.avdl.providers.flydroid.FlydroidPlugin
+import com.geekorum.gradle.avdl.providers.flydroid.flydroid
+import com.geekorum.gradle.avdl.tasks.LaunchDeviceTask
+import com.geekorum.gradle.avdl.tasks.StopDeviceTask
+import com.geekorum.gradle.avdl.tasks.orderForTask
+import com.geekorum.gradle.avdl.tasks.registerAvdlDevicesTask
+import org.gradle.api.Project
+import org.gradle.api.Task
+import org.gradle.api.provider.Provider
+import org.gradle.api.services.BuildService
+import org.gradle.api.services.BuildServiceParameters
+import org.gradle.api.tasks.TaskContainer
+import org.gradle.api.tasks.TaskProvider
+import org.gradle.kotlin.dsl.*
+
+
+fun Project.configureAvdlDevices(flydroidUrl: String, flydroidKey: String) {
+    apply<FlydroidPlugin>()
+
+    val oneInstrumentedTestService = gradle.sharedServices.registerIfAbsent(
+            "oneInstrumentedTest", OneInstrumentedTestService::class.java) {
+        maxParallelUsages.set(1)
+    }
+
+    rootProject.serializeInstrumentedTestTask(oneInstrumentedTestService)
+
+    val android = the<TestedExtension>()
+    configure<AvdlExtension> {
+        devices {
+            android.testVariants.all {
+                register("android-n-${project.path}-$baseName") {
+                    setup = flydroid {
+                        url = flydroidUrl
+                        this.flydroidKey = flydroidKey
+                        // android-q images fail, don't manage to start the tests
+                        image = "android-n"
+                         useTunnel = true
+                    }
+                }
+            }
+        }
+    }
+
+    tasks {
+        var lastStopTask: TaskProvider<out Task>? = null
+        var lastTestTask: TaskProvider<out Task>? = null
+        android.testVariants.all {
+            val (startTask, stopTask ) =
+                registerAvdlDevicesTaskForVariant(this, listOf("android-n-${project.path}-$baseName"))
+            listOf(startTask, stopTask).forEach {
+                it.configure {
+                    usesService(oneInstrumentedTestService)
+                }
+            }
+
+            lastStopTask?.let {
+                startTask.configure {
+                    mustRunAfter(it)
+                }
+            }
+            lastTestTask?.let {
+                startTask.configure {
+                    mustRunAfter(it)
+                }
+            }
+            lastStopTask = stopTask
+            lastTestTask = connectedInstrumentTestProvider
+        }
+    }
+}
+
+private fun TaskContainer.registerAvdlDevicesTaskForVariant(
+    variant: TestVariant, devices: List<String>
+): Pair<TaskProvider<LaunchDeviceTask>, TaskProvider<StopDeviceTask>> {
+    val tasks =
+        registerAvdlDevicesTask(variant.name, devices)
+    tasks.orderForTask(variant.connectedInstrumentTestProvider)
+    return tasks
+}
+
+
+private fun Project.serializeInstrumentedTestTask(oneInstrumentedTestService: Provider<OneInstrumentedTestService>) {
+    fun Project.configureTestTasks() {
+        extensions.configure<TestedExtension> {
+            testVariants.all {
+                connectedInstrumentTestProvider.configure {
+                    usesService(oneInstrumentedTestService)
+                }
+            }
+        }
+    }
+
+    allprojects {
+        val project = this
+        plugins.withType<AppPlugin> { project.configureTestTasks() }
+        plugins.withType<DynamicFeaturePlugin> { project.configureTestTasks() }
+        plugins.withType<LibraryPlugin> { project.configureTestTasks() }
+    }
+}
+
+abstract class OneInstrumentedTestService : BuildService<BuildServiceParameters.None>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/buildSrc/src/main/kotlin/Repositories.kt	Fri May 08 21:33:19 2020 -0400
@@ -0,0 +1,21 @@
+package com.geekorum.build
+
+import org.gradle.api.artifacts.repositories.MavenArtifactRepository
+
+/**
+ * Setup the content of google() repository
+ */
+fun MavenArtifactRepository.setupGoogleContent() = apply {
+    require(name == "Google") { "Only apply to `google()` repository "}
+    content {
+        includeGroupByRegex("""android\.arch\..*""")
+        includeGroupByRegex("""androidx\..*""")
+        includeGroupByRegex("""com\.android\..*""")
+        includeGroupByRegex("""com\.google\..*""")
+        includeGroup("com.crashlytics.sdk.android")
+        includeGroup("io.fabric.sdk.android")
+        includeGroup("org.chromium.net")
+        includeGroup("zipflinger")
+        includeGroup("com.android")
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/buildSrc/src/main/kotlin/RepositoryChangeset.kt	Fri May 08 21:33:19 2020 -0400
@@ -0,0 +1,52 @@
+package com.geekorum.build
+
+import org.gradle.api.Project
+import java.io.File
+import java.io.IOException
+import java.util.concurrent.TimeUnit
+
+internal fun Project.getGitSha1(): String? = runCommand("git rev-parse HEAD", workingDir = projectDir)?.trim()
+
+internal fun Project.getHgSha1(): String? = runCommand("hg id --debug -i -r .", workingDir = projectDir)?.trim()
+
+internal fun Project.getHgLocalRevisionNumber(): String? = runCommand("hg id -n -r .", workingDir = projectDir)?.trim()
+
+fun Project.getChangeSet(): String {
+    val git = rootProject.file(".git")
+    val hg = rootProject.file(".hg")
+    return when {
+        git.exists() -> "git:${getGitSha1()}"
+        hg.exists() -> "hg:${getHgSha1()}"
+        else -> "unknown"
+    }
+}
+
+/**
+ * Compute a version code following this format : MmmPBBB
+ * M is major, mm is minor, P is patch
+ * BBB is build version number from hg
+ */
+fun Project.computeChangesetVersionCode(major: Int = 0, minor: Int = 0, patch: Int = 0): Int {
+    val base = (major * 1000000) + (minor * 10000) + (patch * 1000)
+    return base + (getHgLocalRevisionNumber()?.trim()?.toIntOrNull() ?: 0)
+}
+
+private fun Project.runCommand(
+    command: String,
+    workingDir: File = File("."),
+    timeoutAmount: Long = 60,
+    timeoutUnit: TimeUnit = TimeUnit.MINUTES
+): String? {
+    return try {
+        ProcessBuilder(*command.split("\\s".toRegex()).toTypedArray())
+            .directory(workingDir)
+            .redirectOutput(ProcessBuilder.Redirect.PIPE)
+            .redirectError(ProcessBuilder.Redirect.PIPE)
+            .start().apply {
+                waitFor(timeoutAmount, timeoutUnit)
+            }.inputStream.bufferedReader().readText()
+    } catch (e: IOException) {
+        logger.info("Unable to run command", e)
+        null
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/buildSrc/src/main/kotlin/SourceLicenseChecker.kt	Fri May 08 21:33:19 2020 -0400
@@ -0,0 +1,47 @@
+package com.geekorum.build
+
+import com.hierynomus.gradle.license.LicenseBasePlugin
+import com.hierynomus.gradle.license.tasks.LicenseCheck
+import com.hierynomus.gradle.license.tasks.LicenseFormat
+import nl.javadude.gradle.plugins.license.LicenseExtension
+import nl.javadude.gradle.plugins.license.LicensePlugin
+import org.gradle.api.Project
+import org.gradle.api.Task
+import org.gradle.kotlin.dsl.apply
+import org.gradle.kotlin.dsl.configure
+import org.gradle.kotlin.dsl.invoke
+import org.gradle.kotlin.dsl.named
+
+internal fun Project.configureSourceLicenseChecker(): Unit {
+    apply<LicensePlugin>()
+
+    configure<LicenseExtension> {
+        header = file("$rootDir/config/license/header.txt")
+        // ignore failures for now until we set the final license
+        ignoreFailures = true
+
+        excludes(listOf("**/*.webp", "**/*.png"))
+    }
+
+    tasks {
+        val checkKotlinFilesLicenseTask = register("checkKotlinFilesLicense", LicenseCheck::class.java) {
+            source = fileTree("src").apply {
+                include("**/*.kt")
+            }
+        }
+
+        val formatKotlinFilesLicenseTask = register("formatKotlinFilesLicense", LicenseFormat::class.java) {
+            source = fileTree("src").apply {
+                include("**/*.kt")
+            }
+        }
+
+        named<Task>(LicenseBasePlugin.getLICENSE_TASK_BASE_NAME()) {
+            dependsOn(checkKotlinFilesLicenseTask)
+        }
+
+        named<Task>(LicenseBasePlugin.getFORMAT_TASK_BASE_NAME()) {
+            dependsOn(formatKotlinFilesLicenseTask)
+        }
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/buildSrc/src/main/kotlin/VersionAlignment.kt	Fri May 08 21:33:19 2020 -0400
@@ -0,0 +1,59 @@
+package com.geekorum.build
+
+import org.gradle.api.artifacts.ComponentMetadataContext
+import org.gradle.api.artifacts.ComponentMetadataRule
+import org.gradle.api.artifacts.Dependency
+import org.gradle.api.artifacts.dsl.ComponentMetadataHandler
+import org.gradle.api.artifacts.dsl.DependencyHandler
+
+private val componentsPlatform = mutableMapOf<ComponentMetadataHandler, MutableSet<String>>()
+
+fun DependencyHandler.createComponentsPlatforms() {
+    components.apply {
+        getOrCreatePlatform(DaggerPlatform)
+    }
+}
+
+private fun ComponentMetadataHandler.getOrCreatePlatform(platformFactory: PlatformFactory): String {
+    val componentsSet = componentsPlatform.getOrPut(this) { mutableSetOf() }
+    if (!componentsSet.contains(platformFactory.platformName)) {
+        componentsSet.add(platformFactory.createPlatform(this))
+    }
+    return platformFactory.platformName
+}
+
+internal class DaggerPlatform {
+    companion object : PlatformFactory("com.google.dagger:dagger-platform",
+        AlignmentRule::class.java)
+
+    open class AlignmentRule : SameGroupAlignmentRule(platformName, "com.google.dagger")
+}
+
+fun DependencyHandler.enforcedDaggerPlatform(version: String): Dependency {
+    return enforcedPlatform("${components.getOrCreatePlatform(DaggerPlatform)}:$version")
+}
+
+open class PlatformFactory(
+    internal val platformName: String,
+    private val alignmentRule: Class<out ComponentMetadataRule>
+) {
+    fun createPlatform(components: ComponentMetadataHandler): String {
+        components.all(alignmentRule)
+        return platformName
+    }
+}
+
+internal open class SameGroupAlignmentRule(
+    private val platformName: String,
+    private val group: String
+) : ComponentMetadataRule {
+
+    override fun execute(ctx: ComponentMetadataContext) {
+        ctx.details.run {
+            if (id.group == group) {
+                belongsTo("$platformName:${id.version}")
+            }
+        }
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/buildSrc/src/main/kotlin/android-avdl.gradle.kts	Fri May 08 21:33:19 2020 -0400
@@ -0,0 +1,39 @@
+/*
+ * Geekttrss is a RSS feed reader application on the Android Platform.
+ *
+ * Copyright (C) 2017-2020 by Frederic-Charles Barthelery.
+ *
+ * This file is part of Geekttrss.
+ *
+ * Geekttrss is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Geekttrss is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Geekttrss.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.geekorum.build
+
+/**
+ * This defines the configuration to run tests with gradle-avdl.
+ * The CI will run the test on on the following devices.
+ * To run this your project must have declared the following gradle properties
+ * via command line or your gradle.properties
+ * RUN_TESTS_ON_AVDL=true
+ */
+
+private val runTestOnAvdl = findProperty("RUN_TESTS_ON_AVDL") as String?
+val runTests =  runTestOnAvdl?.toBoolean() ?: false
+
+if (runTests) {
+    val flydroidUrl = checkNotNull(findProperty("FLYDROID_URL") as String?) { "FLYDROID_URL property not specified" }
+    val flydroidKey = checkNotNull(findProperty("FLYDROID_KEY") as String?) { "FLYDROID_KEY property not specified" }
+
+    configureAvdlDevices(flydroidUrl, flydroidKey)
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/buildSrc/src/main/kotlin/android-signing.gradle.kts	Fri May 08 21:33:19 2020 -0400
@@ -0,0 +1,13 @@
+package com.geekorum.build
+
+/**
+ * Define the following properties to set the signing configuration for release build
+ *    - RELEASE_STORE_FILE : Path to the keystore file
+ *    - RELEASE_STORE_PASSWORD: Password of the keystore file
+ *    - RELEASE_KEY_ALIAS: key alias to use to sign
+ *    - RELEASE_KEY_PASSWORD: password of the key alias
+ */
+
+if (findProperty("RELEASE_STORE_FILE") != null) {
+    configureReleaseSigningConfig()
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/buildSrc/src/main/kotlin/android-tests.gradle.kts	Fri May 08 21:33:19 2020 -0400
@@ -0,0 +1,3 @@
+package com.geekorum.build
+
+configureTests()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/buildSrc/src/main/kotlin/play-store-publish.gradle.kts	Fri May 08 21:33:19 2020 -0400
@@ -0,0 +1,13 @@
+package com.geekorum.build
+
+/**
+ Configuration for "com.github.triplet.play" plugin
+ This configuration expects the given properties
+ PLAY_STORE_JSON_KEY_FILE: google play console service credentials json file to use
+ PLAY_STORE_TRACK: track to publish the build, default to internal but can be set to alpha, beta or production
+ PLAY_STORE_FROM_TRACK: track from which to promote a build, default to internal but can be set to alpha, beta or production
+*/
+
+if (findProperty("PLAY_STORE_JSON_KEY_FILE") != null) {
+    configureAndroidPlayStorePublisher()
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/buildSrc/src/main/kotlin/source-license-checker.gradle.kts	Fri May 08 21:33:19 2020 -0400
@@ -0,0 +1,14 @@
+package com.geekorum.build
+
+/**
+ * You need to define a License header file in "$rootDir/config/license/header.txt"
+ * Define the following property to enable check of license headers
+ *    - CHECK_LICENSE_HEADERS : true or false. default is false
+ */
+
+val checkLicenseHeadersString = findProperty("CHECK_LICENSE_HEADERS") as String?
+val checkLicenseHeader =  checkLicenseHeadersString?.toBoolean() ?: false
+
+if (checkLicenseHeader) {
+    configureSourceLicenseChecker()
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/config/README.md	Fri May 08 21:33:19 2020 -0400
@@ -0,0 +1,8 @@
+Config
+======
+
+This directory contains various configuration files for gradle and its plugins :
+
+  * checkstyle configuration
+  * task to get a source archive of the project
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/config/android-checkstyle.gradle	Fri May 08 21:33:19 2020 -0400
@@ -0,0 +1,28 @@
+apply plugin: 'checkstyle'
+check.dependsOn 'checkstyle'
+
+checkstyle {
+    toolVersion = '6.19'
+}
+
+task checkstyle(type: Checkstyle) {
+    description = "Check Java style with Checkstyle"
+    configFile = file("${project.rootDir}/config/checkstyle/checkstyle.xml")
+    source = javaSources()
+    classpath = files()
+    ignoreFailures = true
+}
+
+def javaSources() {
+    def files = []
+    android.sourceSets.each { sourceSet ->
+        sourceSet.java.each { javaSource ->
+            javaSource.getSrcDirs().each {
+                if (it.exists()) {
+                    files.add(it)
+                }
+            }
+        }
+    }
+    return files
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/config/android-maven-publication.gradle	Fri May 08 21:33:19 2020 -0400
@@ -0,0 +1,30 @@
+/**
+ * This configuration allows to publish android libraries into a maven repository.
+ * You need to define in your build.gradle the following variables :
+ * version = "1.0"                // the version of your projects
+ * ext.artifactId="geny-widgets"  // the maven artifact id
+ * Use ./gradlew tasks to see the name of the publishing tasks.
+ */
+if (plugins.hasPlugin("com.android.library")) {
+
+    apply plugin: 'maven-publish'
+
+    if (!project.ext.properties.containsKey("artifactId")) {
+        ext.artifactId = project.name
+    }
+
+    publishing {
+        publications {
+            projectRelease(MavenPublication) {
+                artifactId project.artifactId
+                artifact "build/outputs/aar/${project.archivesBaseName}-release.aar"
+            }
+
+            projectSnapshot(MavenPublication) {
+                artifactId project.artifactId
+                artifact "build/outputs/aar/${project.archivesBaseName}-debug.aar"
+                version getVersion() + "-SNAPSHOT"
+            }
+        }
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/config/checkstyle/checkstyle.xml	Fri May 08 21:33:19 2020 -0400
@@ -0,0 +1,182 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE module PUBLIC
+  "-//Puppy Crawl//DTD Check Configuration 1.3//EN"
+  "http://www.puppycrawl.com/dtds/configuration_1_3.dtd">
+
+<!-- This is a checkstyle configuration file. For descriptions of
+what the following rules do, please see the checkstyle configuration
+page at http://checkstyle.sourceforge.net/config.html -->
+
+<module name="Checker">
+
+  <!-- Checks whether files end with a new line.                        -->
+  <!-- See http://checkstyle.sf.net/config_misc.html#NewlineAtEndOfFile -->
+  <module name="NewlineAtEndOfFile" />
+
+  <!-- Checks that property files contain the same keys.         -->
+  <!-- See http://checkstyle.sf.net/config_misc.html#Translation -->
+  <module name="Translation" />
+
+  <!-- Checks for Size Violations.                    -->
+  <!-- See http://checkstyle.sf.net/config_sizes.html -->
+  <module name="FileLength" />
+
+  <!-- Checks for whitespace                               -->
+  <!-- See http://checkstyle.sf.net/config_whitespace.html -->
+  <module name="FileTabCharacter" />
+
+  <!-- Miscellaneous other checks.                   -->
+  <!-- See http://checkstyle.sf.net/config_misc.html -->
+  <module name="RegexpSingleline">
+    <property name="format" value="\s+$" />
+    <property name="minimum" value="0" />
+    <property name="maximum" value="0" />
+    <property name="message" value="Line has trailing spaces." />
+    <property name="severity" value="info" />
+  </module>
+
+  <module name="SuppressWarningsFilter"/>
+
+  <module name="TreeWalker">
+
+    <!-- Checks for Naming Conventions.                  -->
+    <!-- See http://checkstyle.sf.net/config_naming.html -->
+    <module name="ConstantName" />
+    <module name="LocalFinalVariableName" />
+    <module name="LocalVariableName" />
+    <module name="MemberName" />
+    <module name="MethodName" />
+    <module name="PackageName" />
+    <module name="ParameterName" />
+    <module name="StaticVariableName" />
+    <module name="TypeName" />
+
+    <module name="SuppressWarningsHolder"/>
+
+    <!-- Checks for imports                              -->
+    <!-- See http://checkstyle.sf.net/config_import.html -->
+    <module name="AvoidStarImport">
+      <property name="allowStaticMemberImports" value="true" />
+    </module>
+    <module name="IllegalImport" />
+    <!-- defaults to sun.* packages -->
+    <module name="RedundantImport" />
+    <module name="UnusedImports" />
+    <module name="CustomImportOrder">
+        <property name="thirdPartyPackageRegExp" value=".*"/>
+        <property name="specialImportsRegExp" value="com.genymobile"/>
+        <property name="separateLineBetweenGroups" value="true"/>
+        <property name="customImportOrderRules" value="SPECIAL_IMPORTS###THIRD_PARTY_PACKAGE###STANDARD_JAVA_PACKAGE###STATIC"/>
+    </module>
+
+
+    <!-- Checks for Size Violations.                    -->
+    <!-- See http://checkstyle.sf.net/config_sizes.html -->
+    <module name="LineLength">
+      <!-- what is a good max value? -->
+      <property name="max" value="150" />
+      <!-- ignore lines like "$File: //depot/... $" -->
+      <property name="ignorePattern" value="\$File.*\$" />
+      <property name="severity" value="info" />
+    </module>
+    <module name="MethodLength" />
+    <module name="ParameterNumber">
+      <property name="ignoreOverriddenMethods" value="true"/>
+    </module>
+
+
+    <!-- Checks for whitespace                               -->
+    <!-- See http://checkstyle.sf.net/config_whitespace.html -->
+    <module name="EmptyForIteratorPad" />
+    <module name="GenericWhitespace" />
+    <module name="MethodParamPad" />
+    <module name="NoWhitespaceAfter" />
+    <module name="NoWhitespaceBefore" />
+    <module name="OperatorWrap" />
+    <module name="ParenPad" />
+    <module name="TypecastParenPad" />
+    <module name="WhitespaceAfter" />
+    <module name="WhitespaceAround" />
+
+    <!-- Modifier Checks                                    -->
+    <!-- See http://checkstyle.sf.net/config_modifiers.html -->
+    <module name="ModifierOrder" />
+    <module name="RedundantModifier" />
+
+
+    <!-- Checks for blocks. You know, those {}'s         -->
+    <!-- See http://checkstyle.sf.net/config_blocks.html -->
+    <module name="AvoidNestedBlocks" />
+    <module name="EmptyBlock">
+      <property name="option" value="text" />
+    </module>
+    <module name="LeftCurly" />
+    <module name="NeedBraces" />
+    <module name="RightCurly" />
+
+
+    <!-- Checks for common coding problems               -->
+    <!-- See http://checkstyle.sf.net/config_coding.html -->
+    <!-- <module name="AvoidInlineConditionals"/> -->
+    <module name="EmptyStatement" />
+    <module name="EqualsHashCode" />
+    <module name="HiddenField">
+      <property name="tokens" value="VARIABLE_DEF" />
+      <!-- only check variables not parameters -->
+      <property name="ignoreConstructorParameter" value="true" />
+      <property name="ignoreSetter" value="true" />
+      <property name="severity" value="warning" />
+    </module>
+    <module name="IllegalInstantiation" />
+    <module name="InnerAssignment" />
+    <module name="MagicNumber">
+      <property name="severity" value="info" />
+      <property name="ignoreHashCodeMethod" value="true" />
+      <property name="ignoreAnnotation" value="true" />
+    </module>
+    <module name="MissingSwitchDefault" />
+    <module name="SimplifyBooleanExpression" />
+    <module name="SimplifyBooleanReturn" />
+
+    <!-- Checks for class design                         -->
+    <!-- See http://checkstyle.sf.net/config_design.html -->
+    <!-- <module name="DesignForExtension"/> -->
+    <module name="FinalClass" />
+    <module name="HideUtilityClassConstructor" />
+    <module name="InterfaceIsType" />
+    <module name="VisibilityModifier" />
+
+
+    <!-- Miscellaneous other checks.                   -->
+    <!-- See http://checkstyle.sf.net/config_misc.html -->
+    <module name="ArrayTypeStyle" />
+    <!-- <module name="FinalParameters"/> -->
+    <module name="TodoComment">
+      <property name="format" value="TODO" />
+      <property name="severity" value="info" />
+    </module>
+    <module name="UpperEll" />
+
+    <module name="FileContentsHolder" />
+    <!-- Required by comment suppression filters -->
+
+  </module>
+
+  <module name="SuppressionFilter">
+    <!--<property name="file" value="team-props/checkstyle/checkstyle-suppressions.xml" />-->
+  </module>
+
+  <!-- Enable suppression comments -->
+  <module name="SuppressionCommentFilter">
+    <property name="offCommentFormat" value="CHECKSTYLE IGNORE\s+(\S+)" />
+    <property name="onCommentFormat" value="CHECKSTYLE END IGNORE\s+(\S+)" />
+    <property name="checkFormat" value="$1" />
+  </module>
+  <module name="SuppressWithNearbyCommentFilter">
+    <!-- Syntax is "SUPPRESS CHECKSTYLE name" -->
+    <property name="commentFormat" value="SUPPRESS CHECKSTYLE (\w+)" />
+    <property name="checkFormat" value="$1" />
+    <property name="influenceFormat" value="1" />
+  </module>
+
+</module>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/config/java-checkstyle.gradle	Fri May 08 21:33:19 2020 -0400
@@ -0,0 +1,8 @@
+apply plugin: 'checkstyle'
+
+checkstyle {
+    toolVersion = '6.19'
+    configFile = file("${project.rootDir}/config/checkstyle/checkstyle.xml")
+}
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/config/license/header.txt	Fri May 08 21:33:19 2020 -0400
@@ -0,0 +1,20 @@
+Geekdroid is a utility library for development on the Android
+Platform.
+
+Copyright (C) 2017-2020 by Frederic-Charles Barthelery.
+
+This file is part of Geekdroid.
+
+Geekdroid is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+Geekdroid is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with Geekdroid.  If not, see <http://www.gnu.org/licenses/>.
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/config/source-archive.gradle	Fri May 08 21:33:19 2020 -0400
@@ -0,0 +1,38 @@
+task sourceArchive {
+    description "Get the source archive of the project"
+}
+
+sourceArchive.doLast {
+    makeSourceArchive(rootProject.name)
+}
+
+project.build.dependsOn sourceArchive
+
+def makeSourceArchive(String projectName) {
+    def git = new File(".git")
+    def hg = new File(".hg")
+    def tag
+    if (git.exists()) {
+        tag = getGitSha1()
+        println("Building source archive \"${projectName}-src-${tag}.zip\" from git")
+        exec {
+            workingDir project.rootDir
+            commandLine "git", "archive", "--format=zip", "--prefix=${projectName}-src-${tag}/", "-o", "${projectName}-src-${tag}.zip", "HEAD"
+        }
+    } else if (hg.exists()) {
+        tag = getHgSha1()
+        println("Building source archive \"${projectName}-src-${tag}.zip\" from hg")
+        exec {
+            workingDir project.rootDir
+            commandLine "hg", "archive", "-t", "zip", "-r", tag, "${projectName}-src-${tag}.zip"
+        }
+    }
+}
+
+def getGitSha1() {
+    return 'git rev-parse HEAD'.execute().text.trim()
+}
+
+def getHgSha1() {
+    return 'hg id --debug -i -r .'.execute().text.trim()
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/geekdroid-firebase/build.gradle.kts	Fri May 08 21:33:19 2020 -0400
@@ -0,0 +1,63 @@
+import com.geekorum.build.configureJavaVersion
+
+plugins {
+    id("com.android.library")
+    kotlin("android")
+    id("com.geekorum.build.android-tests")
+    id("com.geekorum.build.android-avdl")
+}
+
+val artifactId by extra(name)
+
+android {
+    val compileSdkVersion: String by rootProject.extra
+    compileSdkVersion(compileSdkVersion)
+
+    defaultConfig {
+        minSdkVersion(24)
+        targetSdkVersion(29)
+    }
+    configureJavaVersion()
+
+    buildTypes {
+        getByName("release") {
+            isMinifyEnabled = false
+            proguardFiles(getDefaultProguardFile("proguard-android.txt"),
+                "proguard-rules.pro")
+        }
+    }
+
+    lintOptions {
+        isAbortOnError = false
+    }
+
+}
+
+dependencies {
+    implementation(enforcedPlatform(kotlin("bom")))
+    implementation(kotlin("stdlib-jdk8"))
+
+    implementation(enforcedPlatform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.3.5"))
+    api("org.jetbrains.kotlinx:kotlinx-coroutines-core")
+    api("org.jetbrains.kotlinx:kotlinx-coroutines-play-services")
+
+    implementation("com.jakewharton.timber:timber:4.7.1")
+
+    implementation("com.crashlytics.sdk.android:crashlytics:2.10.1")
+    implementation("com.google.firebase:firebase-crashlytics:17.0.0-beta02")
+
+    api("com.google.firebase:firebase-firestore-ktx:21.4.1")
+    implementation("com.google.firebase:firebase-auth:19.3.0")
+
+    // not firebase but they often work together so here we are
+    implementation("com.google.android.gms:play-services-location:17.0.0")
+
+    // not firebase but similar to gms api
+    implementation("com.google.android.play:core:1.7.1")
+
+    // fix for guava conflict
+    // firebase depends on a older version of these dependencies while testImplementation dependencies
+    // depends on new version
+    implementation("org.checkerframework:checker-compat-qual:2.5.5")
+    implementation("com.google.guava:guava:27.0.1-android")
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/geekdroid-firebase/proguard-rules.pro	Fri May 08 21:33:19 2020 -0400
@@ -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
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/geekdroid-firebase/src/androidTest/java/com/geekorum/geekdroid/firebase/ExampleInstrumentedTest.java	Fri May 08 21:33:19 2020 -0400
@@ -0,0 +1,46 @@
+/**
+ * Geekdroid is a utility library for development on the Android
+ * Platform.
+ *
+ * Copyright (C) 2017-2020 by Frederic-Charles Barthelery.
+ *
+ * This file is part of Geekdroid.
+ *
+ * Geekdroid is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Geekdroid is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Geekdroid.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.geekorum.geekdroid.firebase;
+
+import android.content.Context;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
+ */
+@RunWith(AndroidJUnit4.class)
+public class ExampleInstrumentedTest {
+    @Test
+    public void useAppContext() {
+        // Context of the app under test.
+        Context appContext = ApplicationProvider.getApplicationContext();
+
+        assertEquals("com.geekorum.geekdroid.firebase.test", appContext.getPackageName());
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/geekdroid-firebase/src/main/AndroidManifest.xml	Fri May 08 21:33:19 2020 -0400
@@ -0,0 +1,2 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.geekorum.geekdroid.firebase" />
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/geekdroid-firebase/src/main/java/com/geekorum/geekdroid/firebase/CurrentUserLiveData.kt	Fri May 08 21:33:19 2020 -0400
@@ -0,0 +1,46 @@
+/**
+ * Geekdroid is a utility library for development on the Android
+ * Platform.
+ *
+ * Copyright (C) 2017-2020 by Frederic-Charles Barthelery.
+ *
+ * This file is part of Geekdroid.
+ *
+ * Geekdroid is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Geekdroid is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Geekdroid.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.geekorum.geekdroid.firebase
+
+import androidx.lifecycle.LiveData
+import com.google.firebase.auth.FirebaseAuth
+import com.google.firebase.auth.FirebaseUser
+
+/**
+ * Livedata to observe the [FirebaseAuth.getCurrentUser]
+ */
+class CurrentUserLiveData(
+    private val firebaseAuth: FirebaseAuth = FirebaseAuth.getInstance()
+): LiveData<FirebaseUser?>() {
+
+    private val authStateListener = { it: FirebaseAuth ->
+        value = it.currentUser
+    }
+
+    override fun onActive() {
+        firebaseAuth.addAuthStateListener(authStateListener)
+    }
+
+    override fun onInactive() {
+        firebaseAuth.removeAuthStateListener(authStateListener)
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/geekdroid-firebase/src/main/java/com/geekorum/geekdroid/firebase/Firestore.kt	Fri May 08 21:33:19 2020 -0400
@@ -0,0 +1,116 @@
+/*
+ * Geekdroid is a utility library for development on the Android
+ * Platform.
+ *
+ * Copyright (C) 2017-2020 by Frederic-Charles Barthelery.
+ *
+ * This file is part of Geekdroid.
+ *
+ * Geekdroid is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Geekdroid is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Geekdroid.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.geekorum.geekdroid.firebase
+
+import androidx.lifecycle.LiveData
+import com.google.firebase.firestore.CollectionReference
+import com.google.firebase.firestore.DocumentReference
+import com.google.firebase.firestore.DocumentSnapshot
+import com.google.firebase.firestore.ListenerRegistration
+import com.google.firebase.firestore.Query
+import com.google.firebase.firestore.QuerySnapshot
+import com.google.firebase.firestore.ktx.toObject
+import com.google.firebase.firestore.ktx.toObjects
+import kotlinx.coroutines.tasks.await
+import timber.log.Timber
+
+
+class FirestoreQueryLiveData<T> constructor(
+    private val query: Query,
+    private val clazz: Class<T>
+) : LiveData<List<T>>() {
+
+    private val TAG = FirestoreQueryLiveData::class.java.simpleName
+    private var listenerRegistration: ListenerRegistration? = null
+
+
+    override fun onActive() {
+        listenerRegistration = query.addSnapshotListener { snapshot, firestoreException ->
+            if (firestoreException != null) {
+                Timber.e(firestoreException, "Error when listening to firestore")
+            }
+            value = snapshot?.toObjects(clazz) ?: emptyList()
+        }
+
+    }
+
+    override fun onInactive() {
+        super.onInactive()
+        listenerRegistration?.remove()
+    }
+}
+
+inline fun <reified T> Query.toLiveData() : LiveData<List<T>> =
+    FirestoreQueryLiveData(this)
+
+inline fun <reified T> FirestoreQueryLiveData(query: Query): FirestoreQueryLiveData<T> {
+    return FirestoreQueryLiveData(query, T::class.java)
+}
+
+class FirestoreDocumentLiveData<T>(
+    private val document: DocumentReference,
+    private val clazz: Class<T>
+) : LiveData<T>() {
+
+    private val TAG = FirestoreDocumentLiveData::class.java.simpleName
+    private var listenerRegistration: ListenerRegistration? = null
+
+
+    override fun onActive() {
+        listenerRegistration = document.addSnapshotListener { snapshot, firestoreException ->
+            if (firestoreException != null) {
+                Timber.e(firestoreException, "Error when listening to firestore")
+            }
+            value = snapshot?.toObject(clazz)
+        }
+    }
+
+    override fun onInactive() {
+        super.onInactive()
+        listenerRegistration?.remove()
+    }
+}
+
+inline fun <reified T> DocumentReference.toLiveData(): LiveData<T?> =
+    FirestoreDocumentLiveData(this)
+
+inline fun <reified T> FirestoreDocumentLiveData(document: DocumentReference): FirestoreDocumentLiveData<T> {
+    return FirestoreDocumentLiveData(document, T::class.java)
+}
+
+@Deprecated("Use firebase-firestore-ktx", ReplaceWith("toObject()", imports = ["com.google.firebase.firestore.ktx.toObject"]))
+inline fun <reified T> DocumentSnapshot.toObject(): T? = toObject()
+
+@Deprecated("Use firebase-firestore-ktx", ReplaceWith("toObjects()", imports = ["com.google.firebase.firestore.ktx.toObjects"]))
+inline fun <reified T: Any> QuerySnapshot.toObjects(): List<T> = toObjects()
+
+/* suspend version of get(), set(), update(), delete() */
+suspend fun DocumentReference.aSet(pojo: Any): Void = set(pojo).await()
+suspend fun DocumentReference.aUpdate(data: Map<String, Any>): Void = update(data).await()
+suspend fun DocumentReference.aDelete(): Void = delete().await()
+suspend fun DocumentReference.aGet(): DocumentSnapshot = get().await()
+suspend fun CollectionReference.aAdd(pojo: Any): DocumentReference = add(pojo).await()
+
+
+suspend inline fun <reified T> DocumentReference.toObject(): T? {
+    return get().await().toObject()
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/geekdroid-firebase/src/main/java/com/geekorum/geekdroid/firebase/Tasks.kt	Fri May 08 21:33:19 2020 -0400
@@ -0,0 +1,31 @@
+/**
+ * Geekdroid is a utility library for development on the Android
+ * Platform.
+ *
+ * Copyright (C) 2017-2020 by Frederic-Charles Barthelery.
+ *
+ * This file is part of Geekdroid.
+ *
+ * Geekdroid is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Geekdroid is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Geekdroid.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.geekorum.geekdroid.firebase
+
+import kotlinx.coroutines.tasks.await
+import com.google.android.gms.tasks.Task
+
+/**
+ * Await for the result of a [Task]
+ */
+@Deprecated("Moved to gms package", ReplaceWith("await()", "com.geekorum.geekdroid.gms.await"))
+suspend fun <T> Task<T>.await(): T = await()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/geekdroid-firebase/src/main/java/com/geekorum/geekdroid/firebase/logging/CrashlyticsLoggingTree.kt	Fri May 08 21:33:19 2020 -0400
@@ -0,0 +1,69 @@
+/**
+ * Geekdroid is a utility library for development on the Android
+ * Platform.
+ *
+ * Copyright (C) 2017-2020 by Frederic-Charles Barthelery.
+ *
+ * This file is part of Geekdroid.
+ *
+ * Geekdroid is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Geekdroid is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Geekdroid.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.geekorum.geekdroid.firebase.logging
+
+import android.util.Log
+import com.crashlytics.android.Crashlytics
+import com.google.firebase.crashlytics.FirebaseCrashlytics
+import timber.log.Timber
+
+/**
+ * A [Timber.Tree] to log message in Firebase Crashlytics using Fabric.
+ */
+@Deprecated("Use FirebaseCrashlyticsLoggingTree",
+    ReplaceWith("FirebaseCrashlyticsLoggingTree(FirebaseCrashlytics.getInstance())", "com.google.firebase.crashlytics.FirebaseCrashlytics"))
+class CrashlyticsLoggingTree : Timber.Tree() {
+    override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
+        Crashlytics.log(priority, tag, message)
+        if (priority >= Log.ERROR) {
+            t?.let {
+                Crashlytics.logException(it)
+            }
+        }
+    }
+}
+
+/**
+ * A [Timber.Tree] to log message in Firebase Crashlytics.
+ */
+class FirebaseCrashlyticsLoggingTree(
+    private val crashlytics: FirebaseCrashlytics
+) : Timber.Tree() {
+    override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
+        crashlytics.log("${priorityLetter(priority)}/$tag: $message")
+        if (priority >= Log.ERROR) {
+            t?.let {
+                crashlytics.recordException(t)
+            }
+        }
+    }
+
+    private fun priorityLetter(priority: Int) = when (priority) {
+        Log.ERROR -> 'E'
+        Log.WARN -> 'W'
+        Log.DEBUG -> 'D'
+        Log.ASSERT -> 'A'
+        Log.INFO -> 'I'
+        Log.VERBOSE -> 'V'
+        else -> 'V'
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/geekdroid-firebase/src/main/java/com/geekorum/geekdroid/gms/LocationServices.kt	Fri May 08 21:33:19 2020 -0400
@@ -0,0 +1,50 @@
+/**
+ * Geekdroid is a utility library for development on the Android
+ * Platform.
+ *
+ * Copyright (C) 2017-2020 by Frederic-Charles Barthelery.
+ *
+ * This file is part of Geekdroid.
+ *
+ * Geekdroid is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Geekdroid is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Geekdroid.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.geekorum.geekdroid.gms
+
+import android.location.Location
+import androidx.annotation.RequiresPermission
+import com.google.android.gms.location.FusedLocationProviderClient
+import com.google.android.gms.location.LocationCallback
+import com.google.android.gms.location.LocationRequest
+import com.google.android.gms.location.LocationResult
+import kotlinx.coroutines.suspendCancellableCoroutine
+import kotlin.coroutines.resume
+import kotlin.coroutines.resumeWithException
+
+@RequiresPermission(anyOf = ["android.permission.ACCESS_COARSE_LOCATION", "android.permission.ACCESS_FINE_LOCATION"])
+suspend fun FusedLocationProviderClient.requestLocation(locationRequest: LocationRequest): Location {
+    return suspendCancellableCoroutine { cont ->
+        val locationCallback = object : LocationCallback() {
+            override fun onLocationResult(result: LocationResult) {
+                removeLocationUpdates(this)
+                cont.resume(result.lastLocation)
+            }
+        }
+        cont.invokeOnCancellation {
+            removeLocationUpdates(locationCallback)
+        }
+        requestLocationUpdates(locationRequest, locationCallback, null).addOnFailureListener {
+            cont.resumeWithException(it)
+        }
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/geekdroid-firebase/src/main/java/com/geekorum/geekdroid/gms/Tasks.kt	Fri May 08 21:33:19 2020 -0400
@@ -0,0 +1,141 @@
+/**
+ * Geekdroid is a utility library for development on the Android
+ * Platform.
+ *
+ * Copyright (C) 2017-2020 by Frederic-Charles Barthelery.
+ *
+ * This file is part of Geekdroid.
+ *
+ * Geekdroid is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Geekdroid is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Geekdroid.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.geekorum.geekdroid.gms
+
+import com.google.android.gms.tasks.Task
+import kotlinx.coroutines.CancellationException
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.Deferred
+import kotlinx.coroutines.suspendCancellableCoroutine
+import kotlin.coroutines.resume
+import kotlin.coroutines.resumeWithException
+import com.google.android.play.core.tasks.Task as PlayCoreTask
+
+/**
+ * Await for the result of a [Task]
+ */
+@Deprecated("Use kotlinx-coroutines-play-services")
+suspend fun <T> Task<T>.await(): T {
+    try {
+        if (isComplete) return result as T
+    } catch (e: RuntimeException) {
+        return suspendCancellableCoroutine {
+            it.resumeWithException(e.cause ?: e)
+        }
+    }
+
+    return suspendCancellableCoroutine { cont ->
+        addOnCompleteListener { task ->
+            if (task.isSuccessful) {
+                cont.resume(task.result!!)
+            } else {
+                cont.resumeWithException(task.exception!!)
+            }
+        }
+    }
+}
+
+/**
+ * Await for the nullable result of a [Task]
+ * As [Task] is a Java API without proper nullability annotations,
+ * use this method if you know the task can returns null
+ */
+@Deprecated("Use kotlinx-coroutines-play-services")
+suspend fun <T> Task<T>.awaitNullable(): T? {
+    try {
+        if (isComplete) return result
+    } catch (e: RuntimeException) {
+        return suspendCancellableCoroutine {
+            it.resumeWithException(e.cause ?: e)
+        }
+    }
+
+    return suspendCancellableCoroutine { cont ->
+        addOnCompleteListener { task ->
+            if (task.isSuccessful) {
+                cont.resume(task.result)
+            } else {
+                cont.resumeWithException(task.exception!!)
+            }
+        }
+    }
+}
+
+
+/**
+ * Converts this task to an instance of [Deferred].
+ */
+fun <T> PlayCoreTask<T>.asDeferred(): Deferred<T> {
+    if (isComplete) {
+        val e = exception
+        return if (e == null) {
+            @Suppress("UNCHECKED_CAST")
+            CompletableDeferred<T>().apply { complete(result as T) }
+        } else {
+            CompletableDeferred<T>().apply { completeExceptionally(e) }
+        }
+    }
+
+    val result = CompletableDeferred<T>()
+    addOnCompleteListener {
+        val e = it.exception
+        if (e == null) {
+            @Suppress("UNCHECKED_CAST")
+            result.complete(it.result as T)
+        } else {
+            result.completeExceptionally(e)
+        }
+    }
+    return result
+}
+
+/**
+ * Awaits for completion of the task without blocking a thread.
+ *
+ * If the [Job] of the current coroutine is cancelled or completed while this suspending function is waiting, this function
+ * stops waiting for the completion stage and immediately resumes with [CancellationException].
+ */
+suspend fun <T> PlayCoreTask<T>.await(): T {
+    // fast path
+    if (isComplete) {
+        val e = exception
+        return if (e == null) {
+            @Suppress("UNCHECKED_CAST")
+            result as T
+        } else {
+            throw e
+        }
+    }
+
+    return suspendCancellableCoroutine { cont ->
+        addOnCompleteListener {
+            val e = exception
+            if (e == null) {
+                @Suppress("UNCHECKED_CAST")
+                cont.resume(result as T)
+            } else {
+                cont.resumeWithException(e)
+            }
+        }
+    }
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/geekdroid-firebase/src/test/java/com/geekorum/geekdroid/firebase/ExampleUnitTest.java	Fri May 08 21:33:19 2020 -0400
@@ -0,0 +1,38 @@
+/**
+ * Geekdroid is a utility library for development on the Android
+ * Platform.
+ *
+ * Copyright (C) 2017-2020 by Frederic-Charles Barthelery.
+ *
+ * This file is part of Geekdroid.
+ *
+ * Geekdroid is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Geekdroid is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Geekdroid.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.geekorum.geekdroid.firebase;
+
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
+ */
+public class ExampleUnitTest {
+    @Test
+    public void addition_isCorrect() {
+        assertEquals(4, 2 + 2);
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/geekdroid/.gitignore	Fri May 08 21:33:19 2020 -0400
@@ -0,0 +1,1 @@
+/build
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/geekdroid/build.gradle.kts	Fri May 08 21:33:19 2020 -0400
@@ -0,0 +1,87 @@
+import com.geekorum.build.configureJavaVersion
+import com.geekorum.build.enforcedDaggerPlatform
+
+plugins {
+    id("com.android.library")
+    kotlin("android")
+    kotlin("kapt")
+    id("com.geekorum.build.android-tests")
+    id("com.geekorum.build.android-avdl")
+}
+
+val archivesBaseName by extra("geekdroid")
+val artifactId by extra (archivesBaseName)
+
+android {
+    val compileSdkVersion: String by rootProject.extra
+    compileSdkVersion(compileSdkVersion)
+
+    defaultConfig {
+        minSdkVersion(24)
+        targetSdkVersion(29)
+    }
+    configureJavaVersion()
+
+    buildTypes {
+        getByName("release") {
+            isMinifyEnabled = false
+            proguardFiles(getDefaultProguardFile("proguard-android.txt"),
+                    "proguard-rules.pro")
+        }
+    }
+
+    lintOptions {
+        isAbortOnError = false
+    }
+
+    dataBinding {
+        isEnabled = true
+    }
+
+}
+
+dependencies {
+    implementation("androidx.recyclerview:recyclerview:1.1.0")
+    implementation("androidx.appcompat:appcompat:1.1.0")
+    implementation("com.google.android.material:material:1.1.0")
+    implementation("androidx.constraintlayout:constraintlayout:1.1.3")
+    implementation("androidx.coordinatorlayout:coordinatorlayout:1.1.0")
+    implementation("androidx.annotation:annotation:1.1.0")
+    implementation("androidx.preference:preference:1.1.0")
+    implementation("androidx.core:core-ktx:1.2.0")
+    implementation("androidx.fragment:fragment-ktx:1.2.2")
+
+    implementation("com.squareup.picasso:picasso:2.5.2")
+    implementation("com.squareup.okhttp3:okhttp:4.1.0")
+
+    val daggerVersion = "2.27"
+    implementation(enforcedDaggerPlatform(daggerVersion))
+    kapt(enforcedDaggerPlatform(daggerVersion))
+    implementation("com.google.dagger:dagger:$daggerVersion")
+    kapt("com.google.dagger:dagger-compiler:$daggerVersion")
+    compileOnly("com.squareup.inject:assisted-inject-annotations-dagger2:0.5.2")
+    kapt("com.squareup.inject:assisted-inject-processor-dagger2:0.5.2")
+
+    implementation(enforcedPlatform(kotlin("bom")))
+    implementation(kotlin("stdlib-jdk8"))
+
+    implementation(enforcedPlatform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.3.5"))
+    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core")
+
+    implementation("androidx.lifecycle:lifecycle-livedata-core-ktx:2.2.0")
+    implementation("androidx.lifecycle:lifecycle-viewmodel-savedstate:2.2.0")
+    testImplementation("androidx.arch.core:core-testing:2.1.0")
+
+    implementation("androidx.room:room-runtime:2.2.4")
+    implementation("androidx.browser:browser:1.2.0")
+    implementation("androidx.work:work-runtime:2.3.3")
+    implementation("androidx.navigation:navigation-common-ktx:2.2.1")
+    implementation("androidx.navigation:navigation-fragment:2.2.1")
+}
+
+
+apply {
+    // from("$projectDir/../config/android-checkstyle.gradle")
+    from("$projectDir/../config/source-archive.gradle")
+    from("$projectDir/../config/android-maven-publication.gradle")
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/geekdroid/proguard-rules.pro	Fri May 08 21:33:19 2020 -0400
@@ -0,0 +1,17 @@
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in /opt/android-sdk-update-manager/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the proguardFiles
+# directive in build.gradle.
+#
+# For more details, see
+#   http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+# 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 *;
+#}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/geekdroid/src/androidTest/java/com/geekorum/geekdroid/ExampleInstrumentedTest.java	Fri May 08 21:33:19 2020 -0400
@@ -0,0 +1,47 @@
+/**
+ * Geekdroid is a utility library for development on the Android
+ * Platform.
+ *
+ * Copyright (C) 2017-2020 by Frederic-Charles Barthelery.
+ *
+ * This file is part of Geekdroid.
+ *
+ * Geekdroid is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Geekdroid is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Geekdroid.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.geekorum.geekdroid;
+
+import android.content.Context;
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.*;
+
+/**
+ * Instrumentation test, which will execute on an Android device.
+ *
+ * @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
+ */
+@RunWith(AndroidJUnit4.class)
+public class ExampleInstrumentedTest {
+    @Test
+    public void useAppContext() throws Exception {
+        // Context of the app under test.
+        Context appContext = InstrumentationRegistry.getTargetContext();
+
+        assertEquals("com.geekorum.geekdroid.test", appContext.getPackageName());
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/geekdroid/src/main/AndroidManifest.xml	Fri May 08 21:33:19 2020 -0400
@@ -0,0 +1,6 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.geekorum.geekdroid">
+
+    <application />
+
+</manifest>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/geekdroid/src/main/java/com/geekorum/geekdroid/RoomExt.kt	Fri May 08 21:33:19 2020 -0400
@@ -0,0 +1,51 @@
+/**
+ * Geekdroid is a utility library for development on the Android
+ * Platform.
+ *
+ * Copyright (C) 2017-2020 by Frederic-Charles Barthelery.
+ *
+ * This file is part of Geekdroid.
+ *
+ * Geekdroid is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Geekdroid is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Geekdroid.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.geekorum.geekdroid
+
+import androidx.room.TypeConverter
+
+/**
+ * Extensions for the Room architecture component libraries
+ */
+
+
+/**
+ * Generic TypeConverters for Room.
+ */
+class RoomConverters {
+
+    @TypeConverter
+    fun fromPlainString(data: String?): List<String> {
+        if (data == null) {
+            return emptyList()
+        }
+        return data.split(", ")
+    }
+
+    @TypeConverter
+    fun listToPlainString(data: List<String>?): String {
+        if (data == null) {
+            return ""
+        }
+        return data.joinToString(", ")
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/geekdroid/src/main/java/com/geekorum/geekdroid/accounts/AccountAuthenticatorAppCompatActivity.kt	Fri May 08 21:33:19 2020 -0400
@@ -0,0 +1,54 @@
+/**
+ * Geekdroid is a utility library for development on the Android
+ * Platform.
+ *
+ * Copyright (C) 2017-2020 by Frederic-Charles Barthelery.
+ *
+ * This file is part of Geekdroid.
+ *
+ * Geekdroid is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Geekdroid is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Geekdroid.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.geekorum.geekdroid.accounts
+
+import android.accounts.AccountAuthenticatorActivity
+import android.accounts.AccountAuthenticatorResponse
+import android.accounts.AccountManager
+import android.os.Bundle
+import androidx.appcompat.app.AppCompatActivity
+
+/**
+ * An [AccountAuthenticatorActivity] that supports and AppCompat theme
+ */
+open class AccountAuthenticatorAppCompatActivity : AppCompatActivity() {
+
+    private var accountAuthenticatorResponse: AccountAuthenticatorResponse? = null
+    var accountAuthenticatorResult: Bundle? = null
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        accountAuthenticatorResponse = intent.getParcelableExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE)
+        accountAuthenticatorResponse?.onRequestContinued()
+    }
+
+    override fun finish() {
+        accountAuthenticatorResponse?.apply {
+            if (accountAuthenticatorResult != null) {
+                onResult(accountAuthenticatorResult)
+            } else {
+                onError(AccountManager.ERROR_CODE_CANCELED, "cancelled")
+            }
+        }
+        super.finish()
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/geekdroid/src/main/java/com/geekorum/geekdroid/accounts/AccountSelector.kt	Fri May 08 21:33:19 2020 -0400
@@ -0,0 +1,85 @@
+/**
+ * Geekdroid is a utility library for development on the Android
+ * Platform.
+ *
+ * Copyright (C) 2017-2020 by Frederic-Charles Barthelery.
+ *
+ * This file is part of Geekdroid.
+ *
+ * Geekdroid is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Geekdroid is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Geekdroid.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.geekorum.geekdroid.accounts
+
+import android.accounts.Account
+import android.accounts.AccountManager
+import android.annotation.SuppressLint
+import android.app.Application
+import android.content.SharedPreferences
+import android.preference.PreferenceManager
+
+import javax.inject.Inject
+
+/**
+ * Helper class to select an user account and save it in order to use it again later.
+ */
+class AccountSelector internal constructor(
+        private val preferences: SharedPreferences,
+        private val accountManager: AccountManager) {
+
+    val savedAccount: Account?
+        get() {
+            val accountName = preferences.getString(PREF_ACCOUNT_NAME, "")!!
+            val accountType = preferences.getString(PREF_ACCOUNT_TYPE, "")!!
+            return if (isExistingAccount(accountName, accountType)) {
+                Account(accountName, accountType)
+            } else null
+        }
+
+    @Inject
+    constructor(application: Application, accountManager: AccountManager)
+            : this(PreferenceManager.getDefaultSharedPreferences(application), accountManager)
+
+
+    fun saveAccount(account: Account) {
+        preferences.edit()
+                .putString(PREF_ACCOUNT_NAME, account.name)
+                .putString(PREF_ACCOUNT_TYPE, account.type)
+                .apply()
+    }
+
+    @SuppressLint("MissingPermission")
+    fun isExistingAccount(accountName: String, accountType: String): Boolean {
+        // we don't need that permission to access how own authenticator
+        val accounts = accountManager.getAccountsByType(accountType)
+        for (account in accounts) {
+            if (accountName == account.name) {
+                return true
+            }
+        }
+        return false
+    }
+
+    fun isExistingAccount(account: Account?): Boolean {
+        account?.let {
+            return isExistingAccount(account.name, account.type)
+        }
+        return false
+    }
+
+    companion object {
+        private const val PREF_ACCOUNT_NAME = "account_selector_saved_account_name"
+        private const val PREF_ACCOUNT_TYPE = "account_selector_saved_account_type"
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/geekdroid/src/main/java/com/geekorum/geekdroid/accounts/AccountTokenRetriever.kt	Fri May 08 21:33:19 2020 -0400
@@ -0,0 +1,80 @@
+/**
+ * Geekdroid is a utility library for development on the Android
+ * Platform.
+ *
+ * Copyright (C) 2017-2020 by Frederic-Charles Barthelery.
+ *
+ * This file is part of Geekdroid.
+ *
+ * Geekdroid is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Geekdroid is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Geekdroid.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.geekorum.geekdroid.accounts
+
+import android.accounts.Account
+import android.accounts.AccountManager
+import android.os.Bundle
+import androidx.annotation.RequiresPermission
+import com.geekorum.geekdroid.network.TokenRetriever
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.suspendCancellableCoroutine
+import kotlinx.coroutines.withContext
+import kotlin.coroutines.resume
+import kotlin.coroutines.resumeWithException
+
+/**
+ * Allows to manipulate tokens from the Android AccountManager.
+ */
+open class AccountTokenRetriever(
+    private val accountManager: AccountManager, private val tokenType: String,
+    private val account: Account, private val notifyAuthFailure: Boolean
+) : TokenRetriever {
+    private var lastToken: String? = null
+
+    @RequiresPermission(value = "android.permission.USE_CREDENTIALS", conditional = true)
+    @Throws(TokenRetriever.RetrieverException::class)
+    override suspend fun getToken(): String {
+        val result = try {
+            accountManager.getAuthToken(account, tokenType, null, notifyAuthFailure)
+        } catch (e: Exception) {
+            throw TokenRetriever.RetrieverException("Unable to retrieve token", e)
+        }
+        return withContext(Dispatchers.Main) {
+            lastToken = result
+            lastToken ?: throw TokenRetriever.RetrieverException("Unable to retrieve token")
+        }
+    }
+
+    @RequiresPermission(
+        anyOf = ["android.permission.USE_CREDENTIALS", "android.permission.MANAGE_ACCOUNTS"],
+        conditional = true)
+    override suspend fun invalidateToken() = withContext(Dispatchers.Main) {
+        accountManager.invalidateAuthToken(account.type, lastToken)
+    }
+
+}
+
+suspend fun AccountManager.getAuthToken(
+    account: Account, tokenType: String, options: Bundle?, notifyAuthFailure: Boolean
+): String? {
+    return suspendCancellableCoroutine { cont ->
+        getAuthToken(account, tokenType, options, notifyAuthFailure, {
+            try {
+                val bundle = it.result
+                cont.resume(bundle[AccountManager.KEY_AUTHTOKEN] as String?)
+            } catch (e: Exception) {
+                cont.resumeWithException(e)
+            }
+        }, null)
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/geekdroid/src/main/java/com/geekorum/geekdroid/accounts/AccountsListViewModel.kt	Fri May 08 21:33:19 2020 -0400
@@ -0,0 +1,52 @@
+/**
+ * Geekdroid is a utility library for development on the Android
+ * Platform.
+ *
+ * Copyright (C) 2017-2020 by Frederic-Charles Barthelery.
+ *
+ * This file is part of Geekdroid.
+ *
+ * Geekdroid is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Geekdroid is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Geekdroid.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.geekorum.geekdroid.accounts
+
+import android.accounts.Account
+import android.accounts.AccountManager
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+
+/**
+ * ViewModel to select and use an account of a specified type.
+ */
+abstract class AccountsListViewModel(protected val accountManager: AccountManager,
+                                     protected val accountSelector: AccountSelector,
+                                     vararg accountTypes: String
+) : ViewModel() {
+
+    private val mutableSelectedAccount = MutableLiveData<Account>()
+    val selectedAccount: LiveData<Account> = mutableSelectedAccount
+
+    val accounts = AccountsLiveData(accountManager, *accountTypes)
+
+    init {
+        mutableSelectedAccount.setValue(accountSelector.savedAccount)
+    }
+
+    fun selectAccount(account: Account) {
+        accountSelector.saveAccount(account)
+        mutableSelectedAccount.setValue(account)
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/geekdroid/src/main/java/com/geekorum/geekdroid/accounts/AccountsLiveData.kt	Fri May 08 21:33:19 2020 -0400
@@ -0,0 +1,66 @@
+/**
+ * Geekdroid is a utility library for development on the Android
+ * Platform.
+ *
+ * Copyright (C) 2017-2020 by Frederic-Charles Barthelery.
+ *
+ * This file is part of Geekdroid.
+ *
+ * Geekdroid is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Geekdroid is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Geekdroid.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.geekorum.geekdroid.accounts
+
+import android.accounts.Account
+import android.accounts.AccountManager
+import android.accounts.OnAccountsUpdateListener
+import android.annotation.SuppressLint
+import android.os.Build
+import androidx.lifecycle.LiveData
+
+/**
+ * Allows to observe the list of [Account] for a specified Account type.
+ */
+class AccountsLiveData(
+        private val accountManager: AccountManager,
+        vararg accountType: String?
+) : LiveData<Array<Account>>() {
+
+    private val accountTypes: List<String> = accountType.asList().requireNoNulls()
+
+    private val accountsListener = OnAccountsUpdateListener {
+        val accounts = retainAccounts(it)
+        if (value?.contentEquals(accounts) != true){
+            value = accounts
+        }
+    }
+
+    @SuppressLint("MissingPermission")
+    override fun onActive() {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+            accountManager.addOnAccountsUpdatedListener(accountsListener, null, true,
+                    accountTypes.toTypedArray())
+        } else {
+            accountManager.addOnAccountsUpdatedListener(accountsListener, null, true)
+        }
+    }
+
+    override fun onInactive() {
+        accountManager.removeOnAccountsUpdatedListener(accountsListener)
+    }
+
+    private fun retainAccounts(accounts: Array<Account>): Array<Account> {
+        return accounts.filter { it.type in accountTypes }.toTypedArray()
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/geekdroid/src/main/java/com/geekorum/geekdroid/accounts/CancellableSyncAdapter.kt	Fri May 08 21:33:19 2020 -0400
@@ -0,0 +1,94 @@
+/**
+ * Geekdroid is a utility library for development on the Android
+ * Platform.
+ *
+ * Copyright (C) 2017-2020 by Frederic-Charles Barthelery.
+ *
+ * This file is part of Geekdroid.
+ *
+ * Geekdroid is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Geekdroid is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Geekdroid.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.geekorum.geekdroid.accounts
+
+import android.accounts.Account
+import android.content.AbstractThreadedSyncAdapter
+import android.content.ContentProviderClient
+import android.content.Context
+import android.content.SyncResult
+import android.os.Bundle
+import androidx.annotation.CallSuper
+import androidx.annotation.WorkerThread
+import kotlinx.coroutines.CancellationException
+import kotlinx.coroutines.CoroutineName
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.runBlocking
+
+/**
+ * An [AbstractThreadedSyncAdapter] that support cancellation of syncs using coroutines
+ */
+abstract class CancellableSyncAdapter(
+    context: Context,
+    autoInitialize: Boolean = true,
+    allowParallelSyncs: Boolean = false
+) : AbstractThreadedSyncAdapter(context, autoInitialize, allowParallelSyncs) {
+
+    private val syncs: MutableMap<Thread, Pair<CancellableSync, CoroutineScope>> = mutableMapOf()
+
+    override fun onPerformSync(
+        account: Account, extras: Bundle, authority: String, provider: ContentProviderClient, syncResult: SyncResult
+    ) {
+        try {
+            runBlocking(CoroutineName(Thread.currentThread().name)) {
+                val cancellableSync = createCancellableSync(account, extras, authority, provider, syncResult)
+                syncs[Thread.currentThread()] = (cancellableSync to this)
+                cancellableSync.sync()
+            }
+        } catch (e: CancellationException) {
+            // it is expected
+        }
+    }
+
+    override fun onSyncCanceled() {
+        for ((t, _) in syncs) {
+            onSyncCanceled(t)
+        }
+        syncs.clear()
+    }
+
+    override fun onSyncCanceled(thread: Thread) {
+        syncs.remove(thread)?.let {
+            it.first.onSyncCancelled()
+            it.second.cancel()
+        }
+    }
+
+    abstract fun createCancellableSync(
+        account: Account, extras: Bundle, authority: String,
+        provider: ContentProviderClient, syncResult: SyncResult
+    ): CancellableSync
+
+
+    /** Interface that should be implemented to be allow to cancel a sync */
+    abstract class CancellableSync {
+
+        @CallSuper
+        open fun onSyncCancelled() {}
+
+        @WorkerThread
+        abstract suspend fun sync()
+
+    }
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/geekdroid/src/main/java/com/geekorum/geekdroid/accounts/SyncInProgressLiveData.kt	Fri May 08 21:33:19 2020 -0400
@@ -0,0 +1,64 @@
+/**
+ * Geekdroid is a utility library for development on the Android
+ * Platform.
+ *
+ * Copyright (C) 2017-2020 by Frederic-Charles Barthelery.
+ *
+ * This file is part of Geekdroid.
+ *
+ * Geekdroid is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Geekdroid is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Geekdroid.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.geekorum.geekdroid.accounts
+
+import android.Manifest
+import android.accounts.Account
+import android.annotation.SuppressLint
+import android.content.ContentResolver
+import android.content.ContentResolver.SYNC_OBSERVER_TYPE_ACTIVE
+import androidx.annotation.RequiresPermission
+import androidx.lifecycle.LiveData
+
+/**
+ * Allows to observe if a Sync operation is active for a given account.
+ *
+ * @see ContentResolver.isSyncActive
+ */
+class SyncInProgressLiveData
+@RequiresPermission(Manifest.permission.READ_SYNC_STATS)
+constructor(
+        private val account: Account,
+        private val authority: String
+) : LiveData<Boolean?>() {
+
+    private var statusObserverHandle: Any? = null
+
+    override fun onActive() {
+        statusObserverHandle = ContentResolver.addStatusChangeListener(SYNC_OBSERVER_TYPE_ACTIVE) { which ->
+            if (which == SYNC_OBSERVER_TYPE_ACTIVE) {
+                updateValue()
+            }
+        }
+        updateValue()
+    }
+
+    override fun onInactive() {
+        ContentResolver.removeStatusChangeListener(statusObserverHandle)
+    }
+
+    @SuppressLint("MissingPermission")
+    private fun updateValue() {
+        val isSyncing = ContentResolver.isSyncActive(account, authority)
+        postValue(isSyncing)
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/geekdroid/src/main/java/com/geekorum/geekdroid/app/AppCompatPreferenceActivity.java	Fri May 08 21:33:19 2020 -0400
@@ -0,0 +1,133 @@
+/**
+ * Geekdroid is a utility library for development on the Android
+ * Platform.
+ *
+ * Copyright (C) 2017-2020 by Frederic-Charles Barthelery.
+ *
+ * This file is part of Geekdroid.
+ *
+ * Geekdroid is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Geekdroid is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Geekdroid.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.geekorum.geekdroid.app;
+
+import android.content.res.Configuration;
+import android.os.Bundle;
+import android.preference.PreferenceActivity;
+import androidx.annotation.LayoutRes;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.ActionBar;
+import androidx.appcompat.app.AppCompatDelegate;
+import androidx.appcompat.widget.Toolbar;
+import android.view.MenuInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * A {@link android.preference.PreferenceActivity} which implements and proxies the necessary calls
+ * to be used with AppCompat.
+ */
+public abstract class AppCompatPreferenceActivity extends PreferenceActivity {
+
+    private AppCompatDelegate mDelegate;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        getDelegate().installViewFactory();
+        getDelegate().onCreate(savedInstanceState);
+        super.onCreate(savedInstanceState);
+    }
+
+    @Override
+    protected void onPostCreate(Bundle savedInstanceState) {
+        super.onPostCreate(savedInstanceState);
+        getDelegate().onPostCreate(savedInstanceState);
+    }
+
+    public ActionBar getSupportActionBar() {
+        return getDelegate().getSupportActionBar();
+    }
+
+    public void setSupportActionBar(@Nullable Toolbar toolbar) {
+        getDelegate().setSupportActionBar(toolbar);
+    }
+
+    @NotNull
+    @Override
+    public MenuInflater getMenuInflater() {
+        return getDelegate().getMenuInflater();
+    }
+
+    @Override
+    public void setContentView(@LayoutRes int layoutResID) {
+        getDelegate().setContentView(layoutResID);
+    }
+
+    @Override
+    public void setContentView(View view) {
+        getDelegate().setContentView(view);
+    }
+
+    @Override
+    public void setContentView(View view, ViewGroup.LayoutParams params) {
+        getDelegate().setContentView(view, params);
+    }
+
+    @Override
+    public void addContentView(View view, ViewGroup.LayoutParams params) {
+        getDelegate().addContentView(view, params);
+    }
+
+    @Override
+    protected void onPostResume() {
+        super.onPostResume();
+        getDelegate().onPostResume();
+    }
+
+    @Override
+    protected void onTitleChanged(CharSequence title, int color) {
+        super.onTitleChanged(title, color);
+        getDelegate().setTitle(title);
+    }
+
+    @Override
+    public void onConfigurationChanged(Configuration newConfig) {
+        super.onConfigurationChanged(newConfig);
+        getDelegate().onConfigurationChanged(newConfig);
+    }
+
+    @Override
+    protected void onStop() {
+        super.onStop();
+        getDelegate().onStop();
+    }
+
+    @Override
+    protected void onDestroy() {
+        super.onDestroy();
+        getDelegate().onDestroy();
+    }
+
+    public void invalidateOptionsMenu() {
+        getDelegate().invalidateOptionsMenu();
+    }
+
+    private AppCompatDelegate getDelegate() {
+        if (mDelegate == null) {
+            mDelegate = AppCompatDelegate.create(this, null);
+        }
+        return mDelegate;
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/geekdroid/src/main/java/com/geekorum/geekdroid/app/BottomSheetDialogActivity.kt	Fri May 08 21:33:19 2020 -0400
@@ -0,0 +1,181 @@
+/**
+ * Geekdroid is a utility library for development on the Android
+ * Platform.
+ *
+ * Copyright (C) 2017-2020 by Frederic-Charles Barthelery.
+ *
+ * This file is part of Geekdroid.
+ *
+ * Geekdroid is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Geekdroid is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Geekdroid.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.geekorum.geekdroid.app
+
+import android.annotation.SuppressLint
+import android.os.Build
+import android.os.Bundle
+import androidx.core.view.AccessibilityDelegateCompat
+import androidx.core.view.ViewCompat
+import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
+import android.view.View
+import android.view.ViewGroup
+import android.view.WindowManager
+import android.widget.FrameLayout
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.content.res.use
+import com.geekorum.geekdroid.databinding.ActivityBottomSheetDialogBinding
+import com.google.android.material.bottomsheet.BottomSheetBehavior
+
+/**
+ * React like a [android.support.design.widget.BottomSheetDialogFragment] but is a separate [Activity].
+ * This allows you to launch the bottom sheet easily from another external activity.
+ */
+abstract class BottomSheetDialogActivity : AppCompatActivity() {
+
+    private lateinit var binding: ActivityBottomSheetDialogBinding
+    private lateinit var behavior: BottomSheetBehavior<FrameLayout>
+    private val callbackDelegator = object : BottomSheetBehavior.BottomSheetCallback() {
+        override fun onSlide(bottomSheet: View, slideOffset: Float) {
+            bottomSheetCallback?.onSlide(bottomSheet, slideOffset)
+        }
+
+        override fun onStateChanged(bottomSheet: View, newState: Int) {
+            if (newState == BottomSheetBehavior.STATE_HIDDEN) {
+                finish()
+            }
+            bottomSheetCallback?.onStateChanged(bottomSheet, newState)
+        }
+    }
+
+    var bottomSheetCallback: BottomSheetBehavior.BottomSheetCallback? = null
+
+    var cancelable = true
+        set(value) {
+            field = value
+            behavior.isHideable = value
+        }
+
+    private var cancelOnTouchOutside = true
+        set(value) {
+            canceledOnTouchOutsideSet = true
+            field = value
+        }
+    private var canceledOnTouchOutsideSet = false
+
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        if (theme == null) {
+            // TODO force a the default theme or crash if incorrect theme applied ?
+            // setup our default theme
+//            setTheme(R.style.Theme_Geekdroid_Light_BottomSheetDialogActivity)
+        }
+        super.onCreate(savedInstanceState)
+        initializeBottomSheet()
+
+        window.decorView // force initialize the window
+
+        // only set if the window is non floating, but we have a floating window
+        // so we need to set it manually
+        window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
+
+        window.setLayout(ViewGroup.LayoutParams.MATCH_PARENT,
+            ViewGroup.LayoutParams.MATCH_PARENT)
+
+    }
+
+    override fun setContentView(layoutResID: Int) {
+        super.setContentView(wrapInBottomSheet(layoutResID, null, null))
+    }
+
+    override fun setContentView(view: View?) {
+        super.setContentView(wrapInBottomSheet(0, view, null))
+    }
+
+    override fun setContentView(view: View?, params: ViewGroup.LayoutParams?) {
+        super.setContentView(wrapInBottomSheet(0, view, params))
+    }
+
+    @SuppressLint("ClickableViewAccessibility")
+    private fun initializeBottomSheet() {
+        binding = ActivityBottomSheetDialogBinding.inflate(layoutInflater, null, false)
+        behavior = BottomSheetBehavior.from(binding.bottomSheet)
+        behavior.setBottomSheetCallback(callbackDelegator)
+        behavior.isHideable = cancelable
+        // We treat the CoordinatorLayout as outside the dialog though it is technically inside
+        binding.touchOutside.setOnClickListener {
+            if (cancelable && cancelOnTouchOutside) {
+                finish()
+            }
+        }
+
+        // Handle accessibility events
+        ViewCompat.setAccessibilityDelegate(binding.bottomSheet, object : AccessibilityDelegateCompat() {
+            override fun onInitializeAccessibilityNodeInfo(
+                host: View,
+                info: AccessibilityNodeInfoCompat
+            ) {
+                super.onInitializeAccessibilityNodeInfo(host, info)
+                if (cancelable) {
+                    info.addAction(AccessibilityNodeInfoCompat.ACTION_DISMISS)
+                    info.isDismissable = true
+                } else {
+                    info.isDismissable = false
+                }
+            }
+
+            override fun performAccessibilityAction(host: View, action: Int, args: Bundle): Boolean {
+                if (action == AccessibilityNodeInfoCompat.ACTION_DISMISS && cancelable) {
+                    finish()
+                    return true
+                }
+                return super.performAccessibilityAction(host, action, args)
+            }
+        })
+
+        binding.bottomSheet.setOnTouchListener { _, _ ->
+            // Consume the event and prevent it from falling through
+            true
+        }
+    }
+
+
+    private fun wrapInBottomSheet(layoutResId: Int, resourceView: View?, params: ViewGroup.LayoutParams?): View {
+        var view = resourceView
+        if (layoutResId != 0 && view == null) {
+            view = layoutInflater.inflate(layoutResId, binding.coordinator, false)
+        }
+        if (params == null) {
+            binding.bottomSheet.addView(view)
+        } else {
+            binding.bottomSheet.addView(view, params)
+        }
+        return binding.root
+    }
+
+    fun shouldFinishOnTouchOutside(): Boolean {
+        if (!canceledOnTouchOutsideSet) {
+            val a = obtainStyledAttributes(
+                intArrayOf(android.R.attr.windowCloseOnTouchOutside)).use {
+                cancelOnTouchOutside = it.getBoolean(0, true)
+            }
+            canceledOnTouchOutsideSet = true
+        }
+        return cancelOnTouchOutside
+    }
+
+    override fun setFinishOnTouchOutside(finish: Boolean) {
+        super.setFinishOnTouchOutside(finish)
+        cancelOnTouchOutside = finish
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/geekdroid/src/main/java/com/geekorum/geekdroid/app/lifecycle/EventObserver.kt	Fri May 08 21:33:19 2020 -0400
@@ -0,0 +1,67 @@
+/**
+ * Geekdroid is a utility library for development on the Android
+ * Platform.
+ *
+ * Copyright (C) 2017-2020 by Frederic-Charles Barthelery.
+ *
+ * This file is part of Geekdroid.
+ *
+ * Geekdroid is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Geekdroid is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Geekdroid.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.geekorum.geekdroid.app.lifecycle
+
+import androidx.lifecycle.Observer
+
+/**
+ * An Event of content T
+ */
+open class Event<out T>(
+    private val content: T) {
+
+    var hasBeenHandled = false
+        private set //external read only
+
+    fun getContentIfNotHandled(): T? {
+        return if (hasBeenHandled) {
+            null
+        } else {
+            hasBeenHandled = true
+            content
+        }
+    }
+
+    fun peekContent(): T = content
+}
+
+/**
+ * An [Event] with no content.
+ */
+class EmptyEvent : Event<Any>(Any()) {
+    companion object {
+        @JvmStatic
+        fun makeEmptyEvent() = EmptyEvent()
+    }
+}
+
+/**
+ * An event observer observe a LiveData<Event> an will be executed only once, when the event changed
+ */
+class EventObserver<T>(
+    private val onEventUnhandled: (T) -> Unit
+) : Observer<Event<T>> {
+
+    override fun onChanged(event: Event<T>?) {
+        event?.getContentIfNotHandled()?.let(onEventUnhandled)
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/geekdroid/src/main/java/com/geekorum/geekdroid/app/lifecycle/Transformations.kt	Fri May 08 21:33:19 2020 -0400
@@ -0,0 +1,92 @@
+/**
+ * Geekdroid is a utility library for development on the Android
+ * Platform.
+ *
+ * Copyright (C) 2017-2020 by Frederic-Charles Barthelery.
+ *
+ * This file is part of Geekdroid.
+ *
+ * Geekdroid is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Geekdroid is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Geekdroid.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.geekorum.geekdroid.app.lifecycle
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MediatorLiveData
+
+/**
+ *  Various LiveData transformations
+ */
+
+/**
+ * Join two [LiveData] collections into a List
+ */
+fun <T> LiveData<out Collection<T>>.join(other: LiveData<out Collection<T>>): LiveData<List<T>> {
+    return this.combine(other) {first, second -> first + second }
+}
+
+/**
+ * Union of two [LiveData] collections into a List
+ */
+fun <T> LiveData<out Collection<T>>.union(other: LiveData<out Collection<T>>): LiveData<List<T>> {
+    return this.combine(other) { first, second -> first.union(second).toList() }
+}
+
+private fun <T> LiveData<out Collection<T>>.combine(
+    other: LiveData<out Collection<T>>,
+    combinator: (first: Collection<T>, second: Collection<T>) -> List<T>
+): LiveData<List<T>> {
+    return MediatorLiveData<List<T>>().apply {
+        var first: Collection<T> = emptyList()
+        var second: Collection<T> = emptyList()
+
+        addSource(this@combine) {
+            first = it
+            value = combinator(first, second)
+        }
+        addSource(other) {
+            second = it
+            value = combinator(first, second)
+        }
+    }
+}
+
+/**
+ * Make this LiveData delivers new value only on new event
+ */
+fun <T> LiveData<T>.withRefreshEvent(event: LiveData<out Event<*>>): LiveData<T> = RefreshOnEventLiveData(this, event)
+
+
+/**
+ * A LiveData that delivers source new value only when event is received.
+ */
+private class RefreshOnEventLiveData<T>(
+    source: LiveData<T>,
+    event: LiveData<out Event<*>>
+) : MediatorLiveData<T>() {
+    private var hasDeliveredFirst = false
+    private var lastData: T? = null
+
+    init {
+        addSource(source) {
+            lastData = it
+            if (!hasDeliveredFirst) {
+                value = it
+                hasDeliveredFirst = true
+            }
+        }
+        addSource(event, EventObserver {
+            value = lastData
+        })
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/geekdroid/src/main/java/com/geekorum/geekdroid/arch/PagingRequestHelper.java	Fri May 08 21:33:19 2020 -0400
@@ -0,0 +1,513 @@
+/**
+ * Geekdroid is a utility library for development on the Android
+ * Platform.
+ *
+ * Copyright (C) 2017-2020 by Frederic-Charles Barthelery.
+ *
+ * This file is part of Geekdroid.
+ *
+ * Geekdroid is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Geekdroid is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Geekdroid.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.geekorum.geekdroid.arch;
+
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import androidx.annotation.AnyThread;
+import androidx.annotation.GuardedBy;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+
+import java.util.Arrays;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.Executor;
+import java.util.concurrent.atomic.AtomicBoolean;
+/**
+ * A helper class for {@link androidx.paging.PagedList.BoundaryCallback BoundaryCallback}s and
+ * {@link DataSource}s to help with tracking network requests.
+ * <p>
+ * It is designed to support 3 types of requests, {@link RequestType#INITIAL INITIAL},
+ * {@link RequestType#BEFORE BEFORE} and {@link RequestType#AFTER AFTER} and runs only 1 request
+ * for each of them via {@link #runIfNotRunning(RequestType, Request)}.
+ * <p>
+ * It tracks a {@link Status} and an {@code error} for each {@link RequestType}.
+ * <p>
+ * A sample usage of this class to limit requests looks like this:
+ * <pre>
+ * class PagingBoundaryCallback extends PagedList.BoundaryCallback&lt;MyItem> {
+ *     // TODO replace with an executor from your application
+ *     Executor executor = Executors.newSingleThreadExecutor();
+ *     PagingRequestHelper helper = new PagingRequestHelper(executor);
+ *     // imaginary API service, using Retrofit
+ *     MyApi api;
+ *
+ *     {@literal @}Override
+ *     public void onItemAtFrontLoaded({@literal @}NonNull MyItem itemAtFront) {
+ *         helper.runIfNotRunning(PagingRequestHelper.RequestType.BEFORE,
+ *                 helperCallback -> api.getTopBefore(itemAtFront.getName(), 10).enqueue(
+ *                         new Callback&lt;ApiResponse>() {
+ *                             {@literal @}Override
+ *                             public void onResponse(Call&lt;ApiResponse> call,
+ *                                     Response&lt;ApiResponse> response) {
+ *                                 // TODO insert new records into database
+ *                                 helperCallback.recordSuccess();
+ *                             }
+ *
+ *                             {@literal @}Override
+ *                             public void onFailure(Call&lt;ApiResponse> call, Throwable t) {
+ *                                 helperCallback.recordFailure(t);
+ *                             }
+ *                         }));
+ *     }
+ *
+ *     {@literal @}Override
+ *     public void onItemAtEndLoaded({@literal @}NonNull MyItem itemAtEnd) {
+ *         helper.runIfNotRunning(PagingRequestHelper.RequestType.AFTER,
+ *                 helperCallback -> api.getTopBefore(itemAtEnd.getName(), 10).enqueue(
+ *                         new Callback&lt;ApiResponse>() {
+ *                             {@literal @}Override
+ *                             public void onResponse(Call&lt;ApiResponse> call,
+ *                                     Response&lt;ApiResponse> response) {
+ *                                 // TODO insert new records into database
+ *                                 helperCallback.recordSuccess();
+ *                             }
+ *
+ *                             {@literal @}Override
+ *                             public void onFailure(Call&lt;ApiResponse> call, Throwable t) {
+ *                                 helperCallback.recordFailure(t);
+ *                             }
+ *                         }));
+ *     }
+ * }
+ * </pre>
+ * <p>
+ * The helper provides an API to observe combined request status, which can be reported back to the
+ * application based on your business rules.
+ * <pre>
+ * MutableLiveData&lt;PagingRequestHelper.Status> combined = new MutableLiveData&lt;>();
+ * helper.addListener(status -> {
+ *     // merge multiple states per request type into one, or dispatch separately depending on
+ *     // your application logic.
+ *     if (status.hasRunning()) {
+ *         combined.postValue(PagingRequestHelper.Status.RUNNING);
+ *     } else if (status.hasError()) {
+ *         // can also obtain the error via {@link StatusReport#getErrorFor(RequestType)}
+ *         combined.postValue(PagingRequestHelper.Status.FAILED);
+ *     } else {
+ *         combined.postValue(PagingRequestHelper.Status.SUCCESS);
+ *     }
+ * });
+ * </pre>
+ */
+// THIS class is likely to be moved into the library in a future release. Feel free to copy it
+// from this sample.
+public class PagingRequestHelper {
+    private final Object mLock = new Object();
+    private final Executor mRetryService;
+    @GuardedBy("mLock")
+    private final RequestQueue[] mRequestQueues = new RequestQueue[]
+            {new RequestQueue(RequestType.INITIAL),
+                    new RequestQueue(RequestType.BEFORE),
+                    new RequestQueue(RequestType.AFTER)};
+    @NonNull
+    final CopyOnWriteArrayList<Listener> mListeners = new CopyOnWriteArrayList<>();
+    /**
+     * Creates a new PagingRequestHelper with the given {@link Executor} which is used to run
+     * retry actions.
+     *
+     * @param retryService The {@link Executor} that can run the retry actions.
+     */
+    public PagingRequestHelper(@NonNull Executor retryService) {
+        mRetryService = retryService;
+    }
+    /**
+     * Adds a new listener that will be notified when any request changes {@link Status state}.
+     *
+     * @param listener The listener that will be notified each time a request's status changes.
+     * @return True if it is added, false otherwise (e.g. it already exists in the list).
+     */
+    @AnyThread
+    public boolean addListener(@NonNull Listener listener) {
+        return mListeners.add(listener);
+    }
+    /**
+     * Removes the given listener from the listeners list.
+     *
+     * @param listener The listener that will be removed.
+     * @return True if the listener is removed, false otherwise (e.g. it never existed)
+     */
+    public boolean removeListener(@NonNull Listener listener) {
+        return mListeners.remove(listener);
+    }
+    /**
+     * Runs the given {@link Request} if no other requests in the given request type is already
+     * running.
+     * <p>
+     * If run, the request will be run in the current thread.
+     *
+     * @param type    The type of the request.
+     * @param request The request to run.
+     * @return True if the request is run, false otherwise.
+     */
+    @SuppressWarnings("WeakerAccess")
+    @AnyThread
+    public boolean runIfNotRunning(@NonNull RequestType type, @NonNull Request request) {
+        boolean hasListeners = !mListeners.isEmpty();
+        StatusReport report = null;
+        synchronized (mLock) {
+            RequestQueue queue = mRequestQueues[type.ordinal()];
+            if (queue.mRunning != null) {
+                return false;
+            }
+            queue.mRunning = request;
+            queue.mStatus = Status.RUNNING;
+            queue.mFailed = null;
+            queue.mLastError = null;
+            if (hasListeners) {
+                report = prepareStatusReportLocked();
+            }
+        }
+        if (report != null) {
+            dispatchReport(report);
+        }
+        final RequestWrapper wrapper = new RequestWrapper(request, this, type);
+        wrapper.run();
+        return true;
+    }
+    @GuardedBy("mLock")
+    private StatusReport prepareStatusReportLocked() {
+        Throwable[] errors = new Throwable[]{
+                mRequestQueues[0].mLastError,
+                mRequestQueues[1].mLastError,
+                mRequestQueues[2].mLastError
+        };
+        return new StatusReport(
+                getStatusForLocked(RequestType.INITIAL),
+                getStatusForLocked(RequestType.BEFORE),
+                getStatusForLocked(RequestType.AFTER),
+                errors
+        );
+    }
+    @GuardedBy("mLock")
+    private Status getStatusForLocked(RequestType type) {
+        return mRequestQueues[type.ordinal()].mStatus;
+    }
+    @AnyThread
+    @VisibleForTesting
+    void recordResult(@NonNull RequestWrapper wrapper, @Nullable Throwable throwable) {
+        StatusReport report = null;
+        final boolean success = throwable == null;
+        boolean hasListeners = !mListeners.isEmpty();
+        synchronized (mLock) {
+            RequestQueue queue = mRequestQueues[wrapper.mType.ordinal()];
+            queue.mRunning = null;
+            queue.mLastError = throwable;
+            if (success) {
+                queue.mFailed = null;
+                queue.mStatus = Status.SUCCESS;
+            } else {
+                queue.mFailed = wrapper;
+                queue.mStatus = Status.FAILED;
+            }
+            if (hasListeners) {
+                report = prepareStatusReportLocked();
+            }
+        }
+        if (report != null) {
+            dispatchReport(report);
+        }
+    }
+    private void dispatchReport(StatusReport report) {
+        for (Listener listener : mListeners) {
+            listener.onStatusChange(report);
+        }
+    }
+    /**
+     * Retries all failed requests.
+     *
+     * @return True if any request is retried, false otherwise.
+     */
+    public boolean retryAllFailed() {
+        final RequestWrapper[] toBeRetried = new RequestWrapper[RequestType.values().length];
+        boolean retried = false;
+        synchronized (mLock) {
+            for (int i = 0; i < RequestType.values().length; i++) {
+                toBeRetried[i] = mRequestQueues[i].mFailed;
+                mRequestQueues[i].mFailed = null;
+            }
+        }
+        for (RequestWrapper failed : toBeRetried) {
+            if (failed != null) {
+                failed.retry(mRetryService);
+                retried = true;
+            }
+        }
+        return retried;
+    }
+    static class RequestWrapper implements Runnable {
+        @NonNull
+        final Request mRequest;
+        @NonNull
+        final PagingRequestHelper mHelper;
+        @NonNull
+        final RequestType mType;
+        RequestWrapper(@NonNull Request request, @NonNull PagingRequestHelper helper,
+                       @NonNull RequestType type) {
+            mRequest = request;
+            mHelper = helper;
+            mType = type;
+        }
+        @Override
+        public void run() {
+            mRequest.run(new Request.Callback(this, mHelper));
+        }
+        void retry(Executor service) {
+            service.execute(new Runnable() {
+                @Override
+                public void run() {
+                    mHelper.runIfNotRunning(mType, mRequest);
+                }
+            });
+        }
+    }
+    /**
+     * Runner class that runs a request tracked by the {@link PagingRequestHelper}.
+     * <p>
+     * When a request is invoked, it must call one of {@link Callback#recordFailure(Throwable)}
+     * or {@link Callback#recordSuccess()} once and only once. This call
+     * can be made any time. Until that method call is made, {@link PagingRequestHelper} will
+     * consider the request is running.
+     */
+    @FunctionalInterface
+    public interface Request {
+        /**
+         * Should run the request and call the given {@link Callback} with the result of the
+         * request.
+         *
+         * @param callback The callback that should be invoked with the result.
+         */
+        void run(Callback callback);
+        /**
+         * Callback class provided to the {@link #run(Callback)} method to report the result.
+         */
+        class Callback {
+            private final AtomicBoolean mCalled = new AtomicBoolean();
+            private final RequestWrapper mWrapper;
+            private final PagingRequestHelper mHelper;
+            Callback(RequestWrapper wrapper, PagingRequestHelper helper) {
+                mWrapper = wrapper;
+                mHelper = helper;
+            }
+            /**
+             * Call this method when the request succeeds and new data is fetched.
+             */
+            @SuppressWarnings("unused")
+            public final void recordSuccess() {
+                if (mCalled.compareAndSet(false, true)) {
+                    mHelper.recordResult(mWrapper, null);
+                } else {
+                    throw new IllegalStateException(
+                            "already called recordSuccess or recordFailure");
+                }
+            }
+            /**
+             * Call this method with the failure message and the request can be retried via
+             * {@link #retryAllFailed()}.
+             *
+             * @param throwable The error that occured while carrying out the request.
+             */
+            @SuppressWarnings("unused")
+            public final void recordFailure(@NonNull Throwable throwable) {
+                //noinspection ConstantConditions
+                if (throwable == null) {
+                    throw new IllegalArgumentException("You must provide a throwable describing"
+                            + " the error to record the failure");
+                }
+                if (mCalled.compareAndSet(false, true)) {
+                    mHelper.recordResult(mWrapper, throwable);
+                } else {
+                    throw new IllegalStateException(
+                            "already called recordSuccess or recordFailure");
+                }
+            }
+        }
+    }
+    /**
+     * Data class that holds the information about the current status of the ongoing requests
+     * using this helper.
+     */
+    public static final class StatusReport {
+        /**
+         * Status of the latest request that were submitted with {@link RequestType#INITIAL}.
+         */
+        @NonNull
+        public final Status initial;
+        /**
+         * Status of the latest request that were submitted with {@link RequestType#BEFORE}.
+         */
+        @NonNull
+        public final Status before;
+        /**
+         * Status of the latest request that were submitted with {@link RequestType#AFTER}.
+         */
+        @NonNull
+        public final Status after;
+        @NonNull
+        private final Throwable[] mErrors;
+        StatusReport(@NonNull Status initial, @NonNull Status before, @NonNull Status after,
+                     @NonNull Throwable[] errors) {
+            this.initial = initial;
+            this.before = before;
+            this.after = after;
+            this.mErrors = errors;
+        }
+        /**
+         * Convenience method to check if there are any running requests.
+         *
+         * @return True if there are any running requests, false otherwise.
+         */
+        public boolean hasRunning() {
+            return initial == Status.RUNNING
+                    || before == Status.RUNNING
+                    || after == Status.RUNNING;
+        }
+        /**
+         * Convenience method to check if there are any requests that resulted in an error.
+         *
+         * @return True if there are any requests that finished with error, false otherwise.
+         */
+        public boolean hasError() {
+            return initial == Status.FAILED
+                    || before == Status.FAILED
+                    || after == Status.FAILED;
+        }
+        /**
+         * Returns the error for the given request type.
+         *
+         * @param type The request type for which the error should be returned.
+         * @return The {@link Throwable} returned by the failing request with the given type or
+         * {@code null} if the request for the given type did not fail.
+         */
+        @Nullable
+        public Throwable getErrorFor(@NonNull RequestType type) {
+            return mErrors[type.ordinal()];
+        }
+        @Override
+        public String toString() {
+            return "StatusReport{"
+                    + "initial=" + initial
+                    + ", before=" + before
+                    + ", after=" + after
+                    + ", mErrors=" + Arrays.toString(mErrors)
+                    + '}';
+        }
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) return true;
+            if (o == null || getClass() != o.getClass()) return false;
+            StatusReport that = (StatusReport) o;
+            if (initial != that.initial) return false;
+            if (before != that.before) return false;
+            if (after != that.after) return false;
+            // Probably incorrect - comparing Object[] arrays with Arrays.equals
+            return Arrays.equals(mErrors, that.mErrors);
+        }
+        @Override
+        public int hashCode() {
+            int result = initial.hashCode();
+            result = 31 * result + before.hashCode();
+            result = 31 * result + after.hashCode();
+            result = 31 * result + Arrays.hashCode(mErrors);
+            return result;
+        }
+    }
+    /**
+     * Listener interface to get notified by request status changes.
+     */
+    public interface Listener {
+        /**
+         * Called when the status for any of the requests has changed.
+         *
+         * @param report The current status report that has all the information about the requests.
+         */
+        void onStatusChange(@NonNull StatusReport report);
+    }
+    /**
+     * Represents the status of a Request for each {@link RequestType}.
+     */
+    public enum Status {
+        /**
+         * There is current a running request.
+         */
+        RUNNING,
+        /**
+         * The last request has succeeded or no such requests have ever been run.
+         */
+        SUCCESS,
+        /**
+         * The last request has failed.
+         */
+        FAILED
+    }
+    /**
+     * Available request types.
+     */
+    public enum RequestType {
+        /**
+         * Corresponds to an initial request made to a {@link DataSource} or the empty state for
+         * a {@link androidx.paging.PagedList.BoundaryCallback BoundaryCallback}.
+         */
+        INITIAL,
+        /**
+         * Corresponds to the {@code loadBefore} calls in {@link DataSource} or
+         * {@code onItemAtFrontLoaded} in
+         * {@link androidx.paging.PagedList.BoundaryCallback BoundaryCallback}.
+         */
+        BEFORE,
+        /**
+         * Corresponds to the {@code loadAfter} calls in {@link DataSource} or
+         * {@code onItemAtEndLoaded} in
+         * {@link androidx.paging.PagedList.BoundaryCallback BoundaryCallback}.
+         */
+        AFTER
+    }
+    class RequestQueue {
+        @NonNull
+        final RequestType mRequestType;
+        @Nullable
+        RequestWrapper mFailed;
+        @Nullable
+        Request mRunning;
+        @Nullable
+        Throwable mLastError;
+        @NonNull
+        Status mStatus = Status.SUCCESS;
+        RequestQueue(@NonNull RequestType requestType) {
+            mRequestType = requestType;
+        }
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/geekdroid/src/main/java/com/geekorum/geekdroid/battery/LiveData.kt	Fri May 08 21:33:19 2020 -0400
@@ -0,0 +1,98 @@
+/**
+ * Geekdroid is a utility library for development on the Android
+ * Platform.
+ *
+ * Copyright (C) 2017-2020 by Frederic-Charles Barthelery.
+ *
+ * This file is part of Geekdroid.
+ *
+ * Geekdroid is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Geekdroid is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Geekdroid.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.geekorum.geekdroid.battery
+
+import android.app.Application
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.os.BatteryManager
+import android.os.Build
+import android.os.PowerManager
+import androidx.lifecycle.LiveData
+import kotlin.math.roundToInt
+
+/**
+ * Observe the battery to know if a low level was reached
+ */
+class LowBatteryLiveData(
+    private val application: Application
+) : LiveData<Boolean>() {
+    private val intentFilter = IntentFilter().apply {
+        addAction(Intent.ACTION_BATTERY_LOW)
+        addAction(Intent.ACTION_BATTERY_OKAY)
+    }
+
+    private val broadcastReceiver = object : BroadcastReceiver() {
+        override fun onReceive(context: Context?, intent: Intent?) {
+            when (intent?.action) {
+                Intent.ACTION_BATTERY_OKAY -> value = false
+                Intent.ACTION_BATTERY_LOW -> value = true
+            }
+        }
+    }
+
+    override fun onActive() {
+        application.registerReceiver(broadcastReceiver, intentFilter)
+        val batteryStatus = application.registerReceiver(null, IntentFilter(Intent.ACTION_BATTERY_CHANGED))
+        value  = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+            batteryStatus?.getBooleanExtra(BatteryManager.EXTRA_BATTERY_LOW, false) ?: false
+        } else {
+            val level = batteryStatus?.getIntExtra(BatteryManager.EXTRA_LEVEL, 0) ?: 0
+            val scale = batteryStatus?.getIntExtra(BatteryManager.EXTRA_SCALE, 100) ?: 0
+            val percent = level.toFloat() / scale * 100
+            percent.roundToInt() <= 15
+        }
+    }
+
+    override fun onInactive() {
+        application.unregisterReceiver(broadcastReceiver)
+    }
+}
+
+
+/**
+ * Observe the [PowerManager] to know if the system is in Power saving mode.
+ */
+class BatterySaverLiveData(
+    private val application: Application,
+    private val powerManager: PowerManager
+) : LiveData<Boolean>() {
+
+    private val intentFilter = IntentFilter(PowerManager.ACTION_POWER_SAVE_MODE_CHANGED)
+
+    private val broadcastReceiver = object : BroadcastReceiver() {
+        override fun onReceive(context: Context?, intent: Intent?) {
+            value = powerManager.isPowerSaveMode
+        }
+    }
+
+    override fun onActive() {
+        application.registerReceiver(broadcastReceiver, intentFilter)
+        value = powerManager.isPowerSaveMode
+    }
+
+    override fun onInactive() {
+        application.unregisterReceiver(broadcastReceiver)
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/geekdroid/src/main/java/com/geekorum/geekdroid/bindings/BindingAdapters.kt	Fri May 08 21:33:19 2020 -0400
@@ -0,0 +1,134 @@
+/**
+ * Geekdroid is a utility library for development on the Android
+ * Platform.
+ *
+ * Copyright (C) 2017-2020 by Frederic-Charles Barthelery.
+ *
+ * This file is part of Geekdroid.
+ *
+ * Geekdroid is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Geekdroid is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Geekdroid.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.geekorum.geekdroid.bindings
+
+import android.graphics.ColorMatrix
+import android.graphics.ColorMatrixColorFilter
+import android.graphics.Rect
+import android.graphics.Typeface
+import android.view.TouchDelegate
+import android.view.View
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.databinding.BindingAdapter
+import androidx.databinding.adapters.ListenerUtil
+import androidx.viewpager.widget.ViewPager
+import androidx.viewpager.widget.ViewPager.OnPageChangeListener
+import com.geekorum.geekdroid.R
+import com.geekorum.geekdroid.views.CheckableImageView
+import com.squareup.picasso.Picasso
+
+/**
+ * Various adapters for the Android Data Binding library
+ */
+
+@BindingAdapter("imageUrl")
+fun setImageUrl(imageView: ImageView, url: String) {
+    val requestCreator = Picasso.with(imageView.context)
+            .load(url)
+    when (imageView.scaleType) {
+        ImageView.ScaleType.CENTER_CROP -> requestCreator.centerCrop()
+        ImageView.ScaleType.FIT_CENTER -> requestCreator.fit().centerInside()
+        ImageView.ScaleType.FIT_XY -> requestCreator.fit()
+        else -> Unit // do nothing for the others
+    }
+    requestCreator.into(imageView)
+}
+
+@BindingAdapter("srcResource")
+fun setImageResources(imageView: ImageView, resourceId: Int) {
+    imageView.setImageResource(resourceId)
+}
+
+@BindingAdapter("grayscale")
+fun setImageGrayscale(imageView: ImageView, greyscale: Boolean) {
+    if (greyscale) {
+        val colorMatrix = ColorMatrix()
+        colorMatrix.setSaturation(0f)
+        imageView.colorFilter = ColorMatrixColorFilter(colorMatrix)
+    } else {
+        imageView.clearColorFilter()
+    }
+}
+
+@BindingAdapter("backgroundResource")
+fun setBackgroundResource(view: View, resourceId: Int) {
+    view.setBackgroundResource(resourceId)
+}
+
+@BindingAdapter("textStyle")
+fun setTextStyle(view: TextView, style: Int) {
+    view.setTypeface(Typeface.create(view.typeface, style), style)
+}
+
+@BindingAdapter("touchHeight", "touchWidth")
+fun setTouchArea(view: View, touchHeight: Float, touchWidth: Float) {
+    val parent = view.parent
+    if (parent is View) {
+        val parentView = parent as View
+        parentView.post {
+            val delegateArea = Rect()
+            view.getHitRect(delegateArea)
+            val deltaLeftRight = (touchWidth - delegateArea.width()) / 2
+            delegateArea.right += deltaLeftRight.toInt()
+            delegateArea.left -= deltaLeftRight.toInt()
+            val deltaTopBottom = (touchHeight - delegateArea.height()) / 2
+            delegateArea.bottom += deltaTopBottom.toInt()
+            delegateArea.top -= deltaTopBottom.toInt()
+            val delegate = TouchDelegate(delegateArea, view)
+            parentView.touchDelegate = delegate
+        }
+    }
+}
+
+@BindingAdapter("onCheckedChanged")
+fun setListener(checkableImageView: CheckableImageView,
+                newListener: CheckableImageView.OnCheckedChangeListener?) {
+    checkableImageView.setOnCheckedChangeListener(newListener)
+}
+
+@BindingAdapter("onPageSelected")
+fun setListener(viewPager: ViewPager, listener: OnPageSelectedListener?) {
+    var newListener: OnPageChangeListener? = null
+    if (listener != null) {
+        newListener = object : OnPageChangeListener {
+            override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {}
+            override fun onPageSelected(position: Int) {
+                listener.onPageSelected(position)
+            }
+
+            override fun onPageScrollStateChanged(state: Int) {}
+        }
+    }
+    val oldListener = ListenerUtil.trackListener(viewPager,
+            newListener, R.id.onPageSelectedListener)
+    if (oldListener != null) {
+        viewPager.removeOnPageChangeListener(oldListener)
+    }
+    if (newListener != null) {
+        viewPager.addOnPageChangeListener(newListener)
+    }
+}
+
+interface OnPageSelectedListener {
+    fun onPageSelected(position: Int)
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/geekdroid/src/main/java/com/geekorum/geekdroid/bindings/Converters.java	Fri May 08 21:33:19 2020 -0400
@@ -0,0 +1,29 @@
+/**
+ * Geekdroid is a utility library for development on the Android
+ * Platform.
+ *
+ * Copyright (C) 2017-2020 by Frederic-Charles Barthelery.
+ *
+ * This file is part of Geekdroid.
+ *
+ * Geekdroid is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Geekdroid is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Geekdroid.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.geekorum.geekdroid.bindings;
+
+/**
+ * Some custom Converters for Android Data Binding Library
+ */
+public class Converters {
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/geekdroid/src/main/java/com/geekorum/geekdroid/dagger/AndroidFrameworkModule.kt	Fri May 08 21:33:19 2020 -0400
@@ -0,0 +1,90 @@
+/**
+ * Geekdroid is a utility library for development on the Android
+ * Platform.
+ *
+ * Copyright (C) 2017-2020 by Frederic-Charles Barthelery.
+ *
+ * This file is part of Geekdroid.
+ *
+ * Geekdroid is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Geekdroid is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Geekdroid.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.geekorum.geekdroid.dagger
+
+import android.accounts.AccountManager
+import android.app.Application
+import android.app.NotificationManager
+import android.content.ContentResolver
+import android.content.Context
+import android.content.pm.PackageManager
+import android.net.ConnectivityManager
+import android.os.PowerManager
+import androidx.core.content.getSystemService
+import dagger.Module
+import dagger.Provides
+import javax.inject.Scope
+import javax.inject.Singleton
+
+/**
+ * Provides binding for some Android Framework services tied to the application context.
+ */
+@Module
+class AndroidFrameworkModule {
+
+    @Provides
+    @Singleton
+    fun providesNotificationManager(application: Application): NotificationManager {
+        return application.getSystemService()!!
+    }
+
+    @Provides
+    @Singleton
+    fun providesConnectivityManager(application: Application): ConnectivityManager {
+        return application.getSystemService()!!
+    }
+
+    @Provides
+    @Singleton
+    fun providesPackageManager(application: Application): PackageManager {
+        return application.packageManager
+    }
+
+    @Provides
+    @Singleton
+    fun providesAccountManager(application: Application): AccountManager {
+        return AccountManager.get(application)
+    }
+
+    @Provides
+    @Singleton
+    fun providesContentResolver(application: Application): ContentResolver {
+        return application.contentResolver
+    }
+
+    @Provides
+    @Singleton
+    fun providesPowerManager(application: Application): PowerManager {
+        return application.getSystemService()!!
+    }
+
+}
+
+
+/**
+ * Scope for object which should be instantiated once per Android component (Activity, Service,
+ * BroadcastReceiver, ContentProvider)
+ */
+@Retention(AnnotationRetention.RUNTIME)
+@MustBeDocumented
+@Scope
+annotation class PerAndroidComponent
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/geekdroid/src/main/java/com/geekorum/geekdroid/dagger/AndroidxCoreModule.kt	Fri May 08 21:33:19 2020 -0400
@@ -0,0 +1,41 @@
+/**
+ * Geekdroid is a utility library for development on the Android
+ * Platform.
+ *
+ * Copyright (C) 2017-2020 by Frederic-Charles Barthelery.
+ *
+ * This file is part of Geekdroid.
+ *
+ * Geekdroid is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Geekdroid is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Geekdroid.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.geekorum.geekdroid.dagger
+
+import android.app.Application
+import androidx.core.app.NotificationManagerCompat
+import dagger.Module
+import dagger.Provides
+import javax.inject.Singleton
+
+/**
+ * Provides binding for some Androidx Core services.
+ */
+@Module
+class AndroidxCoreModule {
+
+    @Provides
+    @Singleton
+    fun providesNotificationManagerCompat(application: Application): NotificationManagerCompat {
+        return NotificationManagerCompat.from(application)
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/geekdroid/src/main/java/com/geekorum/geekdroid/dagger/AppInitializers.kt	Fri May 08 21:33:19 2020 -0400
@@ -0,0 +1,56 @@
+/**
+ * Geekdroid is a utility library for development on the Android
+ * Platform.
+ *
+ * Copyright (C) 2017-2020 by Frederic-Charles Barthelery.
+ *
+ * This file is part of Geekdroid.
+ *
+ * Geekdroid is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Geekdroid is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Geekdroid.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.geekorum.geekdroid.dagger
+
+import android.app.Application
+import dagger.Module
+import dagger.multibindings.Multibinds
+
+/**
+ * Add this module to your component to support dependency injection of [AppInitializer].
+ *
+ * You can provide [AppInitializer] by adding some bindings in your module.
+```
+@Binds
+@IntoSet
+abstract fun bindMyAppInitializer(myAppInitializer: MyAppInitializer): AppInitializer
+```
+ */
+@Module
+abstract class AppInitializersModule private constructor() {
+
+    @Multibinds
+    abstract fun appInitializers(): Set<AppInitializer>
+
+}
+
+
+/**
+ * AppInitializers are meant to be run in [Application.onCreate]
+ */
+interface AppInitializer {
+    fun initialize(app: Application)
+}
+
+fun Collection<AppInitializer>.initialize(app: Application) {
+    forEach { it.initialize(app) }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/geekdroid/src/main/java/com/geekorum/geekdroid/dagger/AssistedInjection.kt	Fri May 08 21:33:19 2020 -0400
@@ -0,0 +1,33 @@
+/**
+ * Geekdroid is a utility library for development on the Android
+ * Platform.
+ *
+ * Copyright (C) 2017-2020 by Frederic-Charles Barthelery.
+ *
+ * This file is part of Geekdroid.
+ *
+ * Geekdroid is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Geekdroid is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Geekdroid.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.geekorum.geekdroid.dagger
+
+import com.squareup.inject.assisted.dagger2.AssistedModule
+import dagger.Module
+
+/**
+ * Allows to use Assisted injection
+ * Generates binding for Assisted injection factories
+ */
+@AssistedModule
+@Module(includes = [AssistedInject_GeekdroidAssistedModule::class])
+internal abstract class GeekdroidAssistedModule
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/geekdroid/src/main/java/com/geekorum/geekdroid/dagger/FragmentFactory.kt	Fri May 08 21:33:19 2020 -0400
@@ -0,0 +1,77 @@
+/**
+ * Geekdroid is a utility library for development on the Android
+ * Platform.
+ *
+ * Copyright (C) 2017-2020 by Frederic-Charles Barthelery.
+ *
+ * This file is part of Geekdroid.
+ *
+ * Geekdroid is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Geekdroid is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Geekdroid.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.geekorum.geekdroid.dagger
+
+import androidx.fragment.app.Fragment
+import dagger.MapKey
+import dagger.Module
+import dagger.multibindings.Multibinds
+import javax.inject.Inject
+import javax.inject.Provider
+import kotlin.reflect.KClass
+
+/**
+ * MultiBinding key for the [DaggerDelegateFragmentFactory]
+ */
+@MapKey
+@MustBeDocumented
+@Target(AnnotationTarget.FUNCTION)
+@Retention(AnnotationRetention.RUNTIME)
+annotation class FragmentKey(
+    val value: KClass<out Fragment>
+)
+
+/**
+ * Add this module to your component to be able to obtain a [DaggerDelegateFragmentFactory] that can create
+ * Fragment using dagger dependency injection.
+ *
+ * You can provide [Fragment] by adding some bindings in your module.
+ * The [DaggerDelegateFragmentFactory] will use the declared bindings.
+```
+@Binds
+@IntoMap
+@FragmentKey(MyFragment::class)
+abstract fun bindMyFragment(fragment: MyFragment): Fragment
+```
+ */
+@Module
+abstract class FragmentFactoriesModule private constructor() {
+
+    @Multibinds
+    abstract fun fragmentFactories(): Map<Class<out Fragment>, Fragment>
+}
+
+/**
+ * Factory that can creates the [Fragment] needed by application after injecting them.
+ */
+class DaggerDelegateFragmentFactory @Inject constructor(
+    private val providers: Map<Class<out Fragment>, @JvmSuppressWildcards Provider<Fragment>>
+) : androidx.fragment.app.FragmentFactory() {
+
+
+    override fun instantiate(classLoader: ClassLoader, className: String): Fragment {
+        val key = loadFragmentClass(classLoader, className)
+        return providers[key]?.get() ?: super.instantiate(classLoader, className)
+    }
+
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/geekdroid/src/main/java/com/geekorum/geekdroid/dagger/ViewModels.kt	Fri May 08 21:33:19 2020 -0400
@@ -0,0 +1,151 @@
+/**
+ * Geekdroid is a utility library for development on the Android
+ * Platform.
+ *
+ * Copyright (C) 2017-2020 by Frederic-Charles Barthelery.
+ *
+ * This file is part of Geekdroid.
+ *
+ * Geekdroid is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Geekdroid is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Geekdroid.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.geekorum.geekdroid.dagger
+
+import android.os.Bundle
+import androidx.lifecycle.AbstractSavedStateViewModelFactory
+import androidx.lifecycle.SavedStateHandle
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
+import androidx.savedstate.SavedStateRegistryOwner
+import com.squareup.inject.assisted.Assisted
+import com.squareup.inject.assisted.AssistedInject
+import dagger.MapKey
+import dagger.Module
+import dagger.multibindings.Multibinds
+import javax.inject.Inject
+import javax.inject.Provider
+import kotlin.annotation.AnnotationTarget.FUNCTION
+import kotlin.reflect.KClass
+
+
+/**
+ * MultiBinding key for the [DaggerDelegateViewModelsFactory]
+ */
+@MapKey
+@MustBeDocumented
+@Target(FUNCTION)
+@Retention(AnnotationRetention.RUNTIME)
+annotation class ViewModelKey(
+    val value: KClass<out ViewModel>
+)
+
+/**
+ * Add this module to your component to be able to obtain a [DaggerDelegateViewModelsFactory] or a [DaggerDelegateSavedStateVMFactory]
+ * that can create [ViewModel]s using dagger dependency injection.
+ *
+ * You can provide [ViewModel]s by binding a [ViewModel] or a [ViewModelAssistedFactory] with a [ViewModelKey].
+ *
+ * ```
+ *  @Binds
+ *  @IntoMap
+ *  @ViewModelKey(MyViewModel::class)
+ *  abstract fun bindMyViewModel(viewModel: MyViewModel): ViewModel
+ *
+ *  @Binds
+ *  @IntoMap
+ *  @ViewModelKey(MySavedStateViewModel::class)
+ *  abstract fun bindMySavedStateViewModel(viewModel: MySavedStateViewModel.Factory): ViewModelAssistedFactory<out ViewModel>
+ * ```
+ * @see [DaggerDelegateSavedStateVMFactory]
+ */
+@Module(includes = [GeekdroidAssistedModule::class])
+abstract class ViewModelsModule private constructor() {
+
+    @Multibinds
+    abstract fun viewModelsFactories(): Map<Class<out ViewModel>, ViewModel>
+
+    @Multibinds
+    abstract fun savedStateViewModelsFactories(): Map<Class<out ViewModel>, ViewModelAssistedFactory<out ViewModel>>
+}
+
+
+/**
+ * Factory that can creates the [ViewModel] needed by application after injecting them.
+ */
+class DaggerDelegateViewModelsFactory @Inject constructor(
+    private val providers: Map<Class<out ViewModel>, @JvmSuppressWildcards Provider<ViewModel>>
+) : ViewModelProvider.Factory {
+
+    override fun <T : ViewModel> create(modelClass: Class<T>): T {
+        providers[modelClass]?.let {
+            @Suppress("UNCHECKED_CAST")
+            return it.get() as T
+        }
+        throw IllegalArgumentException("No ViewModel providers for class $modelClass")
+    }
+}
+
+/**
+ * Factory that can creates [ViewModel] who contribute to saved states via [SavedStateHandle]
+ * and support assisted injection
+ *
+ * ViewModels must have an annotated [AssistedInject.Factory] factory interface inheriting [ViewModelAssistedFactory]
+ * and an annotated [Assisted] state [SavedStateHandle] constructor parameter.
+ *
+ * ```
+ * class MyViewModel @AssistedInject constructor(@Assisted private val state: SavedStateHandle,
+ *                                               val otherDep: OtherDependency
+ * ) : ViewModel() {
+ *
+ *      @AssistedInject.Factory
+ *      interface Factory : ViewModelAssistedFactory<MyViewModel> {
+ *          override fun create(state: SavedStateHandle): MyViewModel
+ *      }
+ * }
+ * ```
+ *
+ * This class also support simple ViewModel injection like [DaggerDelegateViewModelsFactory]
+ * TODO: documentation is highly AssistedInject focused. Maybe it can be used with other assisted injection framework
+ */
+class DaggerDelegateSavedStateVMFactory @AssistedInject constructor(
+    private val simpleProviders: Map<Class<out ViewModel>, @JvmSuppressWildcards Provider<ViewModel>>,
+    private val savedStateFactoryProviders: Map<Class<out ViewModel>, @JvmSuppressWildcards Provider<ViewModelAssistedFactory<out ViewModel>>>,
+    @Assisted val owner: SavedStateRegistryOwner,
+    @Assisted val defaultArgs: Bundle?
+) : AbstractSavedStateViewModelFactory(owner, defaultArgs) {
+
+    override fun <T : ViewModel> create(key: String, modelClass: Class<T>, handle: SavedStateHandle): T {
+        // first try the SavedState version
+        savedStateFactoryProviders[modelClass]?.let {
+            @Suppress("UNCHECKED_CAST")
+            return it.get().create(handle) as T
+        }
+        simpleProviders[modelClass]?.let {
+            @Suppress("UNCHECKED_CAST")
+            return it.get() as T
+        }
+        throw IllegalArgumentException("No ViewModel providers for key $key and class $modelClass")
+    }
+
+    @AssistedInject.Factory
+    interface Creator {
+        fun create(owner: SavedStateRegistryOwner, defaultArgs: Bundle? = null): DaggerDelegateSavedStateVMFactory
+    }
+}
+
+/**
+ * A Factory that can create ViewModel T with a SavedStateHandle
+ */
+interface ViewModelAssistedFactory<T: ViewModel> {
+    fun create(state: SavedStateHandle) : T
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/geekdroid/src/main/java/com/geekorum/geekdroid/dagger/WorkerInjection.kt	Fri May 08 21:33:19 2020 -0400
@@ -0,0 +1,102 @@
+/**
+ * Geekdroid is a utility library for development on the Android
+ * Platform.
+ *
+ * Copyright (C) 2017-2020 by Frederic-Charles Barthelery.
+ *
+ * This file is part of Geekdroid.
+ *
+ * Geekdroid is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Geekdroid is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Geekdroid.  If not, see <http://www.gnu.org/licenses/>.
+ */
+@file:JvmName("DaggerWorkerInjection")
+
+package com.geekorum.geekdroid.dagger
+
+import android.content.Context
+import androidx.work.ListenableWorker
+import androidx.work.WorkerFactory
+import androidx.work.WorkerParameters
+import dagger.MapKey
+import dagger.Module
+import dagger.Provides
+import dagger.multibindings.ElementsIntoSet
+import dagger.multibindings.Multibinds
+import javax.inject.Inject
+import javax.inject.Provider
+import kotlin.annotation.AnnotationTarget.FUNCTION
+import kotlin.reflect.KClass
+
+
+@MapKey
+@Target(FUNCTION)
+annotation class WorkerKey(val value: KClass<out ListenableWorker>)
+
+/**
+ * Add this module to your component to support dependency injection of [ListenableWorker].
+ *
+ * You will get a multibinding Set<WorkerFactory> by adding some WorkerFactory in your module.
+```
+@Binds
+@IntoSet
+abstract fun bindMyWorkerFactory(workerFactory: MyWorkerFactory): WorkerFactory<out Worker>
+```
+ */
+@Module
+abstract class WorkerInjectionModule private constructor() {
+
+    @Multibinds
+    @Deprecated("use Set multibinding")
+    abstract fun workerFactoriesMap(): Map<Class<out ListenableWorker>, WorkerFactory>
+
+    @Multibinds
+    abstract fun workerFactories(): Set<WorkerFactory>
+
+    @Module
+    companion object {
+        @Provides
+        @ElementsIntoSet
+        @JvmStatic
+        fun workersFactoriesMapAsSet(workerFactoriesMap: Map<Class<out ListenableWorker>, @JvmSuppressWildcards WorkerFactory>) : Set<WorkerFactory> {
+            return workerFactoriesMap.values.toSet()
+        }
+    }
+
+}
+
+
+/**
+ * Factory that can creates the [ListenableWorker] needed by the application using Dagger injection
+ */
+// TODO deprecate and use DelegatingWorkerFactory from 2.1.0
+@Deprecated("Use DelegatingWorkerFactory instead")
+class DaggerDelegateWorkersFactory
+@Inject
+constructor(
+    private val providers: Map<Class<out ListenableWorker>, @JvmSuppressWildcards Provider<WorkerFactory>>
+) : WorkerFactory() {
+
+    private val providesByName by lazy {
+        providers.mapKeys { (k, _) ->
+            k.name
+        }
+    }
+
+    override fun createWorker(
+        appContext: Context, workerClassName: String, workerParameters: WorkerParameters
+    ): ListenableWorker? {
+        val factory = providesByName[workerClassName]?.get()
+        return factory?.createWorker(appContext, workerClassName, workerParameters)
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/geekdroid/src/main/java/com/geekorum/geekdroid/jobs/JobThread.java	Fri May 08 21:33:19 2020 -0400
@@ -0,0 +1,50 @@
+/**
+ * Geekdroid is a utility library for development on the Android
+ * Platform.
+ *
+ * Copyright (C) 2017-2020 by Frederic-Charles Barthelery.
+ *
+ * This file is part of Geekdroid.
+ *
+ * Geekdroid is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Geekdroid is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Geekdroid.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.geekorum.geekdroid.jobs;
+
+import android.app.job.JobParameters;
+import android.os.Process;
+import com.geekorum.geekdroid.utils.ProcessPriority;
+
+/**
+ * A thread that execute a job from a {@link ThreadedJobService}
+ * @Deprecated Use androidx.work
+ */
+@Deprecated
+public abstract class JobThread extends Thread {
+    private final ThreadedJobService jobService;
+    private final JobParameters parameters;
+
+    protected JobThread(ThreadedJobService jobService, JobParameters parameters) {
+        this.jobService = jobService;
+        this.parameters = parameters;
+    }
+
+    protected void setProcessPriority(@ProcessPriority int processPriority) {
+        Process.setThreadPriority(processPriority);
+    }
+
+    protected void completeJob(boolean needReschedule) {
+        jobService.completeJob(parameters, needReschedule);
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/geekdroid/src/main/java/com/geekorum/geekdroid/jobs/ThreadedJobService.java	Fri May 08 21:33:19 2020 -0400
@@ -0,0 +1,84 @@
+/**
+ * Geekdroid is a utility library for development on the Android
+ * Platform.
+ *
+ * Copyright (C) 2017-2020 by Frederic-Charles Barthelery.
+ *
+ * This file is part of Geekdroid.
+ *
+ * Geekdroid is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Geekdroid is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Geekdroid.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.geekorum.geekdroid.jobs;
+
+import android.app.job.JobParameters;
+import android.app.job.JobService;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+
+/**
+ * Execute each job of the {@link JobService} in a separate thread.
+ * @Deprecated Use androidx.work
+ */
+@Deprecated
+public abstract class ThreadedJobService extends JobService {
+    private Map<JobParameters, Future<?>> tasks = Collections.synchronizedMap(new HashMap<JobParameters, Future<?>>());
+    private ExecutorService executorService;
+
+    @Override
+    public void onCreate() {
+        super.onCreate();
+        executorService = Executors.newCachedThreadPool();
+    }
+
+    @Override
+    public void onDestroy() {
+        super.onDestroy();
+        executorService.shutdownNow();
+    }
+
+    @Override
+    public boolean onStartJob(JobParameters params) {
+        JobThread jobThread = createJobThread(params);
+        if (jobThread == null) {
+            return false;
+        }
+        Future<?> task = executorService.submit(jobThread);
+        tasks.put(params, task);
+        return true;
+    }
+
+    @Override
+    public boolean onStopJob(JobParameters params) {
+        Future<?> task = tasks.remove(params);
+        if (task != null) {
+            task.cancel(true);
+            return true;
+        }
+        return false;
+    }
+
+    void completeJob(JobParameters jobParameters, boolean needReschedule) {
+        Future<?> task = tasks.remove(jobParameters);
+        if (task != null) {
+            jobFinished(jobParameters, needReschedule);
+        }
+    }
+
+    protected abstract JobThread createJobThread(JobParameters jobParameters);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/geekdroid/src/main/java/com/geekorum/geekdroid/loaders/ObjectCursorLoader.java	Fri May 08 21:33:19 2020 -0400
@@ -0,0 +1,79 @@
+/**
+ * Geekdroid is a utility library for development on the Android
+ * Platform.
+ *
+ * Copyright (C) 2017-2020 by Frederic-Charles Barthelery.
+ *
+ * This file is part of Geekdroid.
+ *
+ * Geekdroid is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Geekdroid is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Geekdroid.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.geekorum.geekdroid.loaders;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import androidx.loader.content.CursorLoader;
+
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * A {@link CursorLoader} that can load data from a cursor and map it into an Object instance.
+ */
+public class ObjectCursorLoader<T> extends CursorLoader {
+
+    private final CursorMapper<T> cursorMapper;
+    private List<T> items = Collections.emptyList();
+
+    public ObjectCursorLoader(Context context, CursorMapper<T> cursorMapper) {
+        super(context);
+        this.cursorMapper = cursorMapper;
+    }
+
+    public ObjectCursorLoader(Context context, Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder, CursorMapper<T> cursorMapper) {
+        super(context, uri, projection, selection, selectionArgs, sortOrder);
+        this.cursorMapper = cursorMapper;
+    }
+
+    @Override
+    public Cursor loadInBackground() {
+        Cursor result;
+        result = super.loadInBackground();
+        loadItems(result);
+        return result;
+    }
+
+    private void loadItems(Cursor cursor) {
+        List<T> newItems = new LinkedList<>();
+        while (cursor.moveToNext()) {
+            newItems.add(cursorMapper.map(cursor));
+        }
+        this.items = newItems;
+    }
+
+    /**
+     * Get the loaded items
+     * @return the items
+     */
+    public List<T> getItems() {
+        return items;
+    }
+
+    public interface CursorMapper<T> {
+        T map(Cursor cursor);
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/geekdroid/src/main/java/com/geekorum/geekdroid/network/BrowserLauncher.kt	Fri May 08 21:33:19 2020 -0400
@@ -0,0 +1,150 @@
+/**
+ * Geekdroid is a utility library for development on the Android
+ * Platform.
+ *
+ * Copyright (C) 2017-2020 by Frederic-Charles Barthelery.
+ *
+ * This file is part of Geekdroid.
+ *
+ * Geekdroid is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Geekdroid is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Geekdroid.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.geekorum.geekdroid.network
+
+import android.app.Application
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.net.Uri
+import android.os.Bundle
+import androidx.browser.customtabs.CustomTabsClient
+import androidx.browser.customtabs.CustomTabsIntent
+import androidx.browser.customtabs.CustomTabsService
+import androidx.browser.customtabs.CustomTabsServiceConnection
+import androidx.browser.customtabs.CustomTabsSession
+import androidx.core.net.toUri
+import androidx.core.os.bundleOf
+import javax.inject.Inject
+
+/**
+ * Allow to easily launch and use a Browser [CustomTabsService]
+ */
+class BrowserLauncher
+@Inject constructor(
+    private val application: Application,
+    private val packageManager: PackageManager
+) {
+    private var customTabsClient: CustomTabsClient? = null
+    private var customTabsSession: CustomTabsSession? = null
+    private var serviceBinded: Boolean = false
+
+    private val customTabsConnection = object : CustomTabsServiceConnection() {
+        override fun onCustomTabsServiceConnected(name: ComponentName, client: CustomTabsClient) {
+            customTabsClient = client
+            customTabsClient?.warmup(0)
+            customTabsSession = customTabsClient?.newSession(null)
+        }
+
+        override fun onServiceDisconnected(name: ComponentName) {
+            customTabsClient = null
+        }
+    }
+
+    private val browserPackageNames: List<String>
+        get() {
+            val activityIntent = Intent(Intent.ACTION_VIEW, "http://".toUri())
+            val resolveInfoList =
+                packageManager.queryIntentActivities(activityIntent, PackageManager.MATCH_DEFAULT_ONLY)
+            return resolveInfoList.map { it.activityInfo.packageName }
+        }
+
+    /**
+     * Warms up the [CustomTabsService] application.
+     * Must be called first
+     *
+     * preferredPackageSelector: allows to select preferred service to use.
+     */
+    fun warmUp(preferredPackageSelector: (List<String>) -> List<String> = { it }) {
+        val packageName = CustomTabsClient.getPackageName(application, preferredPackageSelector(browserPackageNames))
+        if (packageName.isNullOrEmpty()) {
+            return
+        }
+        serviceBinded = CustomTabsClient.bindCustomTabsService(application, packageName, customTabsConnection)
+    }
+
+    @JvmOverloads
+    fun warmUp(preferredPackageSelector: PreferredPackageSelector? = null) {
+        warmUp { preferredPackageSelector?.orderByPreference(it) ?: it }
+    }
+
+    /**
+     * Shutdown the CustomTabsService
+     */
+    fun shutdown() {
+        if (serviceBinded) {
+            application.unbindService(customTabsConnection)
+        }
+    }
+
+    /**
+     * Try to preloads uris.
+     *
+     * @see [CustomTabsSession.mayLaunchUrl]
+     */
+    fun mayLaunchUrl(vararg uris: Uri) {
+        if (customTabsSession != null) {
+            uris.firstOrNull()?.let {
+                val otherLikelyBundles = createLikelyBundles(*uris)
+                customTabsSession?.mayLaunchUrl(it, null, otherLikelyBundles)
+            }
+        }
+    }
+
+    private fun createLikelyBundles(vararg uris: Uri): List<Bundle> {
+        return uris.asSequence()
+            .drop(1)
+            .map { bundleOf(CustomTabsService.KEY_URL to it) }
+            .toList()
+    }
+
+    fun launchUrl(context: Context, uri: Uri, customizer: (CustomTabsIntent.Builder.() -> Unit)? = null) {
+        if (customTabsSession != null) {
+            val builder = CustomTabsIntent.Builder(customTabsSession)
+            customizer?.invoke(builder)
+            val customTabsIntent = builder.build()
+            customTabsIntent.launchUrl(context, uri)
+        } else {
+            launchUriInOtherApp(context, uri)
+        }
+    }
+
+    @JvmOverloads
+    fun launchUrl(context: Context, uri: Uri, customizer: LaunchCustomizer? = null) {
+        launchUrl(context, uri) { customizer?.customize(this) }
+    }
+
+    interface LaunchCustomizer {
+        fun customize(builder: CustomTabsIntent.Builder)
+    }
+
+    interface PreferredPackageSelector {
+        fun orderByPreference(availablePackages: List<String>): List<String>
+    }
+
+    private fun launchUriInOtherApp(context: Context, uri: Uri) {
+        val intent = Intent(Intent.ACTION_VIEW, uri)
+        context.startActivity(intent)
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/geekdroid/src/main/java/com/geekorum/geekdroid/network/OkHttpWebViewClient.java	Fri May 08 21:33:19 2020 -0400
@@ -0,0 +1,74 @@
+/**
+ * Geekdroid is a utility library for development on the Android
+ * Platform.
+ *
+ * Copyright (C) 2017-2020 by Frederic-Charles Barthelery.
+ *
+ * This file is part of Geekdroid.
+ *
+ * Geekdroid is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Geekdroid is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Geekdroid.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.geekorum.geekdroid.network;
+
+import android.util.Log;
+import android.webkit.WebResourceRequest;
+import android.webkit.WebResourceResponse;
+import android.webkit.WebView;
+import android.webkit.WebViewClient;
+import okhttp3.Call;
+import okhttp3.Headers;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.Response;
+
+import java.io.IOException;
+
+import javax.inject.Inject;
+
+/**
+ * A {@link WebViewClient} that intercepts background request to execute them with OkHttp.
+ */
+public class OkHttpWebViewClient extends WebViewClient {
+
+    private final OkHttpClient okHttpClient;
+    private static final String TAG = OkHttpWebViewClient.class.getSimpleName();
+
+    @Inject
+    public OkHttpWebViewClient(OkHttpClient okHttpClient) {
+        this.okHttpClient = okHttpClient;
+    }
+
+    @Override
+    public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
+        if (shouldInterceptRequest(request)) {
+            Request req = new Request.Builder().get()
+                    .url(request.getUrl().toString())
+                    .headers(Headers.of(request.getRequestHeaders()))
+                    .build();
+            Call call = okHttpClient.newCall(req);
+            try {
+                Response response = call.execute();
+                return new WebResourceResponse(response.header("Content-Type"), response.header("Content-Encoding"),
+                        response.body().byteStream());
+            } catch (IOException e) {
+                Log.d(TAG, "error while intercepting request " + request.getUrl(), e);
+            }
+        }
+        return null;
+    }
+
+    protected boolean shouldInterceptRequest(WebResourceRequest request) {
+        return request.getMethod().equals("GET") && !request.isForMainFrame();
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/geekdroid/src/main/java/com/geekorum/geekdroid/network/PicassoOkHttp3Downloader.kt	Fri May 08 21:33:19 2020 -0400
@@ -0,0 +1,102 @@
+/**
+ * Geekdroid is a utility library for development on the Android
+ * Platform.
+ *
+ * Copyright (C) 2017-2020 by Frederic-Charles Barthelery.
+ *
+ * This file is part of Geekdroid.
+ *
+ * Geekdroid is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Geekdroid is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Geekdroid.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.geekorum.geekdroid.network
+
+import android.net.Uri
+import com.squareup.picasso.Downloader
+import com.squareup.picasso.NetworkPolicy
+import com.squareup.picasso.Picasso
+import okhttp3.*
+import java.io.IOException
+
+/**
+ * Quick and dirty implementation of Picasso [Downloader] which uses OkHttp3.
+ * This will be enough until Picasso-3.0.0 is out.
+ * TODO use OkHttp3Downloader from Picasso-3.0.0
+ */
+class PicassoOkHttp3Downloader(
+    private val client: OkHttpClient
+) : Downloader {
+
+    private val cache: Cache? = client.cache
+    private val sharedClient = true
+
+    override fun shutdown() {
+        if (!sharedClient) {
+            cache.use {
+                // simple way to close silently
+            }
+        }
+    }
+
+    override fun load(uri: Uri, networkPolicy: Int): Downloader.Response {
+        val request = createRequest(uri, networkPolicy)
+        val response = client.newCall(request).execute()
+        return createResult(response, networkPolicy)
+    }
+
+    private fun createRequest(uri: Uri, networkPolicy: Int): Request {
+        val cacheControl = buildCacheControl(networkPolicy)
+        return Request.Builder()
+            .url(uri.toString())
+            .apply {
+                if (cacheControl != null) {
+                    cacheControl(cacheControl)
+                }
+            }.build()
+    }
+
+    private fun buildCacheControl(networkPolicy: Int): CacheControl? {
+        return when {
+            networkPolicy == 0 -> null
+            NetworkPolicy.isOfflineOnly(networkPolicy) -> CacheControl.FORCE_CACHE
+            else -> CacheControl.Builder().apply {
+                if (NetworkPolicy.shouldReadFromDiskCache(networkPolicy)) {
+                    noCache()
+                }
+                if (NetworkPolicy.shouldWriteToDiskCache(networkPolicy)) {
+                    noStore()
+                }
+            }.build()
+        }
+    }
+
+    private fun createResult(response: Response, networkPolicy: Int): Downloader.Response {
+        val body = response.body!!
+        val loadedFrom = if (response.cacheResponse == null) Picasso.LoadedFrom.NETWORK else Picasso.LoadedFrom.DISK
+        return when {
+            !response.isSuccessful -> {
+                body.close()
+                throw Downloader.ResponseException("Failed request", networkPolicy, response.code)
+            }
+            // Sometimes response content length is zero when requests are being replayed. Haven't found
+            // root cause to this but retrying the request seems safe to do so.
+            loadedFrom == Picasso.LoadedFrom.DISK && body.contentLength() == 0L -> {
+                body.close()
+                throw IOException("Received response with 0 content-length header")
+            }
+            else -> Downloader.Response(body.byteStream(), loadedFrom != Picasso.LoadedFrom.NETWORK,
+                body.contentLength())
+        }
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/geekdroid/src/main/java/com/geekorum/geekdroid/network/Socket.kt	Fri May 08 21:33:19 2020 -0400
@@ -0,0 +1,70 @@
+/**
+ * Geekdroid is a utility library for development on the Android
+ * Platform.
+ *
+ * Copyright (C) 2017-2020 by Frederic-Charles Barthelery.
+ *
+ * This file is part of Geekdroid.
+ *
+ * Geekdroid is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Geekdroid is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Geekdroid.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.geekorum.geekdroid.network
+
+import android.net.TrafficStats
+import java.net.InetAddress
+import java.net.Socket
+import javax.net.SocketFactory
+
+/**
+ * A [SocketFactory] who tag its socket for [TrafficStats] analysis
+ */
+class TaggedSocketFactory(
+    private val delegate: SocketFactory,
+    private val tag: Int
+) : SocketFactory() {
+    override fun createSocket(host: String?, port: Int): Socket {
+        return delegate.createSocket(host, port).also {
+            configureSocket(it)
+        }
+    }
+
+
+    override fun createSocket(host: String?, port: Int, localHost: InetAddress?, localPort: Int): Socket {
+        return delegate.createSocket(host, port, localHost, localPort).also {
+            configureSocket(it)
+        }
+    }
+
+    override fun createSocket(host: InetAddress?, port: Int): Socket {
+        return delegate.createSocket(host, port).also {
+            configureSocket(it)
+        }
+    }
+
+    override fun createSocket(address: InetAddress?, port: Int, localAddress: InetAddress?, localPort: Int): Socket {
+        return delegate.createSocket(address, port, localAddress, localPort).also {
+            configureSocket(it)
+        }
+    }
+
+    override fun createSocket(): Socket {
+        return delegate.createSocket().also {
+            configureSocket(it)
+        }
+    }
+
+    private fun configureSocket(socket: Socket) {
+        TrafficStats.setThreadStatsTag(tag)
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/geekdroid/src/main/java/com/geekorum/geekdroid/network/TokenRetriever.kt	Fri May 08 21:33:19 2020 -0400
@@ -0,0 +1,37 @@
+/**
+ * Geekdroid is a utility library for development on the Android
+ * Platform.
+ *
+ * Copyright (C) 2017-2020 by Frederic-Charles Barthelery.
+ *
+ * This file is part of Geekdroid.
+ *
+ * Geekdroid is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Geekdroid is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Geekdroid.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.geekorum.geekdroid.network
+
+/**
+ * Api to retrieve and manipulate token
+ */
+interface TokenRetriever {
+    @Throws(RetrieverException::class)
+    suspend fun getToken(): String
+
+    suspend fun invalidateToken()
+
+    class RetrieverException @JvmOverloads constructor(
+        message: String? = null, cause: Throwable? = null
+    ) : Exception(message, cause)
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/geekdroid/src/main/java/com/geekorum/geekdroid/preferences/PreferenceSummaryBinder.java	Fri May 08 21:33:19 2020 -0400
@@ -0,0 +1,91 @@
+/**
+ * Geekdroid is a utility library for development on the Android
+ * Platform.
+ *
+ * Copyright (C) 2017-2020 by Frederic-Charles Barthelery.
+ *
+ * This file is part of Geekdroid.
+ *
+ * Geekdroid is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Geekdroid is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Geekdroid.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.geekorum.geekdroid.preferences;
+
+import androidx.preference.ListPreference;
+import androidx.preference.Preference;
+import androidx.preference.PreferenceManager;
+
+/**
+ * Helper class to set the summary of an {@link android.preference.Preference} to its actual value.
+ * @deprecated use androidx.preference.Preference.SummaryProvider
+ */
+@Deprecated
+public class PreferenceSummaryBinder implements Preference.OnPreferenceChangeListener {
+
+    /**
+     * A preference value change listener that updates the preference's summary
+     * to reflect its new value.
+     */
+    @Override
+    public boolean onPreferenceChange(Preference preference, Object value) {
+        String stringValue = value.toString();
+
+        if (preference instanceof ListPreference) {
+            setListPreferenceSummary(preference, stringValue);
+        } else {
+            setGenericPreferenceSummary(preference, stringValue);
+        }
+        return true;
+    }
+
+    private void setGenericPreferenceSummary(Preference preference, String stringValue) {
+        // For all other preferences, set the summary to the value's
+        // simple string representation.
+        preference.setSummary(stringValue);
+    }
+
+    private void setListPreferenceSummary(Preference preference, String stringValue) {
+        // For list preferences, look up the correct display value in
+        // the preference's 'entries' list.
+        ListPreference listPreference = (ListPreference) preference;
+        int index = listPreference.findIndexOfValue(stringValue);
+
+        // Set the summary to reflect the new value.
+        preference.setSummary(
+                index >= 0
+                        ? listPreference.getEntries()[index]
+                        : null);
+    }
+
+    /**
+     * Binds a preference's summary to its value. More specifically, when the
+     * preference's value is changed, its summary (line of text below the
+     * preference title) is updated to reflect the value. The summary is also
+     * immediately updated upon calling this method. The exact display format is
+     * dependent on the type of preference.
+     *
+     */
+    public void bindPreferenceSummaryToValue(Preference preference) {
+        // Set the listener to watch for value changes.
+        preference.setOnPreferenceChangeListener(this);
+
+        // Trigger the listener immediately with the preference's
+        // current value.
+        onPreferenceChange(preference,
+                PreferenceManager
+                        .getDefaultSharedPreferences(preference.getContext())
+                        .getString(preference.getKey(), ""));
+    }
+
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/geekdroid/src/main/java/com/geekorum/geekdroid/preferences/RingtonePreferenceSummaryBinder.kt	Fri May 08 21:33:19 2020 -0400
@@ -0,0 +1,79 @@
+/**
+ * Geekdroid is a utility library for development on the Android
+ * Platform.
+ *
+ * Copyright (C) 2017-2020 by Frederic-Charles Barthelery.
+ *
+ * This file is part of Geekdroid.
+ *
+ * Geekdroid is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Geekdroid is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Geekdroid.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.geekorum.geekdroid.preferences
+
+import android.media.Ringtone
+import android.media.RingtoneManager
+import android.preference.Preference
+import android.preference.RingtonePreference
+import androidx.core.net.toUri
+import androidx.preference.PreferenceManager
+import com.geekorum.geekdroid.R
+
+/**
+ * Helper class to set the summary of an [android.preference.RingtonePreference] to its actual value.
+ * @deprecated use androidx.preference.Preference.SummaryProvider
+ */
+@Deprecated("Use androidx.preference.Preference.SummaryProvider")
+class RingtonePreferenceSummaryBinder : Preference.OnPreferenceChangeListener {
+    override fun onPreferenceChange(preference: Preference, newValue: Any): Boolean {
+        val stringValue = newValue.toString()
+
+        when (preference) {
+            is RingtonePreference -> setRingtonePreferenceSummary(preference, stringValue)
+        }
+        return true
+    }
+
+    private fun setRingtonePreferenceSummary(preference: Preference, stringValue: String) {
+        val summary = when {
+            // Empty values correspond to 'silent' (no ringtone).
+            stringValue.isEmpty() -> preference.context.getString(R.string.geekdroid_pref_ringtone_silent)
+            else -> {
+                val ringtone: Ringtone? = RingtoneManager.getRingtone(preference.context, stringValue.toUri())
+                ringtone?.getTitle(preference.context)
+            }
+        }
+        preference.summary = summary
+    }
+
+    /**
+     * Binds a preference's summary to its value. More specifically, when the
+     * preference's value is changed, its summary (line of text below the
+     * preference title) is updated to reflect the value. The summary is also
+     * immediately updated upon calling this method. The exact display format is
+     * dependent on the type of preference.
+     *
+     */
+    fun bindPreferenceSummaryToValue(preference: Preference) {
+        // Set the listener to watch for value changes.
+        preference.onPreferenceChangeListener = this
+
+        // Trigger the listener immediately with the preference's
+        // current value.
+        onPreferenceChange(preference,
+            PreferenceManager
+                .getDefaultSharedPreferences(preference.context)
+                .getString(preference.key, "")!!)
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/geekdroid/src/main/java/com/geekorum/geekdroid/security/SimpleEncryption.kt	Fri May 08 21:33:19 2020 -0400
@@ -0,0 +1,108 @@
+/**
+ * Geekdroid is a utility library for development on the Android
+ * Platform.
+ *
+ * Copyright (C) 2017-2020 by Frederic-Charles Barthelery.
+ *
+ * This file is part of Geekdroid.
+ *
+ * Geekdroid is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Geekdroid is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Geekdroid.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.geekorum.geekdroid.security
+
+import android.os.Build
+import android.security.keystore.KeyGenParameterSpec
+import android.security.keystore.KeyProperties
+import android.security.keystore.KeyProperties.BLOCK_MODE_GCM
+import android.security.keystore.KeyProperties.ENCRYPTION_PADDING_NONE
+import android.security.keystore.KeyProperties.KEY_ALGORITHM_AES
+import android.security.keystore.KeyProperties.PURPOSE_DECRYPT
+import android.security.keystore.KeyProperties.PURPOSE_ENCRYPT
+import androidx.annotation.RequiresApi
+import java.security.Key
+import java.security.KeyStore
+import java.security.spec.AlgorithmParameterSpec
+import javax.crypto.Cipher
+import javax.crypto.Cipher.getInstance
+import javax.crypto.KeyGenerator
+import javax.crypto.spec.GCMParameterSpec
+import javax.inject.Inject
+
+/**
+ * Simplify usage of best practices for secret encryption on Android.
+ */
+@RequiresApi(Build.VERSION_CODES.M)
+class SecretEncryption @Inject constructor() {
+    companion object {
+        const val ANDROID_KEYSTORE_PROVIDER = "AndroidKeyStore"
+        const val CIPHER_TRANSFORMATION = "$KEY_ALGORITHM_AES/$BLOCK_MODE_GCM/$ENCRYPTION_PADDING_NONE"
+    }
+
+    private val keystore = KeyStore.getInstance(ANDROID_KEYSTORE_PROVIDER).apply { load(null) }
+
+    fun getKey(alias: String) = keystore.getKey(alias, null)
+
+    fun getOrCreateKey(alias: String): Key {
+        return if (keystore.containsAlias(alias)) {
+            getKey(alias)
+        } else {
+            val params = defaultKeyGenParameterSpec(alias)
+            generateKey(KeyProperties.KEY_ALGORITHM_AES, params)
+        }
+    }
+
+    fun hasKey(alias: String) = keystore.isKeyEntry(alias)
+
+    fun deleteKey(alias: String) = keystore.deleteEntry(alias)
+
+    private fun defaultKeyGenParameterSpec(keyAlias: String): KeyGenParameterSpec {
+        return KeyGenParameterSpec.Builder(keyAlias, (PURPOSE_ENCRYPT or PURPOSE_DECRYPT))
+            .setBlockModes(BLOCK_MODE_GCM)
+            .setEncryptionPaddings(ENCRYPTION_PADDING_NONE)
+            .build()
+    }
+
+    private fun generateKey(algorithm: String, params: AlgorithmParameterSpec): Key {
+        val keyGenerator = KeyGenerator.getInstance(algorithm, ANDROID_KEYSTORE_PROVIDER)
+        keyGenerator.init(params)
+        return keyGenerator.generateKey()
+    }
+
+    fun getSecretCipher(key: Key): SecretCipher = SecretCipher(key)
+
+    fun getSecretCipher(keyAlias: String): SecretCipher {
+        val key = getOrCreateKey(keyAlias)
+        return getSecretCipher(key)
+    }
+}
+
+@RequiresApi(Build.VERSION_CODES.M)
+class SecretCipher(
+    private val key: Key
+) {
+
+    private val cipher = getInstance(SecretEncryption.CIPHER_TRANSFORMATION)
+
+    fun encrypt(input: ByteArray): ByteArray {
+        cipher.init(Cipher.ENCRYPT_MODE, key)
+        return cipher.doFinal(input)
+    }
+
+    val parametersSpec: GCMParameterSpec get() = cipher.parameters.getParameterSpec(GCMParameterSpec::class.java)
+
+    fun decrypt(input: ByteArray, gcmParameterSpec: GCMParameterSpec): ByteArray {
+        cipher.init(Cipher.DECRYPT_MODE, key, gcmParameterSpec)
+        return cipher.doFinal(input)
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/geekdroid/src/main/java/com/geekorum/geekdroid/utils/PriorityRunnable.java	Fri May 08 21:33:19 2020 -0400
@@ -0,0 +1,44 @@
+/**
+ * Geekdroid is a utility library for development on the Android
+ * Platform.
+ *
+ * Copyright (C) 2017-2020 by Frederic-Charles Barthelery.
+ *
+ * This file is part of Geekdroid.
+ *
+ * Geekdroid is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Geekdroid is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Geekdroid.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.geekorum.geekdroid.utils;
+
+import android.os.Process;
+
+/**
+ * A Runnable who can have have and Android Process Priority.
+ */
+public class PriorityRunnable implements Runnable {
+    private final int processPriority;
+    private final Runnable runnable;
+
+    public PriorityRunnable(@ProcessPriority int processPriority, Runnable runnable) {
+        this.processPriority = processPriority;
+        this.runnable = runnable;
+    }
+
+    @Override
+    public void run() {
+        Process.setThreadPriority(processPriority);
+        runnable.run();
+    }
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/geekdroid/src/main/java/com/geekorum/geekdroid/utils/ProcessPriority.java	Fri May 08 21:33:19 2020 -0400
@@ -0,0 +1,39 @@
+/**
+ * Geekdroid is a utility library for development on the Android
+ * Platform.
+ *
+ * Copyright (C) 2017-2020 by Frederic-Charles Barthelery.
+ *
+ * This file is part of Geekdroid.
+ *
+ * Geekdroid is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Geekdroid is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Geekdroid.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.geekorum.geekdroid.utils;
+
+import android.os.Process;
+import androidx.annotation.IntDef;
+
+import java.lang.annotation.Retention;
+
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+/**
+ * Describes the allowed value for a Process priority.
+ */
+@IntDef({Process.THREAD_PRIORITY_AUDIO, Process.THREAD_PRIORITY_BACKGROUND, Process.THREAD_PRIORITY_DEFAULT,
+        Process.THREAD_PRIORITY_DISPLAY, Process.THREAD_PRIORITY_FOREGROUND, Process.THREAD_PRIORITY_LESS_FAVORABLE,
+        Process.THREAD_PRIORITY_LOWEST, Process.THREAD_PRIORITY_MORE_FAVORABLE, Process.THREAD_PRIORITY_URGENT_AUDIO,
+        Process.THREAD_PRIORITY_URGENT_DISPLAY })
+@Retention(SOURCE)
+public @interface ProcessPriority { }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/geekdroid/src/main/java/com/geekorum/geekdroid/views/AccountMenuLineView.kt	Fri May 08 21:33:19 2020 -0400
@@ -0,0 +1,121 @@
+/**
+ * Geekdroid is a utility library for development on the Android
+ * Platform.
+ *
+ * Copyright (C) 2017-2020 by Frederic-Charles Barthelery.
+ *
+ * This file is part of Geekdroid.
+ *
+ * Geekdroid is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Geekdroid is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Geekdroid.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.geekorum.geekdroid.views
+
+import android.accounts.Account
+import android.content.Context
+import android.os.Parcel
+import android.os.Parcelable
+import android.util.AttributeSet
+import android.view.View
+import android.widget.ImageView
+import android.widget.LinearLayout
+import android.widget.TextView
+import com.geekorum.geekdroid.R
+
+/**
+ * A simple view to simulate a spinner to select an Account in a Navigation menu.
+ */
+class AccountMenuLineView
+    @JvmOverloads constructor(context: Context,
+                              attrs: AttributeSet? = null,
+                              defStyleAttr: Int = 0
+    ) : LinearLayout(context, attrs, defStyleAttr) {
+
+    private lateinit var accountText: TextView
+    private lateinit var handleView: ImageView
+
+    var isExpanded: Boolean = false
+        set(expanded) {
+            field = expanded
+            refreshDrawableState()
+        }
+
+    init {
+        init(context, attrs)
+    }
+
+    private fun init(context: Context, attrs: AttributeSet?) {
+        View.inflate(context, R.layout.view_account_menu_line, this)
+        handleView = findViewById(R.id.handle)
+        accountText = findViewById(R.id.account)
+    }
+
+    fun setAccount(account: Account) {
+        accountText.text = account.name
+    }
+
+    public override fun onCreateDrawableState(extraSpace: Int): IntArray {
+        val drawableState = super.onCreateDrawableState(extraSpace + 1)
+        if (isExpanded) {
+            View.mergeDrawableStates(drawableState, EXPANDED_STATE_SET)
+        }
+        return drawableState
+    }
+
+    internal class SavedState : BaseSavedState {
+        internal var expanded: Boolean = false
+
+        constructor(source: Parcel) : super(source) {
+            expanded = source.readValue(javaClass.classLoader) as Boolean
+        }
+
+        constructor(superState: Parcelable) : super(superState)
+
+        override fun writeToParcel(out: Parcel, flags: Int) {
+            super.writeToParcel(out, flags)
+            out.writeValue(expanded)
+        }
+
+        companion object {
+
+            @JvmField
+            val CREATOR: Parcelable.Creator<SavedState> = object : Parcelable.Creator<SavedState> {
+
+                override fun createFromParcel(`in`: Parcel): SavedState = SavedState(`in`)
+
+                override fun newArray(size: Int): Array<SavedState?> = arrayOfNulls(size)
+            }
+        }
+    }
+
+    public override fun onSaveInstanceState(): Parcelable? {
+        val superState = checkNotNull(super.onSaveInstanceState())
+        return SavedState(superState).apply {
+            expanded = isExpanded
+        }
+    }
+
+    public override fun onRestoreInstanceState(state: Parcelable) {
+        val ss = state as SavedState
+
+        super.onRestoreInstanceState(ss.superState)
+        isExpanded = ss.expanded
+        requestLayout()
+    }
+
+    companion object {
+        private val EXPANDED_STATE_SET = intArrayOf(android.R.attr.state_expanded)
+    }
+
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/geekdroid/src/main/java/com/geekorum/geekdroid/views/CheckableImageView.java	Fri May 08 21:33:19 2020 -0400
@@ -0,0 +1,178 @@
+/**
+ * Geekdroid is a utility library for development on the Android
+ * Platform.
+ *
+ * Copyright (C) 2017-2020 by Frederic-Charles Barthelery.
+ *
+ * This file is part of Geekdroid.
+ *
+ * Geekdroid is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Geekdroid is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Geekdroid.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.geekorum.geekdroid.views;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.AttributeSet;
+import android.widget.Checkable;
+import android.widget.ImageView;
+import com.geekorum.geekdroid.R;
+
+/**
+ * A simple ImageView with a Checked state.
+ * <p>
+ * Basically, it works like a {@link android.widget.CheckBox}. However, Checkbox is based on TextView
+ * and therefore expect a text which prevent it to be centered. The {@link CheckableImageView} is based on {@link ImageView}
+ * and give us total control on the drawable placement.
+ */
+public class CheckableImageView extends ImageView implements Checkable {
+
+    private static final int[] CHECKED_STATE_SET = {
+            android.R.attr.state_checked
+    };
+
+    private boolean checked;
+    private OnCheckedChangeListener onCheckedChangeListener;
+
+    public CheckableImageView(Context context) {
+        this(context, null);
+    }
+
+    public CheckableImageView(Context context, AttributeSet attrs) {
+        this(context, attrs, R.attr.checkableImageViewStyle);
+    }
+
+    public CheckableImageView(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr, R.style.Widget_Geekdroid_CheckableImageView);
+        init(context, attrs);
+    }
+
+    void init(Context context, AttributeSet attrs) {
+        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CheckableImageView);
+        this.checked = typedArray.getBoolean(R.styleable.CheckableImageView_android_checked, false);
+        typedArray.recycle();
+    }
+
+    @Override
+    public void setChecked(boolean checked) {
+        if (this.checked != checked) {
+            this.checked = checked;
+            drawableStateChanged();
+            if (onCheckedChangeListener != null) {
+                onCheckedChangeListener.onCheckedChanged(this, this.checked);
+            }
+        }
+    }
+
+    @Override
+    public boolean isChecked() {
+        return checked;
+    }
+
+    @Override
+    public void toggle() {
+        setChecked(!checked);
+    }
+
+    @Override
+    public int[] onCreateDrawableState(int extraSpace) {
+        final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
+        if (isChecked()) {
+            mergeDrawableStates(drawableState, CHECKED_STATE_SET);
+        }
+        return drawableState;
+    }
+
+    @Override
+    public boolean performClick() {
+        toggle();
+        return super.performClick();
+    }
+
+    /**
+     * Interface definition for a callback to be invoked when the checked state
+     * of a CheckableImageView  changed.
+     */
+    public interface OnCheckedChangeListener {
+        /**
+         * Called when the checked state of a compound button has changed.
+         *
+         * @param checkableImageView The CheckableImageView view whose state has changed.
+         * @param isChecked          The new checked state of buttonView.
+         */
+        void onCheckedChanged(CheckableImageView checkableImageView, boolean isChecked);
+    }
+
+    /**
+     * Register a callback to be invoked when the checked state of this button
+     * changes.
+     *
+     * @param listener the callback to call on checked state change
+     */
+    public void setOnCheckedChangeListener(OnCheckedChangeListener listener) {
+        onCheckedChangeListener = listener;
+    }
+
+    static class SavedState extends BaseSavedState {
+        private boolean checked;
+
+        @SuppressLint("ParcelClassLoader")
+        SavedState(Parcel source) {
+            super(source);
+            checked = (Boolean) source.readValue(null);
+        }
+
+        SavedState(Parcelable superState) {
+            super(superState);
+        }
+
+        @Override
+        public void writeToParcel(Parcel out, int flags) {
+            super.writeToParcel(out, flags);
+            out.writeValue(checked);
+        }
+
+        public static final Parcelable.Creator<SavedState> CREATOR
+                = new Parcelable.Creator<SavedState>() {
+            public SavedState createFromParcel(Parcel in) {
+                return new SavedState(in);
+            }
+
+            public SavedState[] newArray(int size) {
+                return new SavedState[size];
+            }
+        };
+    }
+
+    @Override
+    public Parcelable onSaveInstanceState() {
+        Parcelable superState = super.onSaveInstanceState();
+
+        SavedState ss = new SavedState(superState);
+
+        ss.checked = isChecked();
+        return ss;
+    }
+
+    @Override
+    public void onRestoreInstanceState(Parcelable state) {
+        SavedState ss = (SavedState) state;
+
+        super.onRestoreInstanceState(ss.getSuperState());
+        setChecked(ss.checked);
+        requestLayout();
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/geekdroid/src/main/java/com/geekorum/geekdroid/views/EdgeToEdge.kt	Fri May 08 21:33:19 2020 -0400
@@ -0,0 +1,35 @@
+/**
+ * Geekdroid is a utility library for development on the Android
+ * Platform.
+ *
+ * Copyright (C) 2017-2020 by Frederic-Charles Barthelery.
+ *
+ * This file is part of Geekdroid.
+ *
+ * Geekdroid is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Geekdroid is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Geekdroid.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.geekorum.geekdroid.views
+
+import android.annotation.SuppressLint
+import android.view.View
+import androidx.core.view.WindowInsetsCompat
+import com.google.android.material.internal.ViewUtils
+
+
+// see https://medium.com/androiddevelopers/windowinsets-listeners-to-layouts-8f9ccc8fa4d1
+// Lazy implementation by using the Material one
+@SuppressLint("RestrictedApi")
+fun View.doOnApplyWindowInsets(block: (View, WindowInsetsCompat, ViewUtils.RelativePadding) -> WindowInsetsCompat) {
+    ViewUtils.doOnApplyWindowInsets(this, ViewUtils.OnApplyWindowInsetsListener(block))
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/geekdroid/src/main/java/com/geekorum/geekdroid/views/MultipleLongClickGestureDetector.java	Fri May 08 21:33:19 2020 -0400
@@ -0,0 +1,182 @@
+/**
+ * Geekdroid is a utility library for development on the Android
+ * Platform.
+ *
+ * Copyright (C) 2017-2020 by Frederic-Charles Barthelery.
+ *
+ * This file is part of Geekdroid.
+ *
+ * Geekdroid is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Geekdroid is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Geekdroid.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.geekorum.geekdroid.views;
+
+import android.content.Context;
+import android.os.Handler;
+import android.os.Message;
+import androidx.core.view.MotionEventCompat;
+import android.view.MotionEvent;
+import android.view.ViewConfiguration;
+
+/**
+ * A simple gesture detector that detect when the user stay in long click with 3 touches.
+ */
+public class MultipleLongClickGestureDetector {
+
+    private static final int TRIPLE_LONG_PRESS = 0;
+    private static final int LONGPRESS_TIMEOUT = ViewConfiguration.getLongPressTimeout();
+    private static final int TAP_TIMEOUT = ViewConfiguration.getTapTimeout();
+
+    private final Handler handler;
+    private final OnGestureListener listener;
+    private final int nbPointersTrigger;
+    private int currentPointers;
+    private MotionEvent multiplePointerEvent;
+
+    private class GestureHandler extends Handler {
+        GestureHandler() {
+            super();
+        }
+
+        GestureHandler(Handler handler) {
+            super(handler.getLooper());
+        }
+
+        @Override
+        public void handleMessage(Message msg) {
+            switch (msg.what) {
+                case TRIPLE_LONG_PRESS:
+                    dispatchTripleLongPress();
+                    break;
+
+                default:
+                    throw new RuntimeException("Unknown message " + msg); //never
+            }
+        }
+    }
+
+    /**
+     * Creates a {@link MultipleLongClickGestureDetector} with the supplied listener.
+     * You may only use this constructor from a UI thread (this is the usual situation).
+     *
+     * @param context  the application's context
+     * @param listener the listener invoked for all the callbacks, this must
+     *                 not be null.
+     * @param nbPointers the number of down pointers before sending a multiple long click event
+     *
+     * @throws NullPointerException if {@code listener} is null.
+     * @see Handler#Handler()
+     */
+    public MultipleLongClickGestureDetector(Context context, OnGestureListener listener, int nbPointers) {
+        this(context, listener, nbPointers, null);
+    }
+
+
+    /**
+     * Creates a {@link MultipleLongClickGestureDetector} with the supplied listener.
+     * You may only use this constructor from a UI thread (this is the usual situation).
+     *
+     * @param context  the application's context
+     * @param listener the listener invoked for all the callbacks, this must
+     *                 not be null.
+     * @param handler  the handler to use
+     *
+     * @throws NullPointerException if {@code listener} is null.
+     * @see Handler#Handler()
+     */
+    public MultipleLongClickGestureDetector(Context context, OnGestureListener listener, int nbPointers,
+                                            Handler handler) {
+        if (handler != null) {
+            this.handler = new GestureHandler(handler);
+        } else {
+            this.handler = new GestureHandler();
+        }
+        this.listener = listener;
+        this.nbPointersTrigger = nbPointers;
+        init(context);
+    }
+
+    private void init(Context context) {
+        if (context == null) {
+            throw new IllegalArgumentException("Context must not be null");
+        }
+        if (listener == null) {
+            throw new IllegalArgumentException("OnGestureListener must not be null");
+        }
+    }
+
+    /**
+     * Analyzes the given motion event and if applicable triggers the
+     * appropriate callbacks on the {@link android.view.GestureDetector.OnGestureListener} supplied.
+     *
+     * @param ev The current motion event.
+     *
+     * @return true if the {@link android.view.GestureDetector.OnGestureListener} consumed the event,
+     * else false.
+     */
+    public boolean onTouchEvent(MotionEvent ev) {
+        final int action = ev.getAction();
+
+        boolean handled = false;
+        switch (action & MotionEventCompat.ACTION_MASK) {
+            case MotionEventCompat.ACTION_POINTER_DOWN:
+                currentPointers++;
+                if (currentPointers == nbPointersTrigger) {
+                    multiplePointerEvent = ev;
+                }
+                if (currentPointers >= nbPointersTrigger) {
+                    cancel();
+                    handler.sendEmptyMessageDelayed(TRIPLE_LONG_PRESS, TAP_TIMEOUT + LONGPRESS_TIMEOUT);
+                }
+                break;
+
+            case MotionEventCompat.ACTION_POINTER_UP:
+                currentPointers--;
+                if (currentPointers < nbPointersTrigger) {
+                    cancel();
+                }
+                break;
+
+            case MotionEvent.ACTION_DOWN:
+                currentPointers = 1;
+                handled = true;
+                break;
+
+            case MotionEvent.ACTION_UP:
+                currentPointers--;
+                cancel();
+                break;
+
+            case MotionEvent.ACTION_CANCEL:
+                cancel();
+                break;
+            default:
+                // do nothing we don't need the other events
+                break;
+        }
+
+        return handled;
+    }
+
+    private void cancel() {
+        handler.removeMessages(TRIPLE_LONG_PRESS);
+    }
+
+    private void dispatchTripleLongPress() {
+        listener.onMultipleLongPressed(multiplePointerEvent);
+    }
+
+    public interface OnGestureListener {
+        void onMultipleLongPressed(MotionEvent event);
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/geekdroid/src/main/java/com/geekorum/geekdroid/views/ReturningBottomAppBar.kt	Fri May 08 21:33:19 2020 -0400
@@ -0,0 +1,84 @@
+/**
+ * Geekdroid is a utility library for development on the Android
+ * Platform.
+ *
+ * Copyright (C) 2017-2020 by Frederic-Charles Barthelery.
+ *
+ * This file is part of Geekdroid.
+ *
+ * Geekdroid is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Geekdroid is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Geekdroid.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.geekorum.geekdroid.views
+
+import android.content.Context
+import android.util.AttributeSet
+import android.util.Log
+import android.view.View
+import android.view.ViewGroup
+import android.widget.FrameLayout
+import androidx.coordinatorlayout.widget.CoordinatorLayout
+import androidx.core.view.ScrollingView
+import androidx.core.widget.NestedScrollView
+import com.google.android.material.bottomappbar.BottomAppBar
+
+/**
+ * A [BottomAppBar] that returned on screen when you reach the end of the scrolling view
+ */
+class ReturningBottomAppBar : BottomAppBar {
+
+    constructor(context: Context) : super(context)
+    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
+    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
+
+    override fun getBehavior(): BottomAppBar.Behavior {
+        return Behavior()
+    }
+
+    class Behavior : BottomAppBar.Behavior() {
+
+        override fun onNestedScroll(coordinatorLayout: CoordinatorLayout, child: BottomAppBar,
+                                    target: View, dxConsumed: Int, dyConsumed: Int,
+                                    dxUnconsumed: Int, dyUnconsumed: Int, type: Int,
+                                    consumed: IntArray) {
+
+            val isWithinEndOfScroll = when(target) {
+                is NestedScrollView -> target.scrollY > (target.getScrollRange() - child.measuredHeight)
+                else -> dyConsumed == 0
+            }
+            // override dyConsummed as if we were scrolling up.
+            val newDyConsumed = if (isWithinEndOfScroll) {
+                -1
+            } else {
+                dyConsumed
+            }
+            super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, newDyConsumed, dxUnconsumed,
+                dyUnconsumed,
+                type, consumed)
+        }
+
+        private fun NestedScrollView.getScrollRange(): Int {
+            var scrollRange = 0
+            if (childCount > 0) {
+                val child = getChildAt(0)
+                val lp = child.layoutParams as MarginLayoutParams
+                val childSize = child.height + lp.topMargin + lp.bottomMargin
+                val parentSpace = height - paddingTop - paddingBottom
+                scrollRange = Math.max(0, childSize - parentSpace)
+            }
+            return scrollRange
+        }
+
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/geekdroid/src/main/java/com/geekorum/geekdroid/views/banners/Banners.kt	Fri May 08 21:33:19 2020 -0400
@@ -0,0 +1,165 @@
+/**
+ * Geekdroid is a utility library for development on the Android
+ * Platform.
+ *
+ * Copyright (C) 2017-2020 by Frederic-Charles Barthelery.
+ *
+ * This file is part of Geekdroid.
+ *
+ * Geekdroid is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Geekdroid is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Geekdroid.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.geekorum.geekdroid.views.banners
+
+import android.content.Context
+import android.util.AttributeSet
+import android.view.LayoutInflater
+import android.view.View
+import android.view.WindowInsets
+import android.widget.FrameLayout
+import androidx.coordinatorlayout.widget.CoordinatorLayout
+import androidx.core.graphics.drawable.IconCompat
+import androidx.core.view.ViewCompat
+import androidx.core.view.forEach
+import com.geekorum.geekdroid.databinding.ViewBannerExtendedBinding
+import com.geekorum.geekdroid.databinding.ViewBannerSimpleBinding
+import com.geekorum.geekdroid.views.doOnApplyWindowInsets
+import com.google.android.material.card.MaterialCardView
+import com.google.android.material.internal.ViewUtils
+import com.google.android.material.shape.MaterialShapeDrawable
+
+/**
+ * Simple container for Material banners.
+ *
+ * Add it in your layout where you want to display a banner, then use [show] and [hide] methods
+ * to control the banner.
+ */
+class BannerContainer @JvmOverloads constructor(
+    context: Context,
+    attrs: AttributeSet? = null
+) : FrameLayout(context, attrs) {
+
+    private val layoutInflater = LayoutInflater.from(context)
+    private val backgroundDrawable: MaterialShapeDrawable = MaterialShapeDrawable.createWithElevationOverlay(context)
+
+    init {
+        background = backgroundDrawable
+    }
+
+    fun hide() {
+        removeAllViews()
+    }
+
+    fun show(banner: BannerSpec) {
+        removeAllViews()
+
+        val binding = when {
+            (banner.positiveBtn != null && banner.negativeBtn != null)
+                    || (banner.icon != null)
+            -> createExtendedBanner(banner)
+            else -> createSimpleBanner(banner)
+        }
+    }
+
+    private fun createSimpleBanner(banner: BannerSpec): ViewBannerSimpleBinding {
+        val view = ViewBannerSimpleBinding.inflate(layoutInflater,
+            this, true)
+        bindSimpleBanner(view, banner)
+        return view
+    }
+
+    private fun bindSimpleBanner(binding: ViewBannerSimpleBinding, banner: BannerSpec) {
+        binding.message.text = banner.message
+        when (banner.positiveBtn) {
+            null -> binding.positiveBtn.visibility = View.GONE
+            else -> {
+                val (text, listener) = banner.positiveBtn
+                binding.positiveBtn.text = text
+                binding.positiveBtn.setOnClickListener(listener)
+            }
+        }
+    }
+
+
+    private fun createExtendedBanner(banner: BannerSpec): ViewBannerExtendedBinding {
+        val view = ViewBannerExtendedBinding.inflate(layoutInflater,
+            this, true)
+        bindExtendedBanner(view, banner)
+        return view
+    }
+
+    private fun bindExtendedBanner(binding: ViewBannerExtendedBinding, banner: BannerSpec) {
+        binding.message.text = banner.message
+        binding.icon.setImageIcon(banner.icon?.toIcon())
+        if (banner.icon == null) {
+            binding.icon.visibility = View.GONE
+        }
+        when (banner.positiveBtn) {
+            null -> binding.positiveBtn.visibility = View.GONE
+            else -> {
+                val (text, listener) = banner.positiveBtn
+                binding.positiveBtn.text = text
+                binding.positiveBtn.setOnClickListener(listener)
+            }
+        }
+        when (banner.negativeBtn) {
+            null -> binding.negativeBtn.visibility = View.GONE
+            else -> {
+                val (text, listener) = banner.negativeBtn
+                binding.negativeBtn.text = text
+                binding.negativeBtn.setOnClickListener(listener)
+            }
+        }
+    }
+
+    override fun onApplyWindowInsets(insets: WindowInsets): WindowInsets {
+        // just dispatch insets to children if any
+        if (insets.isConsumed) {
+            return insets
+        }
+        var consumedInsets = insets
+        forEach {
+            consumedInsets = it.dispatchApplyWindowInsets(consumedInsets)
+            if (consumedInsets.isConsumed) {
+                return@forEach
+            }
+        }
+        return consumedInsets
+    }
+
+    override fun setElevation(elevation: Float) {
+        if (background != null && background == backgroundDrawable) {
+            backgroundDrawable.elevation = elevation
+        } else {
+            post { setElevation(elevation) }
+        }
+    }
+
+    override fun getElevation(): Float = backgroundDrawable.elevation
+}
+
+
+/**
+ * Describe a banner
+ */
+data class BannerSpec(
+    val message: String,
+    val positiveBtn: ButtonSpec? = null,
+    val negativeBtn: ButtonSpec? = null,
+    val icon: IconCompat? = null
+)
+
+data class ButtonSpec(
+    val text: String,
+    val onClickListener: View.OnClickListener
+)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/geekdroid/src/main/java/com/geekorum/geekdroid/views/banners/Builders.kt	Fri May 08 21:33:19 2020 -0400
@@ -0,0 +1,127 @@
+/**
+ * Geekdroid is a utility library for development on the Android
+ * Platform.
+ *
+ * Copyright (C) 2017-2020 by Frederic-Charles Barthelery.
+ *
+ * This file is part of Geekdroid.
+ *
+ * Geekdroid is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Geekdroid is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Geekdroid.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.geekorum.geekdroid.views.banners
+
+import android.content.Context
+import android.view.View
+import androidx.annotation.StringRes
+import androidx.core.graphics.drawable.IconCompat
+
+/**
+ * Helper class to build a [BannerSpec]
+ */
+class BannerBuilder(
+    private val context: Context
+) {
+    private var message: String = ""
+    private var icon: IconCompat? = null
+
+    private var positiveBtn: ButtonSpec? = null
+    private var negativeBtn: ButtonSpec? = null
+
+    fun setMessage(msg: String): BannerBuilder {
+        message = msg
+        return this
+    }
+
+    fun setMessage(@StringRes msgId: Int): BannerBuilder {
+        return setMessage(context.getString(msgId))
+    }
+
+    fun setIcon(icon: IconCompat?): BannerBuilder {
+        this.icon = icon
+        return this
+    }
+
+    fun setPositiveButton(
+        @StringRes textId: Int, onClickListener: View.OnClickListener
+    ): BannerBuilder {
+        val text = context.getString(textId)
+        return setPositiveButton(text, onClickListener)
+    }
+
+    fun setPositiveButton(text: String, onClickListener: View.OnClickListener): BannerBuilder {
+        positiveBtn = ButtonSpec(text, onClickListener)
+        return this
+    }
+
+    fun setNegativeButton(
+        @StringRes textId: Int, onClickListener: View.OnClickListener
+    ): BannerBuilder {
+        val text = context.getString(textId)
+        return setNegativeButton(text, onClickListener)
+    }
+
+    fun setNegativeButton(text: String, onClickListener: View.OnClickListener): BannerBuilder {
+        negativeBtn = ButtonSpec(text, onClickListener)
+        return this
+    }
+
+    fun build(): BannerSpec {
+        return BannerSpec(message, positiveBtn, negativeBtn, icon)
+    }
+}
+
+
+
+fun buildBanner(context: Context, buildSpec: BannerBuilderDsl.() -> Unit): BannerSpec {
+    val dsl = BannerBuilderDsl(context)
+    dsl.buildSpec()
+    return dsl.build()
+}
+
+class BannerBuilderDsl internal constructor(
+    private val context: Context
+) {
+
+    var message: String = ""
+    var messageId = 0
+    var icon: IconCompat? = null
+
+    private var positiveBtn: ButtonSpec? = null
+    private var negativeBtn: ButtonSpec? = null
+
+    fun setPositiveButton(@StringRes textId: Int, onClickListener: (View) -> Unit) {
+        val text = context.getString(textId)
+        setPositiveButton(text, onClickListener)
+    }
+
+    fun setPositiveButton(text: String, onClickListener: (View) -> Unit) {
+        positiveBtn = ButtonSpec(text,
+            View.OnClickListener(onClickListener))
+    }
+
+    fun setNegativeButton(@StringRes textId: Int, onClickListener: (View) -> Unit) {
+        val text = context.getString(textId)
+        setNegativeButton(text, onClickListener)
+    }
+
+    fun setNegativeButton(text: String, onClickListener: (View) -> Unit) {
+        negativeBtn = ButtonSpec(text,
+            View.OnClickListener(onClickListener))
+    }
+
+    internal fun build(): BannerSpec {
+        val msg = if (messageId != 0) context.getString(messageId) else message
+        return BannerSpec(msg, positiveBtn, negativeBtn, icon)
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/geekdroid/src/main/java/com/geekorum/geekdroid/views/behaviors/NestedCoordinatorLayout.java	Fri May 08 21:33:19 2020 -0400
@@ -0,0 +1,136 @@
+/**
+ * Geekdroid is a utility library for development on the Android
+ * Platform.
+ *
+ * Copyright (C) 2017-2020 by Frederic-Charles Barthelery.
+ *
+ * This file is part of Geekdroid.
+ *
+ * Geekdroid is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Geekdroid is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Geekdroid.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.geekorum.geekdroid.views.behaviors;
+
+import android.content.Context;
+import androidx.coordinatorlayout.widget.CoordinatorLayout;
+import androidx.core.view.NestedScrollingChild;
+import androidx.core.view.NestedScrollingChildHelper;
+import android.util.AttributeSet;
+import android.view.View;
+
+/**
+ * A CoordinatorLayout that support nested scrolling.
+ */
+public class NestedCoordinatorLayout extends CoordinatorLayout implements NestedScrollingChild {
+    private final NestedScrollingChildHelper nestedScrollingChildHelper;
+
+    public NestedCoordinatorLayout(Context context) {
+        super(context);
+        nestedScrollingChildHelper = new NestedScrollingChildHelper(this);
+        setNestedScrollingEnabled(true);
+    }
+
+    public NestedCoordinatorLayout(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        nestedScrollingChildHelper = new NestedScrollingChildHelper(this);
+        setNestedScrollingEnabled(true);
+    }
+
+    public NestedCoordinatorLayout(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        nestedScrollingChildHelper = new NestedScrollingChildHelper(this);
+        setNestedScrollingEnabled(true);
+    }
+
+    @Override
+    public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
+        return nestedScrollingChildHelper.dispatchNestedFling(velocityX, velocityY, consumed);
+    }
+
+    @Override
+    public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
+        return nestedScrollingChildHelper.dispatchNestedPreFling(velocityX, velocityY);
+    }
+
+    @Override
+    public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
+        return nestedScrollingChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
+    }
+
+    @Override
+    public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) {
+        return nestedScrollingChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow);
+    }
+
+    @Override
+    public boolean hasNestedScrollingParent() {
+        return nestedScrollingChildHelper.hasNestedScrollingParent();
+    }
+
+    @Override
+    public boolean isNestedScrollingEnabled() {
+        return nestedScrollingChildHelper.isNestedScrollingEnabled();
+    }
+
+    @Override
+    public void setNestedScrollingEnabled(boolean enabled) {
+        nestedScrollingChildHelper.setNestedScrollingEnabled(enabled);
+    }
+
+    @Override
+    public boolean startNestedScroll(int axes) {
+        return nestedScrollingChildHelper.startNestedScroll(axes);
+    }
+
+    @Override
+    public void stopNestedScroll() {
+        nestedScrollingChildHelper.stopNestedScroll();
+    }
+
+    @Override
+    public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) {
+        super.onNestedScrollAccepted(child, target, nestedScrollAxes);
+        startNestedScroll(nestedScrollAxes);
+    }
+
+    @Override
+    public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
+        super.onNestedScroll(target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed);
+        dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, null);
+    }
+
+    @Override
+    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
+        super.onNestedPreScroll(target, dx, dy, consumed);
+        dispatchNestedPreScroll(dx, dy, consumed, null);
+    }
+
+    @Override
+    public void onStopNestedScroll(View target) {
+        super.onStopNestedScroll(target);
+        stopNestedScroll();
+    }
+
+    @Override
+    public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
+        dispatchNestedPreFling(velocityX, velocityY);
+        return super.onNestedPreFling(target, velocityX, velocityY);
+    }
+
+    @Override
+    public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
+        dispatchNestedFling(velocityX, velocityY, consumed);
+        return super.onNestedFling(target, velocityX, velocityY, consumed);
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/geekdroid/src/main/java/com/geekorum/geekdroid/views/behaviors/ScrollAwareFABBehavior.java	Fri May 08 21:33:19 2020 -0400
@@ -0,0 +1,75 @@
+/**
+ * Geekdroid is a utility library for development on the Android
+ * Platform.
+ *
+ * Copyright (C) 2017-2020 by Frederic-Charles Barthelery.
+ *
+ * This file is part of Geekdroid.
+ *
+ * Geekdroid is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Geekdroid is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Geekdroid.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.geekorum.geekdroid.views.behaviors;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import androidx.coordinatorlayout.widget.CoordinatorLayout;
+import com.google.android.material.floatingactionbutton.FloatingActionButton;
+import androidx.core.view.ViewCompat;
+import android.util.AttributeSet;
+import android.view.View;
+
+/**
+ * A behavior for FAB that goes away when the view is scrolling
+ */
+public class ScrollAwareFABBehavior extends FloatingActionButton.Behavior {
+
+    // Workaround for bug https://issuetracker.google.com/issues/37133586
+    private final FloatingActionButton.OnVisibilityChangedListener onVisibilityChangedListener =
+            new FloatingActionButton.OnVisibilityChangedListener() {
+
+                @Override
+                @SuppressLint("RestrictedApi")
+                public void onHidden(FloatingActionButton fab) {
+                    fab.setVisibility(View.INVISIBLE);
+                }
+            };
+
+    public ScrollAwareFABBehavior() {
+        super();
+    }
+
+    public ScrollAwareFABBehavior(Context context, AttributeSet attrs) {
+        super();
+    }
+
+    @Override
+    public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, FloatingActionButton child,
+                                       View directTargetChild, View target, int nestedScrollAxes) {
+        return nestedScrollAxes == ViewCompat.SCROLL_AXIS_VERTICAL
+                || super.onStartNestedScroll(coordinatorLayout, child, directTargetChild, target, nestedScrollAxes);
+    }
+
+    @Override
+    public void onNestedScroll(CoordinatorLayout coordinatorLayout, FloatingActionButton child, View target,
+                               int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
+        super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed);
+        if (dyConsumed > 0 && child.getVisibility() == View.VISIBLE) {
+            // User scrolled down and the FAB is currently visible -> hide the FAB
+            child.hide(onVisibilityChangedListener);
+        } else if (dyConsumed < 0 && child.getVisibility() != View.VISIBLE) {
+            // User scrolled up and the FAB is currently not visible -> show the FAB
+            child.show();
+        }
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/geekdroid/src/main/java/com/geekorum/geekdroid/views/behaviors/SwingBottomItemAnimator.java	Fri May 08 21:33:19 2020 -0400
@@ -0,0 +1,63 @@
+/**
+ * Geekdroid is a utility library for development on the Android
+ * Platform.
+ *
+ * Copyright (C) 2017-2020 by Frederic-Charles Barthelery.
+ *
+ * This file is part of Geekdroid.
+ *
+ * Geekdroid is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Geekdroid is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Geekdroid.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.geekorum.geekdroid.views.behaviors;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import androidx.recyclerview.widget.DefaultItemAnimator;
+import androidx.recyclerview.widget.RecyclerView;
+import android.view.animation.DecelerateInterpolator;
+
+/**
+ * Simple animator that make items scroll from the bottom of the RecyclerView.
+ */
+public class SwingBottomItemAnimator extends DefaultItemAnimator {
+
+    private RecyclerView recyclerView;
+
+    public SwingBottomItemAnimator(RecyclerView recyclerView) {
+        this.recyclerView = recyclerView;
+    }
+
+    @Override
+    public boolean animateAdd(RecyclerView.ViewHolder holder) {
+        runEnterAnimation(holder);
+        return true;
+    }
+
+    private void runEnterAnimation(final RecyclerView.ViewHolder holder) {
+        final int containerHeight =  recyclerView.getHeight();
+        holder.itemView.setTranslationY(containerHeight);
+        holder.itemView.animate()
+                .translationY(0)
+                .setInterpolator(new DecelerateInterpolator(3.f))
+                .setDuration(700)
+                .setListener(new AnimatorListenerAdapter() {
+                    @Override
+                    public void onAnimationEnd(Animator animation) {
+                        dispatchAddFinished(holder);
+                    }
+                })
+                .start();
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/geekdroid/src/main/java/com/geekorum/geekdroid/views/recyclerview/FirstLayoutItemAnimator.java	Fri May 08 21:33:19 2020 -0400
@@ -0,0 +1,67 @@
+/**
+ * Geekdroid is a utility library for development on the Android
+ * Platform.
+ *
+ * Copyright (C) 2017-2020 by Frederic-Charles Barthelery.
+ *
+ * This file is part of Geekdroid.
+ *
+ * Geekdroid is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Geekdroid is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Geekdroid.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.geekorum.geekdroid.views.recyclerview;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.recyclerview.widget.DefaultItemAnimator;
+import androidx.recyclerview.widget.RecyclerView;
+
+/**
+ * An {@link RecyclerView.ItemAnimator} that only run for the 1st layout.
+ */
+public abstract class FirstLayoutItemAnimator extends DefaultItemAnimator {
+
+    private final RecyclerView recyclerView;
+    private RecyclerView.ItemAnimator nextItemAnimator;
+
+    public FirstLayoutItemAnimator(final RecyclerView recyclerView, final RecyclerView.ItemAnimator nextItemAnimator) {
+        this.recyclerView = recyclerView;
+        this.nextItemAnimator = nextItemAnimator;
+    }
+
+    @Override
+    public boolean animateAppearance(@NonNull RecyclerView.ViewHolder viewHolder, @Nullable ItemHolderInfo preLayoutInfo, @NonNull ItemHolderInfo postLayoutInfo) {
+        boolean result = shouldAnimateView(viewHolder);
+        if (result) {
+            result = animateAdd(viewHolder, postLayoutInfo);
+        }
+
+        // set listener to change ItemAnimator when finished
+        if (result) {
+            isRunning(() -> {
+                if (recyclerView.getItemAnimator() == FirstLayoutItemAnimator.this) {
+                    recyclerView.setItemAnimator(nextItemAnimator);
+                }
+            });
+        } else {
+            dispatchAnimationFinished(viewHolder);
+        }
+        return result;
+    }
+
+    protected boolean shouldAnimateView(@NonNull RecyclerView.ViewHolder viewHolder) {
+        return true;
+    }
+
+    protected abstract boolean animateAdd(@NonNull RecyclerView.ViewHolder viewHolder, @NonNull ItemHolderInfo layoutInfo);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/geekdroid/src/main/java/com/geekorum/geekdroid/views/recyclerview/ItemSwiper.java	Fri May 08 21:33:19 2020 -0400
@@ -0,0 +1,110 @@
+/**
+ * Geekdroid is a utility library for development on the Android
+ * Platform.
+ *
+ * Copyright (C) 2017-2020 by Frederic-Charles Barthelery.
+ *
+ * This file is part of Geekdroid.
+ *
+ * Geekdroid is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Geekdroid is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Geekdroid.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.geekorum.geekdroid.views.recyclerview;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.ItemTouchHelper;
+import androidx.recyclerview.widget.RecyclerView;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Helper class to implement a swipe behavior on a RecyclerView.
+ */
+public abstract class ItemSwiper {
+    private final Callback callback;
+    private final ItemTouchHelper itemTouchHelper;
+
+    @IntDef(flag = true, value = {
+            ItemTouchHelper.START,
+            ItemTouchHelper.END,
+            ItemTouchHelper.LEFT,
+            ItemTouchHelper.RIGHT,
+            ItemTouchHelper.UP,
+            ItemTouchHelper.DOWN,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    @interface SwipeDirsFlags {
+    }
+
+    public ItemSwiper(@SwipeDirsFlags int swipeDirs) {
+        callback = new Callback(swipeDirs);
+        itemTouchHelper = new ItemTouchHelper(callback);
+    }
+
+    protected void setOnSwipingListener(Callback.OnSwipingItemListener listener) {
+        callback.setOnSwipingItemListener(listener);
+    }
+
+    public void attachToRecyclerView(RecyclerView recyclerView) {
+        itemTouchHelper.attachToRecyclerView(recyclerView);
+    }
+
+    /**
+     * Called when a ViewHolder is swiped by the user.
+     *
+     * @see ItemTouchHelper.Callback#onSwiped(RecyclerView.ViewHolder, int)
+     */
+    public abstract void onSwiped(RecyclerView.ViewHolder viewHolder, int direction);
+
+    /**
+     * @see ItemTouchHelper.Callback#getSwipeThreshold(RecyclerView.ViewHolder)
+     */
+    public float getSwipeThreshold(RecyclerView.ViewHolder viewHolder) {
+        return 0.5f;
+    }
+
+    /**
+     * Returns false if this viewHolder is not allowed to swipe.
+     * Default is true
+     */
+    protected boolean isViewAllowedToSwipe(@NonNull RecyclerView.ViewHolder viewHolder) {
+        return true;
+    }
+
+    private class Callback extends SingleItemSwipedCallback {
+
+        Callback(int swipeDirs) {
+            super(swipeDirs);
+        }
+
+        @Override
+        public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
+            ItemSwiper.this.onSwiped(viewHolder, direction);
+        }
+
+        @Override
+        public float getSwipeThreshold(RecyclerView.ViewHolder viewHolder) {
+            return ItemSwiper.this.getSwipeThreshold(viewHolder);
+        }
+
+        @Override
+        public int getSwipeDirs(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
+            if (isViewAllowedToSwipe(viewHolder)) {
+                return super.getSwipeDirs(recyclerView, viewHolder);
+            }
+            return 0;
+        }
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/geekdroid/src/main/java/com/geekorum/geekdroid/views/recyclerview/ScrollFromBottomAppearanceItemAnimator.java	Fri May 08 21:33:19 2020 -0400
@@ -0,0 +1,44 @@
+/**
+ * Geekdroid is a utility library for development on the Android
+ * Platform.
+ *
+ * Copyright (C) 2017-2020 by Frederic-Charles Barthelery.
+ *
+ * This file is part of Geekdroid.
+ *
+ * Geekdroid is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Geekdroid is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Geekdroid.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.geekorum.geekdroid.views.recyclerview;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView;
+
+/**
+ * ItemAnimator to scroll items from bottom of the layout on first appearance.
+ */
+public class ScrollFromBottomAppearanceItemAnimator extends FirstLayoutItemAnimator  {
+    private RecyclerView.LayoutManager layoutManager;
+
+    public ScrollFromBottomAppearanceItemAnimator(RecyclerView recyclerView, RecyclerView.ItemAnimator nextItemAnimator) {
+        super(recyclerView, nextItemAnimator);
+        this.layoutManager = recyclerView.getLayoutManager();
+        setMoveDuration(450);
+    }
+
+    @Override
+    public boolean animateAdd(@NonNull RecyclerView.ViewHolder viewHolder, @NonNull ItemHolderInfo layoutInfo) {
+        return animateMove(viewHolder, 0, layoutManager.getHeight() + viewHolder.itemView.getHeight()
+                * viewHolder.getAdapterPosition(), layoutInfo.left, layoutInfo.top);
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/geekdroid/src/main/java/com/geekorum/geekdroid/views/recyclerview/SingleItemSwipedCallback.java	Fri May 08 21:33:19 2020 -0400
@@ -0,0 +1,79 @@
+/**
+ * Geekdroid is a utility library for development on the Android
+ * Platform.
+ *
+ * Copyright (C) 2017-2020 by Frederic-Charles Barthelery.
+ *
+ * This file is part of Geekdroid.
+ *
+ * Geekdroid is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Geekdroid is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Geekdroid.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.geekorum.geekdroid.views.recyclerview;
+
+import androidx.annotation.Nullable;
+import androidx.recyclerview.widget.ItemTouchHelper;
+import androidx.recyclerview.widget.RecyclerView;
+
+/**
+ * An {@link ItemTouchHelper.Callback} that help to apply a {@link RecyclerView.ItemDecoration}
+ * on the item swiped.
+ */
+public abstract class SingleItemSwipedCallback extends ItemTouchHelper.SimpleCallback {
+
+    private RecyclerView.ViewHolder swipingItem;
+    private OnSwipingItemListener listener;
+
+    public SingleItemSwipedCallback(int swipeDirs) {
+        super(0, swipeDirs);
+    }
+
+    @Override
+    public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
+        return false;
+    }
+
+    @Override
+    public void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState) {
+        super.onSelectedChanged(viewHolder, actionState);
+        if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) {
+            swipingItem = viewHolder;
+            if (listener != null) {
+                listener.onSwipingItem(swipingItem);
+            }
+        }
+    }
+
+    @Override
+    public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
+        super.clearView(recyclerView, viewHolder);
+        if (swipingItem == viewHolder) {
+            swipingItem = null;
+            if (listener != null) {
+                listener.onSwipingItem(null);
+            }
+        }
+    }
+
+    public void setOnSwipingItemListener(OnSwipingItemListener listener) {
+        this.listener = listener;
+    }
+
+    /**
+     * Listener to get the swiping item.
+     */
+    public interface OnSwipingItemListener {
+        void onSwipingItem(@Nullable RecyclerView.ViewHolder item);
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/geekdroid/src/main/java/com/geekorum/geekdroid/views/recyclerview/SpacingItemDecoration.java	Fri May 08 21:33:19 2020 -0400
@@ -0,0 +1,57 @@
+/**
+ * Geekdroid is a utility library for development on the Android
+ * Platform.
+ *
+ * Copyright (C) 2017-2020 by Frederic-Charles Barthelery.
+ *
+ * This file is part of Geekdroid.
+ *
+ * Geekdroid is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Geekdroid is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Geekdroid.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.geekorum.geekdroid.views.recyclerview;
+
+import android.graphics.Rect;
+import android.view.View;
+import androidx.recyclerview.widget.RecyclerView;
+
+/**
+ * {@link RecyclerView.ItemDecoration} that applies a
+ * vertical and horizontal spacing between items of the target
+ * {@link RecyclerView}.
+ */
+public class SpacingItemDecoration extends RecyclerView.ItemDecoration {
+    private final int verticalSpacing;
+    private final int horizontalSpacing;
+
+    public SpacingItemDecoration(int verticalSpacing, int horizontalSpacing) {
+        this.verticalSpacing = verticalSpacing;
+        this.horizontalSpacing = horizontalSpacing;
+    }
+
+    @Override
+    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
+        outRect.bottom = verticalSpacing / 2;
+        outRect.top = verticalSpacing / 2;
+        outRect.left = horizontalSpacing / 2;
+        outRect.right = horizontalSpacing / 2;
+    }
+
+    public int getVerticalSpacing() {
+        return verticalSpacing;
+    }
+
+    public int getHorizontalSpacing() {
+        return horizontalSpacing;
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/geekdroid/src/main/java/com/geekorum/geekdroid/views/recyclerview/ViewItemDecoration.java	Fri May 08 21:33:19 2020 -0400
@@ -0,0 +1,119 @@
+/**
+ * Geekdroid is a utility library for development on the Android
+ * Platform.
+ *
+ * Copyright (C) 2017-2020 by Frederic-Charles Barthelery.
+ *
+ * This file is part of Geekdroid.
+ *
+ * Geekdroid is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Geekdroid is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Geekdroid.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.geekorum.geekdroid.views.recyclerview;
+
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.view.View;
+import android.widget.FrameLayout;
+import androidx.recyclerview.widget.RecyclerView;
+
+/**
+ * A {@link RecyclerView.ItemDecoration} that draws {@link View} for decoration.
+ */
+public class ViewItemDecoration extends RecyclerView.ItemDecoration {
+
+    private final Rect bounds = new Rect();
+    private FrameLayout root;
+    private ViewPainter drawPainter;
+    private ViewPainter drawOverPainter;
+
+    @Override
+    public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
+        if (drawPainter == null) {
+            return;
+        }
+        drawPainter.draw(c, parent, state);
+    }
+
+    @Override
+    public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
+        if (drawOverPainter == null) {
+            return;
+        }
+        drawOverPainter.draw(c, parent, state);
+    }
+
+    protected void setDrawOverPainter(ViewPainter drawOverPainter) {
+        this.drawOverPainter = drawOverPainter;
+    }
+
+    protected void setDrawPainter(ViewPainter viewPainter) {
+        this.drawPainter = viewPainter;
+    }
+
+    /**
+     * The artist that paint the view as a decoration.
+     */
+    protected abstract class ViewPainter {
+
+        private void prepareRoot(RecyclerView parent) {
+            if (root == null) {
+                root = new FrameLayout(parent.getContext());
+            }
+            bounds.setEmpty();
+            root.removeAllViews();
+        }
+
+        private void layoutRoot() {
+            int widthSpec = View.MeasureSpec.makeMeasureSpec(bounds.width(), View.MeasureSpec.EXACTLY);
+            int heightSpec = View.MeasureSpec.makeMeasureSpec(bounds.height(), View.MeasureSpec.EXACTLY);
+            if (!root.canResolveLayoutDirection()) {
+                // as this view is never attached to a window set it layout direction explicitly
+                root.setLayoutDirection(View.LAYOUT_DIRECTION_LOCALE);
+            }
+            root.measure(widthSpec, heightSpec);
+            root.layout(bounds.left, bounds.top, bounds.right, bounds.bottom);
+        }
+
+        private void draw(Canvas c, RecyclerView parent, RecyclerView.State state) {
+            c.save();
+            View view = drawPainter.getView(parent, state);
+            if (view != null) {
+                prepareRoot(parent);
+                root.addView(view, FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT);
+                computeDrawingBounds(parent, state, bounds);
+                layoutRoot();
+                c.translate(bounds.left, bounds.top);
+                root.draw(c);
+            }
+            c.restore();
+        }
+
+        /**
+         * Compute the bounds of the drawing area.
+         * @param parent RecyclerView this ItemDecoration is drawing into
+         * @param state state of parent
+         * @param outBounds output bounds
+         */
+        protected abstract void computeDrawingBounds(RecyclerView parent, RecyclerView.State state, Rect outBounds);
+
+        /**
+         * Get the view to draw as a decoration.
+         * @param parent RecyclerView this ItemDecoration is drawing into
+         * @param state state of parent
+         * @return the view
+         */
+        protected abstract View getView(RecyclerView parent, RecyclerView.State state);
+
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/geekdroid/src/main/res/drawable/view_account_menu_line_handle_closed.xml	Fri May 08 21:33:19 2020 -0400
@@ -0,0 +1,37 @@
+<!--
+
+    Geekdroid is a utility library for development on the Android
+    Platform.
+
+    Copyright (C) 2017-2020 by Frederic-Charles Barthelery.
+
+    This file is part of Geekdroid.
+
+    Geekdroid is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    Geekdroid is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with Geekdroid.  If not, see <http://www.gnu.org/licenses/>.
+
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="16dp"
+        android:height="16dp"
+        android:viewportWidth="30.0"
+        android:viewportHeight="30.0" >
+    <group android:name="rotation"
+           android:pivotX="15"
+           android:pivotY="20"
+           android:rotation="0">
+        <path android:name="triangle"
+              android:fillColor="?colorControlNormal"
+              android:pathData="M5,15 L25,15 L15,25 L5,15 z"/>
+    </group>
+</vector>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/geekdroid/src/main/res/drawable/view_account_menu_line_handle_open.xml	Fri May 08 21:33:19 2020 -0400
@@ -0,0 +1,37 @@
+<!--
+
+    Geekdroid is a utility library for development on the Android
+    Platform.
+
+    Copyright (C) 2017-2020 by Frederic-Charles Barthelery.
+
+    This file is part of Geekdroid.
+
+    Geekdroid is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    Geekdroid is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with Geekdroid.  If not, see <http://www.gnu.org/licenses/>.
+
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="16dp"
+        android:height="16dp"
+        android:viewportWidth="30.0"
+        android:viewportHeight="30.0" >
+    <group android:name="rotation"
+           android:pivotX="15"
+           android:pivotY="20"
+           android:rotation="180">
+        <path android:name="triangle"
+              android:fillColor="?colorControlNormal"
+              android:pathData="M5,15 L25,15 L15,25 L5,15 z"/>
+    </group>
+</vector>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/geekdroid/src/main/res/drawable/view_account_menu_line_handle_selector.xml	Fri May 08 21:33:19 2020 -0400
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+
+    Geekdroid is a utility library for development on the Android
+    Platform.
+
+    Copyright (C) 2017-2020 by Frederic-Charles Barthelery.
+
+    This file is part of Geekdroid.
+
+    Geekdroid is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    Geekdroid is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with Geekdroid.  If not, see <http://www.gnu.org/licenses/>.
+
+-->
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <item android:id="@+id/open"
+          android:state_expanded="true"
+          android:drawable="@drawable/view_account_menu_line_handle_open" />
+
+    <item android:id="@+id/closed"
+          android:drawable="@drawable/view_account_menu_line_handle_closed"/>
+
+</selector>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/geekdroid/src/main/res/layout/activity_bottom_sheet_dialog.xml	Fri May 08 21:33:19 2020 -0400
@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+
+    Geekdroid is a utility library for development on the Android
+    Platform.
+
+    Copyright (C) 2017-2020 by Frederic-Charles Barthelery.
+
+    This file is part of Geekdroid.
+
+    Geekdroid is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    Geekdroid is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with Geekdroid.  If not, see <http://www.gnu.org/licenses/>.
+
+-->
+<layout xmlns:android="http://schemas.android.com/apk/res/android"
+        xmlns:app="http://schemas.android.com/apk/res-auto"
+        xmlns:tools="http://schemas.android.com/tools">
+
+    <FrameLayout android:layout_width="match_parent"
+                 android:layout_height="match_parent"
+                 android:fitsSystemWindows="true">
+
+        <androidx.coordinatorlayout.widget.CoordinatorLayout
+                android:id="@+id/coordinator"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"
+                android:fitsSystemWindows="true">
+
+            <View android:id="@+id/touch_outside"
+                  android:layout_width="match_parent"
+                  android:layout_height="match_parent"
+                  android:importantForAccessibility="no"
+                  android:soundEffectsEnabled="false"
+                  tools:ignore="UnusedAttribute" />
+
+            <FrameLayout
+                    android:id="@+id/bottom_sheet"
+                    style="?attr/bottomSheetStyle"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:layout_gravity="center_horizontal|top"
+                    app:layout_behavior="@string/bottom_sheet_behavior" />
+
+        </androidx.coordinatorlayout.widget.CoordinatorLayout>
+
+    </FrameLayout>
+
+</layout>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/geekdroid/src/main/res/layout/view_account_menu_line.xml	Fri May 08 21:33:19 2020 -0400
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+
+    Geekdroid is a utility library for development on the Android
+    Platform.
+
+    Copyright (C) 2017-2020 by Frederic-Charles Barthelery.
+
+    This file is part of Geekdroid.
+
+    Geekdroid is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    Geekdroid is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with Geekdroid.  If not, see <http://www.gnu.org/licenses/>.
+
+-->
+<merge
+        android:id="@+id/select_account"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="horizontal" xmlns:android="http://schemas.android.com/apk/res/android"
+        xmlns:tools="http://schemas.android.com/tools">
+
+    <TextView android:id="@+id/account"
+              android:layout_width="match_parent"
+              android:layout_height="wrap_content"
+              android:layout_gravity="bottom"
+              android:layout_weight="1"
+              android:textAppearance="@style/TextAppearance.AppCompat.Body1"
+              tools:text="Account name"/>
+
+    <ImageView android:id="@+id/handle"
+               android:layout_width="wrap_content"
+               android:layout_height="wrap_content"
+               android:layout_weight="0"
+               android:duplicateParentState="true"
+               android:src="@drawable/view_account_menu_line_handle_selector"
+               android:importantForAccessibility="no"
+               />
+</merge>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/geekdroid/src/main/res/layout/view_banner_extended.xml	Fri May 08 21:33:19 2020 -0400
@@ -0,0 +1,90 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+
+    Geekdroid is a utility library for development on the Android
+    Platform.
+
+    Copyright (C) 2017-2020 by Frederic-Charles Barthelery.
+
+    This file is part of Geekdroid.
+
+    Geekdroid is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    Geekdroid is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with Geekdroid.  If not, see <http://www.gnu.org/licenses/>.
+
+-->
+<layout xmlns:android="http://schemas.android.com/apk/res/android"
+        xmlns:app="http://schemas.android.com/apk/res-auto"
+        xmlns:tools="http://schemas.android.com/tools">
+
+    <data>
+
+    </data>
+
+    <androidx.constraintlayout.widget.ConstraintLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:fitsSystemWindows="true"
+            >
+
+        <ImageView
+                android:id="@+id/icon"
+                android:layout_width="40dp"
+                android:layout_height="40dp"
+                android:layout_marginStart="16dp"
+                android:layout_marginTop="24dp"
+                app:layout_constraintTop_toTopOf="parent"
+                app:layout_constraintStart_toStartOf="parent"
+                tools:src="@tools:sample/avatars"
+                />
+
+        <TextView
+                android:id="@+id/message"
+                android:layout_width="0dp"
+                android:layout_height="wrap_content"
+                android:layout_marginEnd="16dp"
+                android:layout_marginStart="16dp"
+                android:layout_marginTop="24dp"
+                app:layout_constraintEnd_toEndOf="parent"
+                app:layout_constraintStart_toEndOf="@id/icon"
+                app:layout_constraintTop_toTopOf="parent"
+                android:textAppearance="?textAppearanceBody2"
+                tools:text="Your password was updated on your other device. Please sign in again" />
+
+        <com.google.android.material.button.MaterialButton
+                android:id="@+id/positiveBtn"
+                style="@style/Widget.MaterialComponents.Button.TextButton.Dialog"
+                android:layout_width="wrap_content"
+                android:layout_height="36dp"
+                android:layout_marginTop="12dp"
+                android:layout_marginBottom="8dp"
+                android:layout_marginEnd="8dp"
+                tools:text="Sign In"
+                app:layout_constraintTop_toBottomOf="@id/message"
+                app:layout_constraintBottom_toBottomOf="parent"
+                app:layout_constraintEnd_toEndOf="parent"
+                />
+
+        <com.google.android.material.button.MaterialButton
+                android:id="@+id/negativeBtn"
+                style="@style/Widget.MaterialComponents.Button.TextButton.Dialog"
+                android:layout_width="wrap_content"
+                android:layout_height="36dp"
+                android:layout_marginTop="12dp"
+                android:layout_marginEnd="8dp"
+                tools:text="Continue as a guest"
+                app:layout_constraintTop_toBottomOf="@id/message"
+                app:layout_constraintEnd_toStartOf="@id/positiveBtn"
+                />
+
+    </androidx.constraintlayout.widget.ConstraintLayout>
+</layout>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/geekdroid/src/main/res/layout/view_banner_simple.xml	Fri May 08 21:33:19 2020 -0400
@@ -0,0 +1,68 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+
+    Geekdroid is a utility library for development on the Android
+    Platform.
+
+    Copyright (C) 2017-2020 by Frederic-Charles Barthelery.
+
+    This file is part of Geekdroid.
+
+    Geekdroid is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    Geekdroid is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with Geekdroid.  If not, see <http://www.gnu.org/licenses/>.
+
+-->
+<layout xmlns:android="http://schemas.android.com/apk/res/android"
+        xmlns:app="http://schemas.android.com/apk/res-auto"
+        xmlns:tools="http://schemas.android.com/tools">
+
+    <data>
+
+    </data>
+
+    <androidx.constraintlayout.widget.ConstraintLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:fitsSystemWindows="true"
+            >
+
+        <TextView
+                android:id="@+id/message"
+                android:layout_width="0dp"
+                android:layout_height="wrap_content"
+                android:layout_marginEnd="36dp"
+                android:layout_marginStart="16dp"
+                app:layout_constraintEnd_toStartOf="@+id/positiveBtn"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintTop_toTopOf="parent"
+                app:layout_constraintBottom_toBottomOf="parent"
+                android:textAppearance="?textAppearanceBody2"
+                android:singleLine="true"
+                tools:text="A new update is available" />
+
+        <com.google.android.material.button.MaterialButton
+                android:id="@+id/positiveBtn"
+                style="@style/Widget.MaterialComponents.Button.TextButton.Dialog"
+                android:layout_width="wrap_content"
+                android:layout_height="36dp"
+                android:layout_marginTop="10dp"
+                android:layout_marginBottom="8dp"
+                android:layout_marginEnd="8dp"
+                tools:text="Install"
+                app:layout_constraintTop_toTopOf="parent"
+                app:layout_constraintBottom_toBottomOf="parent"
+                app:layout_constraintEnd_toEndOf="parent"
+                />
+    </androidx.constraintlayout.widget.ConstraintLayout>
+
+</layout>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/geekdroid/src/main/res/values/attrs.xml	Fri May 08 21:33:19 2020 -0400
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+
+    Geekdroid is a utility library for development on the Android
+    Platform.
+
+    Copyright (C) 2017-2020 by Frederic-Charles Barthelery.
+
+    This file is part of Geekdroid.
+
+    Geekdroid is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    Geekdroid is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with Geekdroid.  If not, see <http://www.gnu.org/licenses/>.
+
+-->
+<resources>
+    <declare-styleable name="CheckableImageView" >
+        <attr name="android:checked" />
+    </declare-styleable>
+    <declare-styleable name="GeekdroidTheme">
+        <attr name="checkableImageViewStyle" format="reference"/>
+    </declare-styleable>
+
+    <declare-styleable name="DialogNavigator">
+        <attr name="android:name"/>
+    </declare-styleable>
+</resources>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/geekdroid/src/main/res/values/ids.xml	Fri May 08 21:33:19 2020 -0400
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+
+    Geekdroid is a utility library for development on the Android
+    Platform.
+
+    Copyright (C) 2017-2020 by Frederic-Charles Barthelery.
+
+    This file is part of Geekdroid.
+
+    Geekdroid is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    Geekdroid is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with Geekdroid.  If not, see <http://www.gnu.org/licenses/>.
+
+-->
+<resources>
+    <item name="onPageSelectedListener" type="id"/>
+</resources>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/geekdroid/src/main/res/values/strings.xml	Fri May 08 21:33:19 2020 -0400
@@ -0,0 +1,27 @@
+<!--
+
+    Geekdroid is a utility library for development on the Android
+    Platform.
+
+    Copyright (C) 2017-2020 by Frederic-Charles Barthelery.
+
+    This file is part of Geekdroid.
+
+    Geekdroid is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    Geekdroid is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with Geekdroid.  If not, see <http://www.gnu.org/licenses/>.
+
+-->
+<resources>
+    <string name="geekdroid_app_name">Geekdroid</string>
+    <string name="geekdroid_pref_ringtone_silent">Silent</string>
+</resources>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/geekdroid/src/main/res/values/styles.xml	Fri May 08 21:33:19 2020 -0400
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+
+    Geekdroid is a utility library for development on the Android
+    Platform.
+
+    Copyright (C) 2017-2020 by Frederic-Charles Barthelery.
+
+    This file is part of Geekdroid.
+
+    Geekdroid is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    Geekdroid is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with Geekdroid.  If not, see <http://www.gnu.org/licenses/>.
+
+-->
+<resources>
+    <style name="Widget.Geekdroid" parent="android:Widget"/>
+    <style name="Widget.Geekdroid.CheckableImageView">
+        <item name="android:clickable">true</item>
+        <item name="android:focusable">true</item>
+        <item name="android:gravity">center</item>
+    </style>
+
+    <style name="Theme.Geekdroid.BottomSheetDialogActivity" parent="@style/Theme.Design.BottomSheetDialog">
+        <item name="windowNoTitle">true</item>
+        <item name="android:windowDrawsSystemBarBackgrounds">true</item>
+        <item name="android:windowTranslucentStatus">false</item>
+    </style>
+
+    <style name="Theme.Geekdroid.Light.BottomSheetDialogActivity" parent="@style/Theme.Design.Light.BottomSheetDialog">
+        <item name="windowNoTitle">true</item>
+        <item name="android:windowDrawsSystemBarBackgrounds">true</item>
+        <item name="android:windowTranslucentStatus">false</item>
+    </style>
+
+</resources>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/geekdroid/src/test/java/com/geekorum/geekdroid/accounts/AccountsLiveDataTest.java	Fri May 08 21:33:19 2020 -0400
@@ -0,0 +1,132 @@
+/**
+ * Geekdroid is a utility library for development on the Android
+ * Platform.
+ *
+ * Copyright (C) 2017-2020 by Frederic-Charles Barthelery.
+ *
+ * This file is part of Geekdroid.
+ *
+ * Geekdroid is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Geekdroid is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Geekdroid.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.geekorum.geekdroid.accounts;
+
+import android.accounts.Account;
+import android.accounts.AccountManager;
+
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule;
+import androidx.lifecycle.Lifecycle;
+import androidx.lifecycle.Observer;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.geekorum.geekdroid.utils.LifecycleMock;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+import org.mockito.quality.Strictness;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.Shadows;
+import org.robolectric.annotation.Config;
+import org.robolectric.shadows.ShadowAccountManager;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+@RunWith(AndroidJUnit4.class)
+@Config(shadows = {com.geekorum.geekdroid.shadows.ShadowAccountManager.class})
+public class AccountsLiveDataTest {
+
+    @Rule
+    public InstantTaskExecutorRule archComponentRule = new InstantTaskExecutorRule();
+    @Rule
+    public MockitoRule mockitoRule = MockitoJUnit.rule().strictness(Strictness.STRICT_STUBS);
+
+    private static final String TEST_ACCOUNT_TYPE = "test.account";
+
+    private final Account[] testAccounts = new Account[]{new Account("test1", TEST_ACCOUNT_TYPE),
+            new Account("test2", TEST_ACCOUNT_TYPE),
+            new Account("test3", TEST_ACCOUNT_TYPE),
+    };
+    private final Account testAccountMore = new Account("test4 more", TEST_ACCOUNT_TYPE);
+
+    private static final String OTHER_ACCOUNT_TYPE = "other";
+    private final Account otherAccount = new Account("test1", OTHER_ACCOUNT_TYPE);
+    private final Account otherAccount2 = new Account("test2", OTHER_ACCOUNT_TYPE);
+
+    @Mock
+    private Observer<Account[]> mockObserver;
+    private LifecycleMock lifecycleMock = new LifecycleMock();
+    private ShadowAccountManager shadowAccountManager;
+
+    private AccountsLiveData accountsLiveData;
+
+    @Before
+    public void setUp() throws Exception {
+        AccountManager accountManager = AccountManager.get(RuntimeEnvironment.application);
+        shadowAccountManager = Shadows.shadowOf(accountManager);
+        for (Account account : testAccounts) {
+            shadowAccountManager.addAccount(account);
+        }
+        shadowAccountManager.addAccount(otherAccount);
+
+        accountsLiveData = new AccountsLiveData(accountManager, TEST_ACCOUNT_TYPE);
+    }
+
+    @Test
+    public void testWhenActiveGetTheCorrectData() throws Exception {
+        accountsLiveData.observe(lifecycleMock, mockObserver);
+        lifecycleMock.getLifecycle().handleLifecycleEvent(Lifecycle.Event.ON_START);
+        Mockito.verify(mockObserver).onChanged(testAccounts);
+    }
+
+    @Test
+    public void testThatWhenActiveGetCorrectUpdates() throws Exception {
+        accountsLiveData.observe(lifecycleMock, mockObserver);
+        lifecycleMock.getLifecycle().handleLifecycleEvent(Lifecycle.Event.ON_START);
+
+        Mockito.verify(mockObserver).onChanged(testAccounts);
+
+        // add some accounts
+        shadowAccountManager.addAccount(otherAccount2);
+        shadowAccountManager.addAccount(testAccountMore);
+        List<Account> accountList = new ArrayList<>(Arrays.asList(testAccounts));
+        accountList.add(testAccountMore);
+        Account[] newTestAccounts = accountList.toArray(new Account[accountList.size()]);
+
+        Mockito.verify(mockObserver, Mockito.times(1)).onChanged(newTestAccounts);
+    }
+
+    @Test
+    public void testThatWhenInactiveGetLaterUpdates() throws Exception {
+        accountsLiveData.observe(lifecycleMock, mockObserver);
+        lifecycleMock.getLifecycle().handleLifecycleEvent(Lifecycle.Event.ON_STOP);
+
+        // add some accounts
+        shadowAccountManager.addAccount(testAccountMore);
+        shadowAccountManager.addAccount(otherAccount2);
+        List<Account> accountList = new ArrayList<>(Arrays.asList(testAccounts));
+        accountList.add(testAccountMore);
+        Account[] newTestAccounts = accountList.toArray(new Account[accountList.size()]);
+
+        lifecycleMock.getLifecycle().handleLifecycleEvent(Lifecycle.Event.ON_START);
+
+        Mockito.verify(mockObserver, Mockito.times(1)).onChanged(newTestAccounts);
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/geekdroid/src/test/java/com/geekorum/geekdroid/accounts/CancellableSyncAdapterTest.kt	Fri May 08 21:33:19 2020 -0400
@@ -0,0 +1,136 @@
+/**
+ * Geekdroid is a utility library for development on the Android
+ * Platform.
+ *
+ * Copyright (C) 2017-2020 by Frederic-Charles Barthelery.
+ *
+ * This file is part of Geekdroid.
+ *
+ * Geekdroid is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Geekdroid is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Geekdroid.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.geekorum.geekdroid.accounts
+
+import android.accounts.Account
+import android.content.ContentProviderClient
+import android.content.SyncResult
+import android.os.Bundle
+import com.geekorum.geekdroid.accounts.CancellableSyncAdapter.CancellableSync
+import com.google.common.truth.Truth.assertThat
+import io.mockk.mockk
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+import org.junit.Before
+import org.junit.Test
+import java.util.concurrent.TimeUnit
+
+
+class CancellableSyncTest {
+
+    lateinit var sync: MockCancellableSync
+
+    @Before
+    fun setUp() {
+        sync = MockCancellableSync()
+    }
+
+    @Test
+    fun testThatJobCompleteNormally() {
+        runBlocking {
+            sync.sync()
+            assertThat(sync.wasCompleted).isTrue()
+        }
+    }
+
+    @Test
+    fun testThatJobCancelledNormally() {
+        runBlocking {
+            val job = launch { sync.sync() }
+            job.cancel()
+            assertThat(sync.wasCompleted).isFalse()
+        }
+    }
+}
+
+
+class CancellableSyncAdapterTest {
+    lateinit var syncAdapter: MockCancellableSyncAdapter
+
+    @Before
+    fun setUp() {
+        syncAdapter = MockCancellableSyncAdapter()
+    }
+
+
+    @Test
+    fun testThatSyncCompleteNormally() {
+        syncAdapter.syncAction = { delay(50) }
+        syncAdapter.onPerformSync(Account("account", "type"),
+            Bundle(), "authority", mockk(),
+            mockk())
+        assertThat(syncAdapter.sync.wasCompleted).isTrue()
+        assertThat(syncAdapter.sync.wasCancelled).isFalse()
+    }
+
+    @Test
+    fun testThatSyncCancelledNormally() {
+        syncAdapter.syncAction = { delay(TimeUnit.SECONDS.toMillis(100)) }
+        runBlocking {
+            launch(Dispatchers.Default) { // with a new context
+                syncAdapter.onPerformSync(Account("account", "type"),
+                    Bundle(), "authority", mockk(), mockk())
+            }
+            // wait a bit before cancelling
+            delay(1000)
+            syncAdapter.onSyncCanceled()
+        }
+        assertThat(syncAdapter.sync.wasCompleted).isFalse()
+        assertThat(syncAdapter.sync.wasCancelled).isTrue()
+    }
+}
+
+
+class MockCancellableSyncAdapter : CancellableSyncAdapter(mockk(), false, false) {
+    var syncAction: suspend () -> Unit = { delay(50) }
+
+    lateinit var sync: MockCancellableSync
+
+    override fun createCancellableSync(
+        account: Account, extras: Bundle, authority: String, provider: ContentProviderClient,
+        syncResult: SyncResult
+    ): CancellableSync {
+        sync = MockCancellableSync(syncAction)
+        return sync
+    }
+}
+
+
+class MockCancellableSync(
+    private val syncAction: suspend () -> Unit = { delay(50) }
+) : CancellableSync() {
+
+    var wasCancelled = false
+    var wasCompleted = false
+
+    override suspend fun sync() {
+        syncAction()
+        wasCompleted = true
+    }
+
+    override fun onSyncCancelled() {
+        wasCancelled = true
+    }
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/geekdroid/src/test/java/com/geekorum/geekdroid/accounts/SyncInProgressLiveDataTest.java	Fri May 08 21:33:19 2020 -0400
@@ -0,0 +1,115 @@
+/**
+ * Geekdroid is a utility library for development on the Android
+ * Platform.
+ *
+ * Copyright (C) 2017-2020 by Frederic-Charles Barthelery.
+ *
+ * This file is part of Geekdroid.
+ *
+ * Geekdroid is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Geekdroid is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Geekdroid.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.geekorum.geekdroid.accounts;
+
+import android.accounts.Account;
+import android.content.ContentResolver;
+import android.os.Bundle;
+
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule;
+import androidx.lifecycle.Lifecycle;
+import androidx.lifecycle.Observer;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.geekorum.geekdroid.shadows.ShadowContentResolver;
+import com.geekorum.geekdroid.utils.LifecycleMock;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+import org.robolectric.annotation.Config;
+
+@RunWith(AndroidJUnit4.class)
+@Config(shadows = {ShadowContentResolver.class})
+public class SyncInProgressLiveDataTest {
+
+    @Rule
+    public final InstantTaskExecutorRule archComponentRule = new InstantTaskExecutorRule();
+    @Rule
+    public final MockitoRule mockitoRule = MockitoJUnit.rule();
+    private final LifecycleMock lifecycleMock = new LifecycleMock();
+
+    private SyncInProgressLiveData syncInProgressLiveData;
+    @Mock
+    private Observer<Boolean> mockObserver;
+
+    private final Account account = new Account("test", "test");
+
+    private static final String AUTHORITY = "authority";
+
+    @Before
+    public void setUp() throws Exception {
+        syncInProgressLiveData = new SyncInProgressLiveData(account, AUTHORITY);
+    }
+
+    @Test
+    public void testWhenActiveAndNoSyncGetTheCorrectData() throws Exception {
+        syncInProgressLiveData.observe(lifecycleMock, mockObserver);
+        lifecycleMock.getLifecycle().handleLifecycleEvent(Lifecycle.Event.ON_START);
+        Mockito.verify(mockObserver).onChanged(false);
+
+        ContentResolver.requestSync(account, AUTHORITY, new Bundle());
+        Mockito.verify(mockObserver).onChanged(true);
+
+        ContentResolver.cancelSync(account, AUTHORITY);
+        Mockito.verify(mockObserver, Mockito.times(2)).onChanged(false);
+    }
+
+    @Test
+    public void testThatWhenInactiveGetLaterUpdates() throws Exception {
+        syncInProgressLiveData.observe(lifecycleMock, mockObserver);
+
+        // start a sync
+        ContentResolver.requestSync(account, AUTHORITY, new Bundle());
+        lifecycleMock.getLifecycle().handleLifecycleEvent(Lifecycle.Event.ON_START);
+        Mockito.verify(mockObserver, Mockito.times(1)).onChanged(true);
+    }
+
+    @Test
+    public void testThatWhenInactiveGetOnlyCorrectUpdates() throws Exception {
+        syncInProgressLiveData.observe(lifecycleMock, mockObserver);
+
+        // start a sync with another account
+        Account otherAccount = new Account("toto", "toto");
+        ContentResolver.requestSync(otherAccount, AUTHORITY, new Bundle());
+
+        lifecycleMock.getLifecycle().handleLifecycleEvent(Lifecycle.Event.ON_START);
+        Mockito.verify(mockObserver, Mockito.times(1)).onChanged(false);
+    }
+
+    @Test
+    public void testThatWhenGettingInactiveNoMoreUpdates() throws Exception {
+        syncInProgressLiveData.observe(lifecycleMock, mockObserver);
+
+        lifecycleMock.getLifecycle().handleLifecycleEvent(Lifecycle.Event.ON_START);
+        lifecycleMock.getLifecycle().handleLifecycleEvent(Lifecycle.Event.ON_STOP);
+
+        // start a sync
+        ContentResolver.requestSync(account, AUTHORITY, new Bundle());
+        Mockito.verify(mockObserver, Mockito.never()).onChanged(true);
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/geekdroid/src/test/java/com/geekorum/geekdroid/battery/LiveDataTest.kt	Fri May 08 21:33:19 2020 -0400
@@ -0,0 +1,145 @@
+/**
+ * Geekdroid is a utility library for development on the Android
+ * Platform.
+ *
+ * Copyright (C) 2017-2020 by Frederic-Charles Barthelery.
+ *
+ * This file is part of Geekdroid.
+ *
+ * Geekdroid is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Geekdroid is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Geekdroid.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.geekorum.geekdroid.battery
+
+import android.app.Application
+import android.content.Intent
+import android.os.BatteryManager
+import android.os.Build
+import android.os.PowerManager
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule
+import androidx.core.content.getSystemService
+import androidx.lifecycle.Observer
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import io.mockk.mockk
+import io.mockk.verifySequence
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.Shadows
+import org.robolectric.annotation.Config
+import org.robolectric.shadows.ShadowPowerManager
+import kotlin.test.BeforeTest
+
+@RunWith(AndroidJUnit4::class)
+class BatterySaverLiveDataTest {
+
+    lateinit var liveData: BatterySaverLiveData
+    lateinit var shadowPowerManager: ShadowPowerManager
+    lateinit var application: Application
+
+    @get:Rule
+    val rule = InstantTaskExecutorRule()
+
+    @BeforeTest
+    fun setUp() {
+        application = ApplicationProvider.getApplicationContext()
+        val powerManager: PowerManager = application.getSystemService()!!
+        shadowPowerManager = Shadows.shadowOf(powerManager)
+        liveData = BatterySaverLiveData(application, powerManager)
+    }
+
+    @Test
+    fun testThatWhenPowerSaveModeChangedLiveDataIsUpdated() {
+        val mockObserver = mockk<Observer<Boolean>>(relaxed = true)
+        liveData.observeForever(mockObserver)
+        shadowPowerManager.setIsPowerSaveMode(true)
+        application.sendBroadcast(Intent(PowerManager.ACTION_POWER_SAVE_MODE_CHANGED))
+        shadowPowerManager.setIsPowerSaveMode(false)
+        application.sendBroadcast(Intent(PowerManager.ACTION_POWER_SAVE_MODE_CHANGED))
+        verifySequence {
+            mockObserver.onChanged(false)
+            mockObserver.onChanged(true)
+            mockObserver.onChanged(false)
+        }
+    }
+
+}
+
+@RunWith(AndroidJUnit4::class)
+class LowBatteryLiveDataTest {
+
+    lateinit var liveData: LowBatteryLiveData
+    lateinit var application: Application
+
+    @get:Rule
+    val rule = InstantTaskExecutorRule()
+
+    @BeforeTest
+    fun setUp() {
+        application = ApplicationProvider.getApplicationContext()
+        liveData = LowBatteryLiveData(application)
+    }
+
+    @Test
+    fun testThatBatteryGetLowLiveDataIsUpdated() {
+        val mockObserver = mockk<Observer<Boolean>>(relaxed = true)
+        liveData.observeForever(mockObserver)
+        application.sendBroadcast(Intent(Intent.ACTION_BATTERY_LOW))
+        verifySequence {
+            // first battery is okay
+            mockObserver.onChanged(false)
+            mockObserver.onChanged(true)
+        }
+    }
+
+    @Test
+    fun testThatBatteryGetOkayLiveDataIsUpdated() {
+        val mockObserver = mockk<Observer<Boolean>>(relaxed = true)
+        liveData.observeForever(mockObserver)
+        application.sendBroadcast(Intent(Intent.ACTION_BATTERY_OKAY))
+        verifySequence {
+            // first battery is okay
+            mockObserver.onChanged(false)
+            // second when broadcast
+            mockObserver.onChanged(false)
+        }
+    }
+
+    @Test
+    @Config(minSdk = Build.VERSION_CODES.P)
+    fun testThatOnPWhenBatteryIsAlreadyLowLivedataIsCorrect() {
+        val mockObserver = mockk<Observer<Boolean>>(relaxed = true)
+        application.sendStickyBroadcast(Intent(Intent.ACTION_BATTERY_CHANGED).apply {
+            putExtra(BatteryManager.EXTRA_BATTERY_LOW, true)
+        })
+        liveData.observeForever(mockObserver)
+        verifySequence {
+            mockObserver.onChanged(true)
+        }
+    }
+
+    @Test
+    @Config(maxSdk = Build.VERSION_CODES.O_MR1)
+    fun testThatBeforePWhenBatteryIsAlreadyLowLivedataIsCorrect() {
+        val mockObserver = mockk<Observer<Boolean>>(relaxed = true)
+        application.sendStickyBroadcast(Intent(Intent.ACTION_BATTERY_CHANGED).apply {
+            putExtra(BatteryManager.EXTRA_LEVEL, 5)
+            putExtra(BatteryManager.EXTRA_SCALE, 100)
+        })
+        liveData.observeForever(mockObserver)
+        verifySequence {
+            mockObserver.onChanged(true)
+        }
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/geekdroid/src/test/java/com/geekorum/geekdroid/shadows/ShadowAccountManager.java	Fri May 08 21:33:19 2020 -0400
@@ -0,0 +1,124 @@
+/**
+ * Geekdroid is a utility library for development on the Android
+ * Platform.
+ *
+ * Copyright (C) 2017-2020 by Frederic-Charles Barthelery.
+ *
+ * This file is part of Geekdroid.
+ *
+ * Geekdroid is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Geekdroid is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Geekdroid.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.geekorum.geekdroid.shadows;
+
+import android.accounts.Account;
+import android.accounts.AccountManager;
+import android.accounts.OnAccountsUpdateListener;
+import android.os.Handler;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+
+import java.util.LinkedList;
+import java.util.List;
+import java.util.ListIterator;
+
+/**
+ * Extends {@link org.robolectric.shadows.ShadowAccountManager} to add support for
+ * {@link AccountManager#addOnAccountsUpdatedListener(OnAccountsUpdateListener, Handler, boolean, String[])}
+ * which was added in API 26. This will probably not needed on later version of Robolectric (current is 3.4.2)
+ */
+@Implements(AccountManager.class)
+public class ShadowAccountManager extends org.robolectric.shadows.ShadowAccountManager {
+
+    private class AccountTypesListener {
+        private final OnAccountsUpdateListener listener;
+        private final String[] accountTypes;
+
+        AccountTypesListener(OnAccountsUpdateListener listener, String[] accountTypes) {
+            this.listener = listener;
+            this.accountTypes = accountTypes;
+        }
+    }
+
+    private List<AccountTypesListener> accountTypesListener = new LinkedList<>();
+
+    @Implementation
+    public void addOnAccountsUpdatedListener(final OnAccountsUpdateListener listener,
+                                             Handler handler, boolean updateImmediately, String[] accountTypes) {
+
+        if (containsAccountListener(listener)) {
+            return;
+        }
+
+        addListener(listener, accountTypes);
+
+        if (updateImmediately) {
+            listener.onAccountsUpdated(getAccounts(accountTypes));
+        }
+    }
+
+    @Override
+    public void removeOnAccountsUpdatedListener(OnAccountsUpdateListener listener) {
+        super.removeOnAccountsUpdatedListener(listener);
+        ListIterator<AccountTypesListener> it = accountTypesListener.listIterator();
+        while (it.hasNext()) {
+            AccountTypesListener next = it.next();
+            if (next.listener == listener) {
+                it.remove();
+            }
+        }
+    }
+
+    @Override
+    public void addAccount(Account account) {
+        super.addAccount(account);
+        notifyListeners(account.type);
+    }
+
+    private boolean containsAccountListener(OnAccountsUpdateListener listener) {
+        for (AccountTypesListener list : accountTypesListener) {
+            if (list == listener) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private void addListener(OnAccountsUpdateListener listener, String[] accountTypes) {
+        AccountTypesListener l = new AccountTypesListener(listener, accountTypes);
+        accountTypesListener.add(l);
+    }
+
+    private Account[] getAccounts(String[] accountTypes) {
+        Account[] accounts = getAccounts();
+        List<Account> result = new LinkedList<>();
+        for (Account account : accounts) {
+            for (String type : accountTypes) {
+                if (type.equals(account.type)) {
+                    result.add(account);
+                }
+            }
+        }
+        return result.toArray(new Account[result.size()]);
+    }
+
+    private void notifyListeners(String accountType) {
+        for (AccountTypesListener typesListener : accountTypesListener) {
+            for (String type : typesListener.accountTypes) {
+                if (type.equals(accountType)) {
+                    typesListener.listener.onAccountsUpdated(getAccounts(new String[]{accountType}));
+                }
+            }
+        }
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/geekdroid/src/test/java/com/geekorum/geekdroid/shadows/ShadowContentResolver.java	Fri May 08 21:33:19 2020 -0400
@@ -0,0 +1,82 @@
+/**
+ * Geekdroid is a utility library for development on the Android
+ * Platform.
+ *
+ * Copyright (C) 2017-2020 by Frederic-Charles Barthelery.
+ *
+ * This file is part of Geekdroid.
+ *
+ * Geekdroid is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Geekdroid is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Geekdroid.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.geekorum.geekdroid.shadows;
+
+import android.accounts.Account;
+import android.content.ContentResolver;
+import android.content.SyncStatusObserver;
+import android.os.Bundle;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Extends {@link org.robolectric.shadows.ShadowContentResolver} to add support for
+ * {@link ContentResolver#addStatusChangeListener(int, SyncStatusObserver)}
+ * which is not implemented in Robolectric's {@link org.robolectric.shadows.ShadowContentResolver}.
+ * This will maybe not needed on later version of Robolectric (current is 3.4.2)
+ */
+@Implements(ContentResolver.class)
+public class ShadowContentResolver extends org.robolectric.shadows.ShadowContentResolver {
+
+    private static Set<SyncStatusObserver> observers = new HashSet<>();
+
+    @Implementation
+    public static Object addStatusChangeListener(int mask, SyncStatusObserver observer) {
+        observers.add(observer);
+        return observer;
+    }
+
+    @Implementation
+    public static void removeStatusChangeListener(Object handle) {
+        observers.remove(handle);
+    }
+
+    @Implementation
+    public static void requestSync(Account account, String authority, Bundle extras) {
+        Status status = getStatus(account, authority, true);
+        int oldSyncRequest = status.syncRequests;
+        org.robolectric.shadows.ShadowContentResolver.requestSync(account, authority, extras);
+        if (oldSyncRequest != status.syncRequests) {
+            notifyListeners(ContentResolver.SYNC_OBSERVER_TYPE_ACTIVE);
+        }
+    }
+
+    @Implementation
+    public static void cancelSync(Account account, String authority) {
+        Status status = getStatus(account, authority);
+        int oldSyncRequest = status.syncRequests;
+        org.robolectric.shadows.ShadowContentResolver.cancelSync(account, authority);
+        if (oldSyncRequest != status.syncRequests) {
+            notifyListeners(ContentResolver.SYNC_OBSERVER_TYPE_ACTIVE);
+        }
+    }
+
+    private static void notifyListeners(int which) {
+        for (SyncStatusObserver observer : observers) {
+            observer.onStatusChanged(which);
+        }
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/geekdroid/src/test/java/com/geekorum/geekdroid/utils/LifecycleMock.java	Fri May 08 21:33:19 2020 -0400
@@ -0,0 +1,41 @@
+/**
+ * Geekdroid is a utility library for development on the Android
+ * Platform.
+ *
+ * Copyright (C) 2017-2020 by Frederic-Charles Barthelery.
+ *
+ * This file is part of Geekdroid.
+ *
+ * Geekdroid is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Geekdroid is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Geekdroid.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.geekorum.geekdroid.utils;
+
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.LifecycleRegistry;
+
+/**
+ * Mock for a {@link androidx.lifecycle.Lifecycle}.
+ *
+ * Allos to manipulate Lifecycle from your tests
+ */
+public class LifecycleMock implements LifecycleOwner {
+
+    private LifecycleRegistry lifecycleRegistry = new LifecycleRegistry(this);
+
+    @Override
+    public LifecycleRegistry getLifecycle() {
+        return lifecycleRegistry;
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gradle.properties	Fri May 08 21:33:19 2020 -0400
@@ -0,0 +1,29 @@
+# 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
+
+# use androidx
+android.useAndroidX=true
+android.enableJetifier=true
+
+# check licenses headers
+# CHECK_LICENSE_HEADERS=true
+
+# avdl configuration
+# RUN_TESTS_ON_AVDL=true
+# FLYDROID_URL=https://flydroid.example.com
+# FLYDROID_KEY=flydroid-api-key
Binary file gradle/wrapper/gradle-wrapper.jar has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gradle/wrapper/gradle-wrapper.properties	Fri May 08 21:33:19 2020 -0400
@@ -0,0 +1,5 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-6.3-all.zip
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gradlew	Fri May 08 21:33:19 2020 -0400
@@ -0,0 +1,183 @@
+#!/usr/bin/env sh
+
+#
+# Copyright 2015 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+##############################################################################
+##
+##  Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+    ls=`ls -ld "$PRG"`
+    link=`expr "$ls" : '.*-> \(.*\)$'`
+    if expr "$link" : '/.*' > /dev/null; then
+        PRG="$link"
+    else
+        PRG=`dirname "$PRG"`"/$link"
+    fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+    echo "$*"
+}
+
+die () {
+    echo
+    echo "$*"
+    echo
+    exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+  CYGWIN* )
+    cygwin=true
+    ;;
+  Darwin* )
+    darwin=true
+    ;;
+  MINGW* )
+    msys=true
+    ;;
+  NONSTOP* )
+    nonstop=true
+    ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+    if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+        # IBM's JDK on AIX uses strange locations for the executables
+        JAVACMD="$JAVA_HOME/jre/sh/java"
+    else
+        JAVACMD="$JAVA_HOME/bin/java"
+    fi
+    if [ ! -x "$JAVACMD" ] ; then
+        die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+    fi
+else
+    JAVACMD="java"
+    which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+    MAX_FD_LIMIT=`ulimit -H -n`
+    if [ $? -eq 0 ] ; then
+        if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+            MAX_FD="$MAX_FD_LIMIT"
+        fi
+        ulimit -n $MAX_FD
+        if [ $? -ne 0 ] ; then
+            warn "Could not set maximum file descriptor limit: $MAX_FD"
+        fi
+    else
+        warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+    fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+    GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
+    APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+    CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+    JAVACMD=`cygpath --unix "$JAVACMD"`
+
+    # We build the pattern for arguments to be converted via cygpath
+    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+    SEP=""
+    for dir in $ROOTDIRSRAW ; do
+        ROOTDIRS="$ROOTDIRS$SEP$dir"
+        SEP="|"
+    done
+    OURCYGPATTERN="(^($ROOTDIRS))"
+    # Add a user-defined pattern to the cygpath arguments
+    if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+        OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+    fi
+    # Now convert the arguments - kludge to limit ourselves to /bin/sh
+    i=0
+    for arg in "$@" ; do
+        CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+        CHECK2=`echo "$arg"|egrep -c "^-"`                                 ### Determine if an option
+
+        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition
+            eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+        else
+            eval `echo args$i`="\"$arg\""
+        fi
+        i=`expr $i + 1`
+    done
+    case $i in
+        0) set -- ;;
+        1) set -- "$args0" ;;
+        2) set -- "$args0" "$args1" ;;
+        3) set -- "$args0" "$args1" "$args2" ;;
+        4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+        5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+        6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+        7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+        8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+        9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+    esac
+fi
+
+# Escape application args
+save () {
+    for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+    echo " "
+}
+APP_ARGS=`save "$@"`
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+exec "$JAVACMD" "$@"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gradlew.bat	Fri May 08 21:33:19 2020 -0400
@@ -0,0 +1,100 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem      https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@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="-Xmx64m" "-Xms64m"
+
+@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
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/settings.gradle.kts	Fri May 08 21:33:19 2020 -0400
@@ -0,0 +1,11 @@
+pluginManagement {
+    repositories {
+        gradlePluginPortal()
+        jcenter()
+        google()
+    }
+}
+
+rootProject.name = "geekdroid"
+include(":geekdroid")
+include(":geekdroid-firebase")