# HG changeset patch # User Da Risk # Date 1588987999 14400 # Node ID 831cffa9c9912baee9b97b5ab02e8101c19bdc46 # Parent fef46dce2812a02d0c0e92d0a4f6bbca53dff556 source import diff -r fef46dce2812 -r 831cffa9c991 .gitignore --- /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 diff -r fef46dce2812 -r 831cffa9c991 .hgignore --- /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/ + diff -r fef46dce2812 -r 831cffa9c991 README.md --- /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 + diff -r fef46dce2812 -r 831cffa9c991 build.gradle.kts --- /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() +} + +subprojects { + group = "com.geekorum" + version = "0.0.1" + + configureAnnotationProcessorDeps() +} + +task("clean", type = Delete::class) { + doLast { + delete(buildDir) + } +} + + diff -r fef46dce2812 -r 831cffa9c991 buildSrc/build.gradle.kts --- /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") +} diff -r fef46dce2812 -r 831cffa9c991 buildSrc/src/main/kotlin/AndroidJavaVersion.kt --- /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") + } + } + } +} diff -r fef46dce2812 -r 831cffa9c991 buildSrc/src/main/kotlin/AndroidPlayStorePublisher.kt --- /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 { + 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() 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")) + } + } + +} diff -r fef46dce2812 -r 831cffa9c991 buildSrc/src/main/kotlin/AndroidSigning.kt --- /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("android") { + signingConfigs { + register("release") { + storeFile = file(releaseStoreFile) + storePassword = releaseStorePassword + keyAlias = releaseKeyAlias + keyPassword = releaseKeyPassword + } + } + + buildTypes { + named("release") { + signingConfig = signingConfigs.getByName("release") + } + } + } +} + diff -r fef46dce2812 -r 831cffa9c991 buildSrc/src/main/kotlin/AndroidTests.kt --- /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 { + defaultConfig { + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + testInstrumentationRunnerArgument("clearPackageData", "true") + testInstrumentationRunnerArgument("disableAnalytics", "true") + } + + testOptions { + execution = "ANDROIDX_TEST_ORCHESTRATOR" + animationsDisabled = true + + unitTests(closureOf { + 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) diff -r fef46dce2812 -r 831cffa9c991 buildSrc/src/main/kotlin/Avdl.kt --- /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 . + */ +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() + + val oneInstrumentedTestService = gradle.sharedServices.registerIfAbsent( + "oneInstrumentedTest", OneInstrumentedTestService::class.java) { + maxParallelUsages.set(1) + } + + rootProject.serializeInstrumentedTestTask(oneInstrumentedTestService) + + val android = the() + configure { + 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? = null + var lastTestTask: TaskProvider? = 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 +): Pair, TaskProvider> { + val tasks = + registerAvdlDevicesTask(variant.name, devices) + tasks.orderForTask(variant.connectedInstrumentTestProvider) + return tasks +} + + +private fun Project.serializeInstrumentedTestTask(oneInstrumentedTestService: Provider) { + fun Project.configureTestTasks() { + extensions.configure { + testVariants.all { + connectedInstrumentTestProvider.configure { + usesService(oneInstrumentedTestService) + } + } + } + } + + allprojects { + val project = this + plugins.withType { project.configureTestTasks() } + plugins.withType { project.configureTestTasks() } + plugins.withType { project.configureTestTasks() } + } +} + +abstract class OneInstrumentedTestService : BuildService diff -r fef46dce2812 -r 831cffa9c991 buildSrc/src/main/kotlin/Repositories.kt --- /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") + } +} diff -r fef46dce2812 -r 831cffa9c991 buildSrc/src/main/kotlin/RepositoryChangeset.kt --- /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 + } +} diff -r fef46dce2812 -r 831cffa9c991 buildSrc/src/main/kotlin/SourceLicenseChecker.kt --- /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() + + configure { + 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(LicenseBasePlugin.getLICENSE_TASK_BASE_NAME()) { + dependsOn(checkKotlinFilesLicenseTask) + } + + named(LicenseBasePlugin.getFORMAT_TASK_BASE_NAME()) { + dependsOn(formatKotlinFilesLicenseTask) + } + } +} diff -r fef46dce2812 -r 831cffa9c991 buildSrc/src/main/kotlin/VersionAlignment.kt --- /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>() + +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 +) { + 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}") + } + } + } + +} diff -r fef46dce2812 -r 831cffa9c991 buildSrc/src/main/kotlin/android-avdl.gradle.kts --- /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 . + */ +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) +} diff -r fef46dce2812 -r 831cffa9c991 buildSrc/src/main/kotlin/android-signing.gradle.kts --- /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() +} diff -r fef46dce2812 -r 831cffa9c991 buildSrc/src/main/kotlin/android-tests.gradle.kts --- /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() diff -r fef46dce2812 -r 831cffa9c991 buildSrc/src/main/kotlin/play-store-publish.gradle.kts --- /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() +} diff -r fef46dce2812 -r 831cffa9c991 buildSrc/src/main/kotlin/source-license-checker.gradle.kts --- /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() +} diff -r fef46dce2812 -r 831cffa9c991 config/README.md --- /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 + diff -r fef46dce2812 -r 831cffa9c991 config/android-checkstyle.gradle --- /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 +} diff -r fef46dce2812 -r 831cffa9c991 config/android-maven-publication.gradle --- /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" + } + } + } +} diff -r fef46dce2812 -r 831cffa9c991 config/checkstyle/checkstyle.xml --- /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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff -r fef46dce2812 -r 831cffa9c991 config/java-checkstyle.gradle --- /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") +} + + diff -r fef46dce2812 -r 831cffa9c991 config/license/header.txt --- /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 . + diff -r fef46dce2812 -r 831cffa9c991 config/source-archive.gradle --- /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() +} diff -r fef46dce2812 -r 831cffa9c991 geekdroid-firebase/build.gradle.kts --- /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") +} diff -r fef46dce2812 -r 831cffa9c991 geekdroid-firebase/proguard-rules.pro --- /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 diff -r fef46dce2812 -r 831cffa9c991 geekdroid-firebase/src/androidTest/java/com/geekorum/geekdroid/firebase/ExampleInstrumentedTest.java --- /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 . + */ +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 Testing documentation + */ +@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()); + } +} diff -r fef46dce2812 -r 831cffa9c991 geekdroid-firebase/src/main/AndroidManifest.xml --- /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 @@ + diff -r fef46dce2812 -r 831cffa9c991 geekdroid-firebase/src/main/java/com/geekorum/geekdroid/firebase/CurrentUserLiveData.kt --- /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 . + */ +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() { + + private val authStateListener = { it: FirebaseAuth -> + value = it.currentUser + } + + override fun onActive() { + firebaseAuth.addAuthStateListener(authStateListener) + } + + override fun onInactive() { + firebaseAuth.removeAuthStateListener(authStateListener) + } +} diff -r fef46dce2812 -r 831cffa9c991 geekdroid-firebase/src/main/java/com/geekorum/geekdroid/firebase/Firestore.kt --- /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 . + */ +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 constructor( + private val query: Query, + private val clazz: Class +) : LiveData>() { + + 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 Query.toLiveData() : LiveData> = + FirestoreQueryLiveData(this) + +inline fun FirestoreQueryLiveData(query: Query): FirestoreQueryLiveData { + return FirestoreQueryLiveData(query, T::class.java) +} + +class FirestoreDocumentLiveData( + private val document: DocumentReference, + private val clazz: Class +) : LiveData() { + + 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 DocumentReference.toLiveData(): LiveData = + FirestoreDocumentLiveData(this) + +inline fun FirestoreDocumentLiveData(document: DocumentReference): FirestoreDocumentLiveData { + return FirestoreDocumentLiveData(document, T::class.java) +} + +@Deprecated("Use firebase-firestore-ktx", ReplaceWith("toObject()", imports = ["com.google.firebase.firestore.ktx.toObject"])) +inline fun DocumentSnapshot.toObject(): T? = toObject() + +@Deprecated("Use firebase-firestore-ktx", ReplaceWith("toObjects()", imports = ["com.google.firebase.firestore.ktx.toObjects"])) +inline fun QuerySnapshot.toObjects(): List = toObjects() + +/* suspend version of get(), set(), update(), delete() */ +suspend fun DocumentReference.aSet(pojo: Any): Void = set(pojo).await() +suspend fun DocumentReference.aUpdate(data: Map): 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 DocumentReference.toObject(): T? { + return get().await().toObject() +} diff -r fef46dce2812 -r 831cffa9c991 geekdroid-firebase/src/main/java/com/geekorum/geekdroid/firebase/Tasks.kt --- /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 . + */ +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 Task.await(): T = await() diff -r fef46dce2812 -r 831cffa9c991 geekdroid-firebase/src/main/java/com/geekorum/geekdroid/firebase/logging/CrashlyticsLoggingTree.kt --- /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 . + */ +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' + } +} diff -r fef46dce2812 -r 831cffa9c991 geekdroid-firebase/src/main/java/com/geekorum/geekdroid/gms/LocationServices.kt --- /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 . + */ +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) + } + } +} diff -r fef46dce2812 -r 831cffa9c991 geekdroid-firebase/src/main/java/com/geekorum/geekdroid/gms/Tasks.kt --- /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 . + */ +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 Task.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 Task.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 PlayCoreTask.asDeferred(): Deferred { + if (isComplete) { + val e = exception + return if (e == null) { + @Suppress("UNCHECKED_CAST") + CompletableDeferred().apply { complete(result as T) } + } else { + CompletableDeferred().apply { completeExceptionally(e) } + } + } + + val result = CompletableDeferred() + 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 PlayCoreTask.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) + } + } + } +} + diff -r fef46dce2812 -r 831cffa9c991 geekdroid-firebase/src/test/java/com/geekorum/geekdroid/firebase/ExampleUnitTest.java --- /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 . + */ +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 Testing documentation + */ +public class ExampleUnitTest { + @Test + public void addition_isCorrect() { + assertEquals(4, 2 + 2); + } +} diff -r fef46dce2812 -r 831cffa9c991 geekdroid/.gitignore --- /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 diff -r fef46dce2812 -r 831cffa9c991 geekdroid/build.gradle.kts --- /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") +} diff -r fef46dce2812 -r 831cffa9c991 geekdroid/proguard-rules.pro --- /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 *; +#} diff -r fef46dce2812 -r 831cffa9c991 geekdroid/src/androidTest/java/com/geekorum/geekdroid/ExampleInstrumentedTest.java --- /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 . + */ +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 Testing documentation + */ +@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()); + } +} diff -r fef46dce2812 -r 831cffa9c991 geekdroid/src/main/AndroidManifest.xml --- /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 @@ + + + + + diff -r fef46dce2812 -r 831cffa9c991 geekdroid/src/main/java/com/geekorum/geekdroid/RoomExt.kt --- /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 . + */ +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 { + if (data == null) { + return emptyList() + } + return data.split(", ") + } + + @TypeConverter + fun listToPlainString(data: List?): String { + if (data == null) { + return "" + } + return data.joinToString(", ") + } +} diff -r fef46dce2812 -r 831cffa9c991 geekdroid/src/main/java/com/geekorum/geekdroid/accounts/AccountAuthenticatorAppCompatActivity.kt --- /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 . + */ +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() + } +} diff -r fef46dce2812 -r 831cffa9c991 geekdroid/src/main/java/com/geekorum/geekdroid/accounts/AccountSelector.kt --- /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 . + */ +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" + } + +} diff -r fef46dce2812 -r 831cffa9c991 geekdroid/src/main/java/com/geekorum/geekdroid/accounts/AccountTokenRetriever.kt --- /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 . + */ +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) + } +} diff -r fef46dce2812 -r 831cffa9c991 geekdroid/src/main/java/com/geekorum/geekdroid/accounts/AccountsListViewModel.kt --- /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 . + */ +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() + val selectedAccount: LiveData = mutableSelectedAccount + + val accounts = AccountsLiveData(accountManager, *accountTypes) + + init { + mutableSelectedAccount.setValue(accountSelector.savedAccount) + } + + fun selectAccount(account: Account) { + accountSelector.saveAccount(account) + mutableSelectedAccount.setValue(account) + } + +} diff -r fef46dce2812 -r 831cffa9c991 geekdroid/src/main/java/com/geekorum/geekdroid/accounts/AccountsLiveData.kt --- /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 . + */ +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>() { + + private val accountTypes: List = 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): Array { + return accounts.filter { it.type in accountTypes }.toTypedArray() + } + +} diff -r fef46dce2812 -r 831cffa9c991 geekdroid/src/main/java/com/geekorum/geekdroid/accounts/CancellableSyncAdapter.kt --- /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 . + */ +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> = 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() + + } +} + diff -r fef46dce2812 -r 831cffa9c991 geekdroid/src/main/java/com/geekorum/geekdroid/accounts/SyncInProgressLiveData.kt --- /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 . + */ +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() { + + 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) + } +} diff -r fef46dce2812 -r 831cffa9c991 geekdroid/src/main/java/com/geekorum/geekdroid/app/AppCompatPreferenceActivity.java --- /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 . + */ +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; + } +} diff -r fef46dce2812 -r 831cffa9c991 geekdroid/src/main/java/com/geekorum/geekdroid/app/BottomSheetDialogActivity.kt --- /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 . + */ +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 + 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 + } + +} diff -r fef46dce2812 -r 831cffa9c991 geekdroid/src/main/java/com/geekorum/geekdroid/app/lifecycle/EventObserver.kt --- /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 . + */ +package com.geekorum.geekdroid.app.lifecycle + +import androidx.lifecycle.Observer + +/** + * An Event of content T + */ +open class Event( + 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()) { + companion object { + @JvmStatic + fun makeEmptyEvent() = EmptyEvent() + } +} + +/** + * An event observer observe a LiveData an will be executed only once, when the event changed + */ +class EventObserver( + private val onEventUnhandled: (T) -> Unit +) : Observer> { + + override fun onChanged(event: Event?) { + event?.getContentIfNotHandled()?.let(onEventUnhandled) + } +} diff -r fef46dce2812 -r 831cffa9c991 geekdroid/src/main/java/com/geekorum/geekdroid/app/lifecycle/Transformations.kt --- /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 . + */ +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 LiveData>.join(other: LiveData>): LiveData> { + return this.combine(other) {first, second -> first + second } +} + +/** + * Union of two [LiveData] collections into a List + */ +fun LiveData>.union(other: LiveData>): LiveData> { + return this.combine(other) { first, second -> first.union(second).toList() } +} + +private fun LiveData>.combine( + other: LiveData>, + combinator: (first: Collection, second: Collection) -> List +): LiveData> { + return MediatorLiveData>().apply { + var first: Collection = emptyList() + var second: Collection = 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 LiveData.withRefreshEvent(event: LiveData>): LiveData = RefreshOnEventLiveData(this, event) + + +/** + * A LiveData that delivers source new value only when event is received. + */ +private class RefreshOnEventLiveData( + source: LiveData, + event: LiveData> +) : MediatorLiveData() { + 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 + }) + } +} diff -r fef46dce2812 -r 831cffa9c991 geekdroid/src/main/java/com/geekorum/geekdroid/arch/PagingRequestHelper.java --- /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 . + */ +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. + *

+ * 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)}. + *

+ * It tracks a {@link Status} and an {@code error} for each {@link RequestType}. + *

+ * A sample usage of this class to limit requests looks like this: + *

+ * 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);
+ *                             }
+ *                         }));
+ *     }
+ * }
+ * 
+ *

+ * The helper provides an API to observe combined request status, which can be reported back to the + * application based on your business rules. + *

+ * 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);
+ *     }
+ * });
+ * 
+ */ +// 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 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. + *

+ * 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}. + *

+ * 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; + } + } +} diff -r fef46dce2812 -r 831cffa9c991 geekdroid/src/main/java/com/geekorum/geekdroid/battery/LiveData.kt --- /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 . + */ +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() { + 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() { + + 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) + } +} diff -r fef46dce2812 -r 831cffa9c991 geekdroid/src/main/java/com/geekorum/geekdroid/bindings/BindingAdapters.kt --- /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 . + */ +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) +} diff -r fef46dce2812 -r 831cffa9c991 geekdroid/src/main/java/com/geekorum/geekdroid/bindings/Converters.java --- /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 . + */ +package com.geekorum.geekdroid.bindings; + +/** + * Some custom Converters for Android Data Binding Library + */ +public class Converters { + +} diff -r fef46dce2812 -r 831cffa9c991 geekdroid/src/main/java/com/geekorum/geekdroid/dagger/AndroidFrameworkModule.kt --- /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 . + */ +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 diff -r fef46dce2812 -r 831cffa9c991 geekdroid/src/main/java/com/geekorum/geekdroid/dagger/AndroidxCoreModule.kt --- /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 . + */ +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) + } +} diff -r fef46dce2812 -r 831cffa9c991 geekdroid/src/main/java/com/geekorum/geekdroid/dagger/AppInitializers.kt --- /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 . + */ +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 + +} + + +/** + * AppInitializers are meant to be run in [Application.onCreate] + */ +interface AppInitializer { + fun initialize(app: Application) +} + +fun Collection.initialize(app: Application) { + forEach { it.initialize(app) } +} diff -r fef46dce2812 -r 831cffa9c991 geekdroid/src/main/java/com/geekorum/geekdroid/dagger/AssistedInjection.kt --- /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 . + */ +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 diff -r fef46dce2812 -r 831cffa9c991 geekdroid/src/main/java/com/geekorum/geekdroid/dagger/FragmentFactory.kt --- /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 . + */ +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 +) + +/** + * 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, Fragment> +} + +/** + * Factory that can creates the [Fragment] needed by application after injecting them. + */ +class DaggerDelegateFragmentFactory @Inject constructor( + private val providers: Map, @JvmSuppressWildcards Provider> +) : 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) + } + +} + diff -r fef46dce2812 -r 831cffa9c991 geekdroid/src/main/java/com/geekorum/geekdroid/dagger/ViewModels.kt --- /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 . + */ +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 +) + +/** + * 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 + * ``` + * @see [DaggerDelegateSavedStateVMFactory] + */ +@Module(includes = [GeekdroidAssistedModule::class]) +abstract class ViewModelsModule private constructor() { + + @Multibinds + abstract fun viewModelsFactories(): Map, ViewModel> + + @Multibinds + abstract fun savedStateViewModelsFactories(): Map, ViewModelAssistedFactory> +} + + +/** + * Factory that can creates the [ViewModel] needed by application after injecting them. + */ +class DaggerDelegateViewModelsFactory @Inject constructor( + private val providers: Map, @JvmSuppressWildcards Provider> +) : ViewModelProvider.Factory { + + override fun create(modelClass: Class): 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 { + * 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, @JvmSuppressWildcards Provider>, + private val savedStateFactoryProviders: Map, @JvmSuppressWildcards Provider>>, + @Assisted val owner: SavedStateRegistryOwner, + @Assisted val defaultArgs: Bundle? +) : AbstractSavedStateViewModelFactory(owner, defaultArgs) { + + override fun create(key: String, modelClass: Class, 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 { + fun create(state: SavedStateHandle) : T +} diff -r fef46dce2812 -r 831cffa9c991 geekdroid/src/main/java/com/geekorum/geekdroid/dagger/WorkerInjection.kt --- /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 . + */ +@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) + +/** + * Add this module to your component to support dependency injection of [ListenableWorker]. + * + * You will get a multibinding Set by adding some WorkerFactory in your module. +``` +@Binds +@IntoSet +abstract fun bindMyWorkerFactory(workerFactory: MyWorkerFactory): WorkerFactory +``` + */ +@Module +abstract class WorkerInjectionModule private constructor() { + + @Multibinds + @Deprecated("use Set multibinding") + abstract fun workerFactoriesMap(): Map, WorkerFactory> + + @Multibinds + abstract fun workerFactories(): Set + + @Module + companion object { + @Provides + @ElementsIntoSet + @JvmStatic + fun workersFactoriesMapAsSet(workerFactoriesMap: Map, @JvmSuppressWildcards WorkerFactory>) : Set { + 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, @JvmSuppressWildcards Provider> +) : 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) + } + +} diff -r fef46dce2812 -r 831cffa9c991 geekdroid/src/main/java/com/geekorum/geekdroid/jobs/JobThread.java --- /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 . + */ +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); + } + +} diff -r fef46dce2812 -r 831cffa9c991 geekdroid/src/main/java/com/geekorum/geekdroid/jobs/ThreadedJobService.java --- /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 . + */ +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> tasks = Collections.synchronizedMap(new HashMap>()); + 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); +} diff -r fef46dce2812 -r 831cffa9c991 geekdroid/src/main/java/com/geekorum/geekdroid/loaders/ObjectCursorLoader.java --- /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 . + */ +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 extends CursorLoader { + + private final CursorMapper cursorMapper; + private List items = Collections.emptyList(); + + public ObjectCursorLoader(Context context, CursorMapper cursorMapper) { + super(context); + this.cursorMapper = cursorMapper; + } + + public ObjectCursorLoader(Context context, Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder, CursorMapper 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 newItems = new LinkedList<>(); + while (cursor.moveToNext()) { + newItems.add(cursorMapper.map(cursor)); + } + this.items = newItems; + } + + /** + * Get the loaded items + * @return the items + */ + public List getItems() { + return items; + } + + public interface CursorMapper { + T map(Cursor cursor); + } + +} diff -r fef46dce2812 -r 831cffa9c991 geekdroid/src/main/java/com/geekorum/geekdroid/network/BrowserLauncher.kt --- /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 . + */ +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 + 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) -> List = { 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 { + 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): List + } + + private fun launchUriInOtherApp(context: Context, uri: Uri) { + val intent = Intent(Intent.ACTION_VIEW, uri) + context.startActivity(intent) + } + +} diff -r fef46dce2812 -r 831cffa9c991 geekdroid/src/main/java/com/geekorum/geekdroid/network/OkHttpWebViewClient.java --- /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 . + */ +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(); + } +} diff -r fef46dce2812 -r 831cffa9c991 geekdroid/src/main/java/com/geekorum/geekdroid/network/PicassoOkHttp3Downloader.kt --- /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 . + */ +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()) + } + } + +} diff -r fef46dce2812 -r 831cffa9c991 geekdroid/src/main/java/com/geekorum/geekdroid/network/Socket.kt --- /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 . + */ +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) + } +} diff -r fef46dce2812 -r 831cffa9c991 geekdroid/src/main/java/com/geekorum/geekdroid/network/TokenRetriever.kt --- /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 . + */ +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) + +} diff -r fef46dce2812 -r 831cffa9c991 geekdroid/src/main/java/com/geekorum/geekdroid/preferences/PreferenceSummaryBinder.java --- /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 . + */ +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(), "")); + } + + +} diff -r fef46dce2812 -r 831cffa9c991 geekdroid/src/main/java/com/geekorum/geekdroid/preferences/RingtonePreferenceSummaryBinder.kt --- /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 . + */ +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, "")!!) + } + +} diff -r fef46dce2812 -r 831cffa9c991 geekdroid/src/main/java/com/geekorum/geekdroid/security/SimpleEncryption.kt --- /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 . + */ +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) + } +} diff -r fef46dce2812 -r 831cffa9c991 geekdroid/src/main/java/com/geekorum/geekdroid/utils/PriorityRunnable.java --- /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 . + */ +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(); + } +} + diff -r fef46dce2812 -r 831cffa9c991 geekdroid/src/main/java/com/geekorum/geekdroid/utils/ProcessPriority.java --- /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 . + */ +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 { } diff -r fef46dce2812 -r 831cffa9c991 geekdroid/src/main/java/com/geekorum/geekdroid/views/AccountMenuLineView.kt --- /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 . + */ +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 = object : Parcelable.Creator { + + override fun createFromParcel(`in`: Parcel): SavedState = SavedState(`in`) + + override fun newArray(size: Int): Array = 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) + } + + +} diff -r fef46dce2812 -r 831cffa9c991 geekdroid/src/main/java/com/geekorum/geekdroid/views/CheckableImageView.java --- /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 . + */ +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. + *

+ * 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 CREATOR + = new Parcelable.Creator() { + 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(); + } +} diff -r fef46dce2812 -r 831cffa9c991 geekdroid/src/main/java/com/geekorum/geekdroid/views/EdgeToEdge.kt --- /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 . + */ +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)) +} diff -r fef46dce2812 -r 831cffa9c991 geekdroid/src/main/java/com/geekorum/geekdroid/views/MultipleLongClickGestureDetector.java --- /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 . + */ +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); + } +} diff -r fef46dce2812 -r 831cffa9c991 geekdroid/src/main/java/com/geekorum/geekdroid/views/ReturningBottomAppBar.kt --- /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 . + */ +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 + } + + } + +} diff -r fef46dce2812 -r 831cffa9c991 geekdroid/src/main/java/com/geekorum/geekdroid/views/banners/Banners.kt --- /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 . + */ +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 +) diff -r fef46dce2812 -r 831cffa9c991 geekdroid/src/main/java/com/geekorum/geekdroid/views/banners/Builders.kt --- /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 . + */ +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) + } +} diff -r fef46dce2812 -r 831cffa9c991 geekdroid/src/main/java/com/geekorum/geekdroid/views/behaviors/NestedCoordinatorLayout.java --- /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 . + */ +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); + } + +} diff -r fef46dce2812 -r 831cffa9c991 geekdroid/src/main/java/com/geekorum/geekdroid/views/behaviors/ScrollAwareFABBehavior.java --- /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 . + */ +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(); + } + } +} diff -r fef46dce2812 -r 831cffa9c991 geekdroid/src/main/java/com/geekorum/geekdroid/views/behaviors/SwingBottomItemAnimator.java --- /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 . + */ +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(); + } + +} diff -r fef46dce2812 -r 831cffa9c991 geekdroid/src/main/java/com/geekorum/geekdroid/views/recyclerview/FirstLayoutItemAnimator.java --- /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 . + */ +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); +} diff -r fef46dce2812 -r 831cffa9c991 geekdroid/src/main/java/com/geekorum/geekdroid/views/recyclerview/ItemSwiper.java --- /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 . + */ +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; + } + } +} diff -r fef46dce2812 -r 831cffa9c991 geekdroid/src/main/java/com/geekorum/geekdroid/views/recyclerview/ScrollFromBottomAppearanceItemAnimator.java --- /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 . + */ +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); + } +} diff -r fef46dce2812 -r 831cffa9c991 geekdroid/src/main/java/com/geekorum/geekdroid/views/recyclerview/SingleItemSwipedCallback.java --- /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 . + */ +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); + } + +} diff -r fef46dce2812 -r 831cffa9c991 geekdroid/src/main/java/com/geekorum/geekdroid/views/recyclerview/SpacingItemDecoration.java --- /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 . + */ +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; + } +} diff -r fef46dce2812 -r 831cffa9c991 geekdroid/src/main/java/com/geekorum/geekdroid/views/recyclerview/ViewItemDecoration.java --- /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 . + */ +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); + + } +} diff -r fef46dce2812 -r 831cffa9c991 geekdroid/src/main/res/drawable/view_account_menu_line_handle_closed.xml --- /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 @@ + + + + + + diff -r fef46dce2812 -r 831cffa9c991 geekdroid/src/main/res/drawable/view_account_menu_line_handle_open.xml --- /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 @@ + + + + + + diff -r fef46dce2812 -r 831cffa9c991 geekdroid/src/main/res/drawable/view_account_menu_line_handle_selector.xml --- /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 @@ + + + + + + + + + diff -r fef46dce2812 -r 831cffa9c991 geekdroid/src/main/res/layout/activity_bottom_sheet_dialog.xml --- /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 @@ + + + + + + + + + + + + + + + + + diff -r fef46dce2812 -r 831cffa9c991 geekdroid/src/main/res/layout/view_account_menu_line.xml --- /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 @@ + + + + + + + + diff -r fef46dce2812 -r 831cffa9c991 geekdroid/src/main/res/layout/view_banner_extended.xml --- /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 @@ + + + + + + + + + + + + + + + + + + + + diff -r fef46dce2812 -r 831cffa9c991 geekdroid/src/main/res/layout/view_banner_simple.xml --- /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 @@ + + + + + + + + + + + + + + + + diff -r fef46dce2812 -r 831cffa9c991 geekdroid/src/main/res/values/attrs.xml --- /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 @@ + + + + + + + + + + + + + + diff -r fef46dce2812 -r 831cffa9c991 geekdroid/src/main/res/values/ids.xml --- /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 @@ + + + + + diff -r fef46dce2812 -r 831cffa9c991 geekdroid/src/main/res/values/strings.xml --- /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 + Silent + diff -r fef46dce2812 -r 831cffa9c991 geekdroid/src/main/res/values/styles.xml --- /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 @@ + + + + + + + + + + diff -r fef46dce2812 -r 831cffa9c991 geekdroid/src/test/java/com/geekorum/geekdroid/accounts/AccountsLiveDataTest.java --- /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 . + */ +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 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 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 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); + } +} diff -r fef46dce2812 -r 831cffa9c991 geekdroid/src/test/java/com/geekorum/geekdroid/accounts/CancellableSyncAdapterTest.kt --- /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 . + */ +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 + } +} + diff -r fef46dce2812 -r 831cffa9c991 geekdroid/src/test/java/com/geekorum/geekdroid/accounts/SyncInProgressLiveDataTest.java --- /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 . + */ +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 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); + } +} diff -r fef46dce2812 -r 831cffa9c991 geekdroid/src/test/java/com/geekorum/geekdroid/battery/LiveDataTest.kt --- /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 . + */ +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>(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>(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>(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>(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>(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) + } + } +} diff -r fef46dce2812 -r 831cffa9c991 geekdroid/src/test/java/com/geekorum/geekdroid/shadows/ShadowAccountManager.java --- /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 . + */ +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 = 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 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 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})); + } + } + } + } +} diff -r fef46dce2812 -r 831cffa9c991 geekdroid/src/test/java/com/geekorum/geekdroid/shadows/ShadowContentResolver.java --- /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 . + */ +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 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); + } + } + +} diff -r fef46dce2812 -r 831cffa9c991 geekdroid/src/test/java/com/geekorum/geekdroid/utils/LifecycleMock.java --- /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 . + */ +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; + } + +} diff -r fef46dce2812 -r 831cffa9c991 gradle.properties --- /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 diff -r fef46dce2812 -r 831cffa9c991 gradle/wrapper/gradle-wrapper.jar Binary file gradle/wrapper/gradle-wrapper.jar has changed diff -r fef46dce2812 -r 831cffa9c991 gradle/wrapper/gradle-wrapper.properties --- /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 diff -r fef46dce2812 -r 831cffa9c991 gradlew --- /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" "$@" diff -r fef46dce2812 -r 831cffa9c991 gradlew.bat --- /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 diff -r fef46dce2812 -r 831cffa9c991 settings.gradle.kts --- /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")