--- /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<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<ApiResponse>() {
+ * {@literal @}Override
+ * public void onResponse(Call<ApiResponse> call,
+ * Response<ApiResponse> response) {
+ * // TODO insert new records into database
+ * helperCallback.recordSuccess();
+ * }
+ *
+ * {@literal @}Override
+ * public void onFailure(Call<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<ApiResponse>() {
+ * {@literal @}Override
+ * public void onResponse(Call<ApiResponse> call,
+ * Response<ApiResponse> response) {
+ * // TODO insert new records into database
+ * helperCallback.recordSuccess();
+ * }
+ *
+ * {@literal @}Override
+ * public void onFailure(Call<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<PagingRequestHelper.Status> combined = new MutableLiveData<>();
+ * 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")