From bf4b1e8b3e203ff7033e0fb8d6705290b2422a55 Mon Sep 17 00:00:00 2001 From: Mattia Iavarone Date: Tue, 8 Sep 2020 16:54:17 +0200 Subject: [PATCH 1/7] Update dependencies, Gradle, Kotlin to 1.4.0, AGP to 4.0.1 --- build.gradle.kts | 8 +++---- buildSrc/.gitignore | 1 - buildSrc/build.gradle.kts | 15 ------------- compiler/build.gradle.kts | 16 +++++++------- demo/build.gradle.kts | 9 +++----- firestore/build.gradle.kts | 28 +++++++++++++----------- gradle/wrapper/gradle-wrapper.properties | 2 +- 7 files changed, 30 insertions(+), 49 deletions(-) delete mode 100644 buildSrc/.gitignore delete mode 100644 buildSrc/build.gradle.kts diff --git a/build.gradle.kts b/build.gradle.kts index 342f69e..2e5f6fb 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -13,7 +13,6 @@ buildscript { extra["minSdkVersion"] = 16 extra["compileSdkVersion"] = 29 extra["targetSdkVersion"] = 29 - extra["kotlinVersion"] = "1.3.61" repositories { google() @@ -22,10 +21,9 @@ buildscript { } dependencies { - val kotlinVersion = property("kotlinVersion") as String - classpath("com.android.tools.build:gradle:3.6.1") - classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion") - classpath("com.otaliastudios.tools:publisher:0.1.5") + classpath("com.android.tools.build:gradle:4.0.1") + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.4.0") + classpath("com.otaliastudios.tools:publisher:0.3.3") } } diff --git a/buildSrc/.gitignore b/buildSrc/.gitignore deleted file mode 100644 index 796b96d..0000000 --- a/buildSrc/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts deleted file mode 100644 index 55be88e..0000000 --- a/buildSrc/build.gradle.kts +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright (c) 2020 Otalia Studios. Author: Mattia Iavarone. - */ - -plugins { - `kotlin-dsl` -} - -repositories { - // Nothing yet -} - -dependencies { - // Nothing yet -} \ No newline at end of file diff --git a/compiler/build.gradle.kts b/compiler/build.gradle.kts index 27bfb9c..a1b22d8 100644 --- a/compiler/build.gradle.kts +++ b/compiler/build.gradle.kts @@ -2,12 +2,12 @@ * Copyright (c) 2018 Otalia Studios. Author: Mattia Iavarone. */ -import com.otaliastudios.tools.publisher.PublisherExtension.License -import com.otaliastudios.tools.publisher.PublisherExtension.Release +import com.otaliastudios.tools.publisher.common.License +import com.otaliastudios.tools.publisher.common.Release plugins { id("kotlin") - id("maven-publisher-bintray") + id("com.otaliastudios.tools.publisher") } java { @@ -16,17 +16,12 @@ java { } dependencies { - val kotlinVersion = property("kotlinVersion") as String - api("org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion") api("com.squareup:kotlinpoet:1.5.0") api("com.squareup:kotlinpoet-metadata:1.5.0") api("com.squareup:kotlinpoet-metadata-specs:1.5.0") } publisher { - auth.user = "BINTRAY_USER" - auth.key = "BINTRAY_KEY" - auth.repo = "BINTRAY_REPO" project.artifact = "firestore-compiler" project.description = property("libDescription") as String project.group = property("libGroup") as String @@ -36,4 +31,9 @@ publisher { release.version = property("libVersion") as String release.setSources(Release.SOURCES_AUTO) release.setDocs(Release.DOCS_AUTO) + bintray { + auth.user = "BINTRAY_USER" + auth.key = "BINTRAY_KEY" + auth.repo = "BINTRAY_REPO" + } } diff --git a/demo/build.gradle.kts b/demo/build.gradle.kts index 3cdff56..b2c7e89 100644 --- a/demo/build.gradle.kts +++ b/demo/build.gradle.kts @@ -34,10 +34,7 @@ dependencies { implementation(project(":firestore")) kapt(project(":compiler")) - - val kotlin = rootProject.extra["kotlinVersion"] - implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin") - implementation("androidx.appcompat:appcompat:1.1.0") - implementation("androidx.core:core-ktx:1.2.0") - implementation("androidx.constraintlayout:constraintlayout:1.1.3") + implementation("androidx.appcompat:appcompat:1.2.0") + implementation("androidx.core:core-ktx:1.3.1") + implementation("androidx.constraintlayout:constraintlayout:2.0.1") } diff --git a/firestore/build.gradle.kts b/firestore/build.gradle.kts index 50e35b8..8968f9c 100644 --- a/firestore/build.gradle.kts +++ b/firestore/build.gradle.kts @@ -2,13 +2,13 @@ * Copyright (c) 2018 Otalia Studios. Author: Mattia Iavarone. */ -import com.otaliastudios.tools.publisher.PublisherExtension.License -import com.otaliastudios.tools.publisher.PublisherExtension.Release +import com.otaliastudios.tools.publisher.common.License +import com.otaliastudios.tools.publisher.common.Release plugins { id("com.android.library") id("kotlin-android") - id("maven-publisher-bintray") + id("com.otaliastudios.tools.publisher") } android { @@ -19,11 +19,13 @@ android { versionName = property("libVersion") as String } - dataBinding.isEnabled = true + buildFeatures { + dataBinding = true + } sourceSets { - get("main").java.srcDirs("src/main/kotlin") - get("test").java.srcDirs("src/test/kotlin") + getByName("main").java.srcDirs("src/main/kotlin") + getByName("test").java.srcDirs("src/test/kotlin") } compileOptions { @@ -32,21 +34,16 @@ android { } buildTypes { - get("release").consumerProguardFile("proguard-rules.pro") + getByName("release").consumerProguardFile("proguard-rules.pro") } } dependencies { - val kotlinVersion = property("kotlinVersion") - api("org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion") - api("com.google.firebase:firebase-firestore:21.4.1") + api("com.google.firebase:firebase-firestore-ktx:21.6.0") api("com.jakewharton.timber:timber:4.7.1") } publisher { - auth.user = "BINTRAY_USER" - auth.key = "BINTRAY_KEY" - auth.repo = "BINTRAY_REPO" project.artifact = "firestore" project.description = property("libDescription") as String project.group = property("libGroup") as String @@ -55,4 +52,9 @@ publisher { project.addLicense(License.APACHE_2_0) release.setSources(Release.SOURCES_AUTO) release.setDocs(Release.DOCS_AUTO) + bintray { + auth.user = "BINTRAY_USER" + auth.key = "BINTRAY_KEY" + auth.repo = "BINTRAY_REPO" + } } \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 1b48104..4238311 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.4-all.zip From 62ab0e86b47e9c258136b3913c2719208765aab6 Mon Sep 17 00:00:00 2001 From: Mattia Iavarone Date: Tue, 8 Sep 2020 17:14:35 +0200 Subject: [PATCH 2/7] Add explicit API mode --- firestore/build.gradle.kts | 6 ++++ .../otaliastudios/firestore/Annotations.kt | 14 ++++---- .../firestore/DocumentReferenceParceler.kt | 2 +- .../firestore/FieldValueParceler.kt | 2 +- .../firestore/FirestoreDocument.kt | 34 +++++++++---------- .../otaliastudios/firestore/FirestoreList.kt | 18 +++++----- .../firestore/FirestoreLogger.kt | 12 +++---- .../otaliastudios/firestore/FirestoreMap.kt | 16 ++++----- .../firestore/FirestoreParceler.kt | 6 ++-- .../firestore/FirestoreParcelers.kt | 4 +-- .../firestore/TimestampParceler.kt | 2 +- .../firestore/batch/FirestoreBatchWrite.kt | 6 ++-- .../firestore/batch/FirestoreBatchWriter.kt | 4 +-- 13 files changed, 65 insertions(+), 61 deletions(-) diff --git a/firestore/build.gradle.kts b/firestore/build.gradle.kts index 8968f9c..b6d3b0c 100644 --- a/firestore/build.gradle.kts +++ b/firestore/build.gradle.kts @@ -36,6 +36,12 @@ android { buildTypes { getByName("release").consumerProguardFile("proguard-rules.pro") } + + kotlinOptions { + // Until the explicitApi() works in the Kotlin block... + // https://youtrack.jetbrains.com/issue/KT-37652 + freeCompilerArgs += listOf("-Xexplicit-api=strict") + } } dependencies { diff --git a/firestore/src/main/kotlin/com/otaliastudios/firestore/Annotations.kt b/firestore/src/main/kotlin/com/otaliastudios/firestore/Annotations.kt index 96baa5d..cc57930 100644 --- a/firestore/src/main/kotlin/com/otaliastudios/firestore/Annotations.kt +++ b/firestore/src/main/kotlin/com/otaliastudios/firestore/Annotations.kt @@ -9,16 +9,16 @@ import androidx.annotation.Keep @Keep @Target(AnnotationTarget.CLASS) @Retention(AnnotationRetention.RUNTIME) -annotation class FirestoreClass +public annotation class FirestoreClass @Keep public interface FirestoreMetadata { - fun create(key: String): T? - fun isNullable(key: String): Boolean - fun getBindableResource(key: String): Int? - fun createInnerType(): T? + public fun create(key: String): T? + public fun isNullable(key: String): Boolean + public fun getBindableResource(key: String): Int? + public fun createInnerType(): T? - companion object { - const val SUFFIX = "MetadataImpl" + public companion object { + public const val SUFFIX: String = "MetadataImpl" } } \ No newline at end of file diff --git a/firestore/src/main/kotlin/com/otaliastudios/firestore/DocumentReferenceParceler.kt b/firestore/src/main/kotlin/com/otaliastudios/firestore/DocumentReferenceParceler.kt index 0e97a3d..11c729a 100644 --- a/firestore/src/main/kotlin/com/otaliastudios/firestore/DocumentReferenceParceler.kt +++ b/firestore/src/main/kotlin/com/otaliastudios/firestore/DocumentReferenceParceler.kt @@ -8,7 +8,7 @@ import com.google.firebase.firestore.FirebaseFirestore * Parcels a possibly null DocumentReference. * Uses FirebaseFirestore.getInstance(). */ -object DocumentReferenceParceler: FirestoreParceler { +public object DocumentReferenceParceler: FirestoreParceler { override fun create(parcel: Parcel): DocumentReference { return FirebaseFirestore.getInstance().document(parcel.readString()!!) diff --git a/firestore/src/main/kotlin/com/otaliastudios/firestore/FieldValueParceler.kt b/firestore/src/main/kotlin/com/otaliastudios/firestore/FieldValueParceler.kt index 6f51f65..3b9606f 100644 --- a/firestore/src/main/kotlin/com/otaliastudios/firestore/FieldValueParceler.kt +++ b/firestore/src/main/kotlin/com/otaliastudios/firestore/FieldValueParceler.kt @@ -6,7 +6,7 @@ import com.google.firebase.firestore.FieldValue /** * Parcels a FieldValue */ -object FieldValueParceler: FirestoreParceler { +public object FieldValueParceler: FirestoreParceler { override fun create(parcel: Parcel): FieldValue { val what = parcel.readString() diff --git a/firestore/src/main/kotlin/com/otaliastudios/firestore/FirestoreDocument.kt b/firestore/src/main/kotlin/com/otaliastudios/firestore/FirestoreDocument.kt index cebaa28..d9fffbc 100755 --- a/firestore/src/main/kotlin/com/otaliastudios/firestore/FirestoreDocument.kt +++ b/firestore/src/main/kotlin/com/otaliastudios/firestore/FirestoreDocument.kt @@ -19,15 +19,15 @@ import kotlin.reflect.KClass * The base document class. */ @Keep -abstract class FirestoreDocument( - @get:Exclude var collection: String? = null, - @get:Exclude var id: String? = null, +public abstract class FirestoreDocument( + @get:Exclude public var collection: String? = null, + @get:Exclude public var id: String? = null, source: Map? = null ) : FirestoreMap(source = source) { @Suppress("MemberVisibilityCanBePrivate", "RedundantModalityModifier") @Exclude - final fun isNew(): Boolean { + public final fun isNew(): Boolean { return createdAt == null } @@ -40,24 +40,24 @@ abstract class FirestoreDocument( } @Exclude - fun getReference(): DocumentReference { + public fun getReference(): DocumentReference { if (id == null) throw IllegalStateException("Cant return reference for unsaved data.") return requireReference() } @get:Keep @set:Keep - var createdAt: Timestamp? by this + public var createdAt: Timestamp? by this @get:Keep @set:Keep - var updatedAt: Timestamp? by this + public var updatedAt: Timestamp? by this internal var cacheState: CacheState = CacheState.FRESH @Exclude - fun getCacheState() = cacheState + public fun getCacheState(): CacheState = cacheState @Exclude - fun delete(): Task { + public fun delete(): Task { @Suppress("UNCHECKED_CAST") return getReference().delete() as Task } @@ -72,7 +72,7 @@ abstract class FirestoreDocument( } @Exclude - fun save(): Task { + public fun save(): Task { return when { isNew() -> create() else -> update() @@ -168,7 +168,7 @@ abstract class FirestoreDocument( } @Exclude - fun trySave(vararg updates: Pair): Task { + public fun trySave(vararg updates: Pair): Task { if (isNew()) throw IllegalStateException("Can not trySave a new object. Please call save() first.") val reference = requireReference() val values = updates.toMap().toMutableMap() @@ -201,13 +201,11 @@ abstract class FirestoreDocument( super.equals(other) } - companion object { + public companion object { @SuppressLint("StaticFieldLeak") internal val FIRESTORE = FirebaseFirestore.getInstance().apply { - firestoreSettings = FirebaseFirestoreSettings.Builder() - .setTimestampsInSnapshotsEnabled(true) - .build() + firestoreSettings = FirebaseFirestoreSettings.Builder().build() } internal val CACHE = LruCache(100) @@ -231,12 +229,12 @@ abstract class FirestoreDocument( FirestoreParcelers.add(FieldValue::class, FieldValueParceler) } - fun getCached(id: String, type: KClass): T? { + public fun getCached(id: String, type: KClass): T? { @Suppress("UNCHECKED_CAST") return CACHE.get(id) as? T } - inline fun getCached(id: String): T? { + public inline fun getCached(id: String): T? { return getCached(id, T::class) } } @@ -253,7 +251,7 @@ abstract class FirestoreDocument( collection = bundle.getString("collection", null) } - enum class CacheState { + public enum class CacheState { FRESH, CACHED_EQUAL, CACHED_CHANGED } } diff --git a/firestore/src/main/kotlin/com/otaliastudios/firestore/FirestoreList.kt b/firestore/src/main/kotlin/com/otaliastudios/firestore/FirestoreList.kt index eeeb1c2..63b820c 100644 --- a/firestore/src/main/kotlin/com/otaliastudios/firestore/FirestoreList.kt +++ b/firestore/src/main/kotlin/com/otaliastudios/firestore/FirestoreList.kt @@ -19,14 +19,14 @@ import com.google.firebase.firestore.Exclude * this list should be marked as dirty. */ @Keep -open class FirestoreList @JvmOverloads constructor( +public open class FirestoreList @JvmOverloads constructor( source: List? = null ) : /* ObservableList, MutableList by data, */Iterable, Parcelable { private val data: MutableList = mutableListOf() @get:Exclude - val size get() = data.size + public val size: Int get() = data.size init { if (source != null) { @@ -167,7 +167,7 @@ open class FirestoreList @JvmOverloads constructor( // Parcelable stuff. - override fun describeContents() = 0 + override fun describeContents(): Int = 0 override fun writeToParcel(parcel: Parcel, flags: Int) { val hashcode = hashCode() @@ -195,11 +195,11 @@ open class FirestoreList @JvmOverloads constructor( parcel.writeBundle(bundle) } - companion object { + public companion object { @Suppress("unused") @JvmField - public val CREATOR = object : Parcelable.ClassLoaderCreator> { + public val CREATOR: Parcelable.Creator> = object : Parcelable.ClassLoaderCreator> { override fun createFromParcel(source: Parcel): FirestoreList { // This should never be called by the framework. @@ -246,24 +246,24 @@ open class FirestoreList @JvmOverloads constructor( // Dirtyness stuff. - fun add(element: T): Boolean { + public fun add(element: T): Boolean { data.add(element) isDirty = true return true } - fun add(index: Int, element: T) { + public fun add(index: Int, element: T) { data.add(index, element) isDirty = true } - fun remove(element: T): Boolean { + public fun remove(element: T): Boolean { val result = data.remove(element) isDirty = true return result } - operator fun set(index: Int, element: T): T { + public operator fun set(index: Int, element: T): T { val value = data.set(index, element) isDirty = true return value diff --git a/firestore/src/main/kotlin/com/otaliastudios/firestore/FirestoreLogger.kt b/firestore/src/main/kotlin/com/otaliastudios/firestore/FirestoreLogger.kt index 252fc56..211d9b5 100644 --- a/firestore/src/main/kotlin/com/otaliastudios/firestore/FirestoreLogger.kt +++ b/firestore/src/main/kotlin/com/otaliastudios/firestore/FirestoreLogger.kt @@ -3,16 +3,16 @@ package com.otaliastudios.firestore import android.util.Log import timber.log.Timber -object FirestoreLogger { +public object FirestoreLogger { - const val VERBOSE = Log.VERBOSE - const val INFO = Log.INFO - const val WARN = Log.WARN - const val ERROR = Log.ERROR + public const val VERBOSE: Int = Log.VERBOSE + public const val INFO: Int = Log.INFO + public const val WARN: Int = Log.WARN + public const val ERROR: Int = Log.ERROR private var level = ERROR - fun setLevel(level: Int) { + public fun setLevel(level: Int) { this.level = level } diff --git a/firestore/src/main/kotlin/com/otaliastudios/firestore/FirestoreMap.kt b/firestore/src/main/kotlin/com/otaliastudios/firestore/FirestoreMap.kt index d8f267d..dd8af41 100644 --- a/firestore/src/main/kotlin/com/otaliastudios/firestore/FirestoreMap.kt +++ b/firestore/src/main/kotlin/com/otaliastudios/firestore/FirestoreMap.kt @@ -18,7 +18,7 @@ import kotlin.reflect.KProperty * Introduce dirtyness checking for childrens. */ @Keep -open class FirestoreMap( +public open class FirestoreMap( source: Map? = null ) : BaseObservable(), /*MutableMap by data,*/ Parcelable { @@ -26,10 +26,10 @@ open class FirestoreMap( private val dirty: MutableSet = mutableSetOf() @get:Exclude - val keys get() = data.keys + public val keys: Set get() = data.keys @get:Exclude - val size get() = data.size + public val size: Int get() = data.size init { if (source != null) { @@ -113,7 +113,7 @@ open class FirestoreMap( return candidate } - final operator fun set(key: String, value: T) { + public operator fun set(key: String, value: T) { val result = onSet(key, value) /* if (result == null) { // Do nothing. @@ -132,7 +132,7 @@ open class FirestoreMap( internal open fun onSet(key: String, value: T): T = value - final operator fun get(key: String): T? { + public operator fun get(key: String): T? { return if (key.contains('.')) { val first = key.split('.')[0] val second = key.removePrefix("$first.") @@ -283,7 +283,7 @@ open class FirestoreMap( return result } - override fun describeContents() = 0 + override fun describeContents(): Int = 0 override fun writeToParcel(parcel: Parcel, flags: Int) { val hashcode = hashCode() @@ -312,11 +312,11 @@ open class FirestoreMap( parcel.writeBundle(bundle) } - companion object { + public companion object { @Suppress("unused") @JvmField - public val CREATOR = object : Parcelable.ClassLoaderCreator> { + public val CREATOR: Parcelable.Creator> = object : Parcelable.ClassLoaderCreator> { override fun createFromParcel(source: Parcel): FirestoreMap { // This should never be called by the framework. diff --git a/firestore/src/main/kotlin/com/otaliastudios/firestore/FirestoreParceler.kt b/firestore/src/main/kotlin/com/otaliastudios/firestore/FirestoreParceler.kt index ca7e984..8678621 100644 --- a/firestore/src/main/kotlin/com/otaliastudios/firestore/FirestoreParceler.kt +++ b/firestore/src/main/kotlin/com/otaliastudios/firestore/FirestoreParceler.kt @@ -2,15 +2,15 @@ package com.otaliastudios.firestore import android.os.Parcel -interface FirestoreParceler { +public interface FirestoreParceler { /** * Writes the [T] instance state to the [parcel]. */ - fun write(data: T, parcel: Parcel, flags: Int) + public fun write(data: T, parcel: Parcel, flags: Int) /** * Reads the [T] instance state from the [parcel], constructs the new [T] instance and returns it. */ - fun create(parcel: Parcel): T + public fun create(parcel: Parcel): T } \ No newline at end of file diff --git a/firestore/src/main/kotlin/com/otaliastudios/firestore/FirestoreParcelers.kt b/firestore/src/main/kotlin/com/otaliastudios/firestore/FirestoreParcelers.kt index 37baecd..d7fa66b 100644 --- a/firestore/src/main/kotlin/com/otaliastudios/firestore/FirestoreParcelers.kt +++ b/firestore/src/main/kotlin/com/otaliastudios/firestore/FirestoreParcelers.kt @@ -14,11 +14,11 @@ import kotlin.reflect.KClass /** * Parceling engine. */ -object FirestoreParcelers { +public object FirestoreParcelers { internal val MAP = mutableMapOf>() - fun add(klass: KClass, parceler: FirestoreParceler) { + public fun add(klass: KClass, parceler: FirestoreParceler) { MAP[klass.java.name] = parceler } diff --git a/firestore/src/main/kotlin/com/otaliastudios/firestore/TimestampParceler.kt b/firestore/src/main/kotlin/com/otaliastudios/firestore/TimestampParceler.kt index 614ae4c..c941eaf 100644 --- a/firestore/src/main/kotlin/com/otaliastudios/firestore/TimestampParceler.kt +++ b/firestore/src/main/kotlin/com/otaliastudios/firestore/TimestampParceler.kt @@ -6,7 +6,7 @@ import com.google.firebase.Timestamp /** * Parcels a possibly null Timestamp. */ -object TimestampParceler: FirestoreParceler { +public object TimestampParceler: FirestoreParceler { override fun create(parcel: Parcel): Timestamp { return Timestamp(parcel.readLong(), parcel.readInt()) diff --git a/firestore/src/main/kotlin/com/otaliastudios/firestore/batch/FirestoreBatchWrite.kt b/firestore/src/main/kotlin/com/otaliastudios/firestore/batch/FirestoreBatchWrite.kt index 027ea64..bc1baf7 100644 --- a/firestore/src/main/kotlin/com/otaliastudios/firestore/batch/FirestoreBatchWrite.kt +++ b/firestore/src/main/kotlin/com/otaliastudios/firestore/batch/FirestoreBatchWrite.kt @@ -4,17 +4,17 @@ import com.google.android.gms.tasks.Task import com.google.android.gms.tasks.Tasks import com.otaliastudios.firestore.FirestoreDocument -class FirestoreBatchWrite internal constructor() { +public class FirestoreBatchWrite internal constructor() { private val batch = FirestoreDocument.FIRESTORE.batch() private val writer = FirestoreBatchWriter(batch) - fun perform(action: FirestoreBatchWriter.() -> Unit): FirestoreBatchWrite { + public fun perform(action: FirestoreBatchWriter.() -> Unit): FirestoreBatchWrite { action(writer) return this } - fun commit(): Task { + public fun commit(): Task { return batch.commit().addOnSuccessListener { writer.ops.forEach { it.notifySuccess() } }.addOnFailureListener { diff --git a/firestore/src/main/kotlin/com/otaliastudios/firestore/batch/FirestoreBatchWriter.kt b/firestore/src/main/kotlin/com/otaliastudios/firestore/batch/FirestoreBatchWriter.kt index 2520c18..08fab46 100644 --- a/firestore/src/main/kotlin/com/otaliastudios/firestore/batch/FirestoreBatchWriter.kt +++ b/firestore/src/main/kotlin/com/otaliastudios/firestore/batch/FirestoreBatchWriter.kt @@ -3,11 +3,11 @@ package com.otaliastudios.firestore.batch import com.google.firebase.firestore.WriteBatch import com.otaliastudios.firestore.FirestoreDocument -class FirestoreBatchWriter internal constructor(private val batch: WriteBatch) { +public class FirestoreBatchWriter internal constructor(private val batch: WriteBatch) { internal val ops = mutableListOf() - fun save(document: FirestoreDocument) { + public fun save(document: FirestoreDocument) { ops.add(document.save(batch)) } From 68088dfd57bb466ddba7d6ca15d0e5bb7cc47540 Mon Sep 17 00:00:00 2001 From: Mattia Iavarone Date: Tue, 8 Sep 2020 17:20:29 +0200 Subject: [PATCH 3/7] Better build checks --- .github/workflows/build.yml | 2 +- compiler/build.gradle.kts | 3 +++ firestore/build.gradle.kts | 3 +++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index eb7e3a0..fc0a309 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -16,4 +16,4 @@ jobs: with: java-version: 1.8 - name: Perform base checks - run: ./gradlew demo:assembleDebug firestore:dokka \ No newline at end of file + run: ./gradlew demo:assembleDebug publishToDirectory \ No newline at end of file diff --git a/compiler/build.gradle.kts b/compiler/build.gradle.kts index a1b22d8..672f227 100644 --- a/compiler/build.gradle.kts +++ b/compiler/build.gradle.kts @@ -36,4 +36,7 @@ publisher { auth.key = "BINTRAY_KEY" auth.repo = "BINTRAY_REPO" } + directory { + directory = "../build/maven" + } } diff --git a/firestore/build.gradle.kts b/firestore/build.gradle.kts index b6d3b0c..a309673 100644 --- a/firestore/build.gradle.kts +++ b/firestore/build.gradle.kts @@ -63,4 +63,7 @@ publisher { auth.key = "BINTRAY_KEY" auth.repo = "BINTRAY_REPO" } + directory { + directory = "../build/maven" + } } \ No newline at end of file From 9f8a15d8ffd06c0f6cfab68e7aa6715db6981a48 Mon Sep 17 00:00:00 2001 From: Mattia Iavarone Date: Tue, 8 Sep 2020 18:43:57 +0200 Subject: [PATCH 4/7] Refactor --- firestore/build.gradle.kts | 1 - .../otaliastudios/firestore/Annotations.kt | 24 ---- .../com/otaliastudios/firestore/Extensions.kt | 81 ------------- .../otaliastudios/firestore/FirestoreClass.kt | 12 ++ .../firestore/FirestoreDocument.kt | 106 ++++++------------ .../otaliastudios/firestore/FirestoreList.kt | 52 +++++---- .../firestore/FirestoreLogger.kt | 44 +++++--- .../otaliastudios/firestore/FirestoreMap.kt | 62 +++++----- .../firestore/FirestoreParceler.kt | 16 --- .../firestore/FirestoreParcelers.kt | 79 ------------- .../firestore/batch/FirestoreBatchWrite.kt | 26 ----- .../firestore/batch/FirestoreBatchWriter.kt | 17 --- .../com/otaliastudios/firestore/batchWrite.kt | 65 +++++++++++ .../com/otaliastudios/firestore/cache.kt | 33 ++++++ .../com/otaliastudios/firestore/config.kt | 19 ++++ .../com/otaliastudios/firestore/metadata.kt | 34 ++++++ .../{ => parcel}/DocumentReferenceParceler.kt | 5 +- .../{ => parcel}/FieldValueParceler.kt | 12 +- .../firestore/parcel/FirestoreParceler.kt | 35 ++++++ .../{ => parcel}/TimestampParceler.kt | 3 +- .../com/otaliastudios/firestore/parcel/api.kt | 76 +++++++++++++ .../com/otaliastudios/firestore/snapshots.kt | 64 +++++++++++ 22 files changed, 480 insertions(+), 386 deletions(-) delete mode 100644 firestore/src/main/kotlin/com/otaliastudios/firestore/Annotations.kt delete mode 100644 firestore/src/main/kotlin/com/otaliastudios/firestore/Extensions.kt create mode 100644 firestore/src/main/kotlin/com/otaliastudios/firestore/FirestoreClass.kt delete mode 100644 firestore/src/main/kotlin/com/otaliastudios/firestore/FirestoreParceler.kt delete mode 100644 firestore/src/main/kotlin/com/otaliastudios/firestore/FirestoreParcelers.kt delete mode 100644 firestore/src/main/kotlin/com/otaliastudios/firestore/batch/FirestoreBatchWrite.kt delete mode 100644 firestore/src/main/kotlin/com/otaliastudios/firestore/batch/FirestoreBatchWriter.kt create mode 100644 firestore/src/main/kotlin/com/otaliastudios/firestore/batchWrite.kt create mode 100644 firestore/src/main/kotlin/com/otaliastudios/firestore/cache.kt create mode 100644 firestore/src/main/kotlin/com/otaliastudios/firestore/config.kt create mode 100644 firestore/src/main/kotlin/com/otaliastudios/firestore/metadata.kt rename firestore/src/main/kotlin/com/otaliastudios/firestore/{ => parcel}/DocumentReferenceParceler.kt (76%) rename firestore/src/main/kotlin/com/otaliastudios/firestore/{ => parcel}/FieldValueParceler.kt (69%) create mode 100644 firestore/src/main/kotlin/com/otaliastudios/firestore/parcel/FirestoreParceler.kt rename firestore/src/main/kotlin/com/otaliastudios/firestore/{ => parcel}/TimestampParceler.kt (81%) create mode 100644 firestore/src/main/kotlin/com/otaliastudios/firestore/parcel/api.kt create mode 100644 firestore/src/main/kotlin/com/otaliastudios/firestore/snapshots.kt diff --git a/firestore/build.gradle.kts b/firestore/build.gradle.kts index a309673..b061986 100644 --- a/firestore/build.gradle.kts +++ b/firestore/build.gradle.kts @@ -46,7 +46,6 @@ android { dependencies { api("com.google.firebase:firebase-firestore-ktx:21.6.0") - api("com.jakewharton.timber:timber:4.7.1") } publisher { diff --git a/firestore/src/main/kotlin/com/otaliastudios/firestore/Annotations.kt b/firestore/src/main/kotlin/com/otaliastudios/firestore/Annotations.kt deleted file mode 100644 index cc57930..0000000 --- a/firestore/src/main/kotlin/com/otaliastudios/firestore/Annotations.kt +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright (c) 2018 Otalia Studios. Author: Mattia Iavarone. - */ - -package com.otaliastudios.firestore - -import androidx.annotation.Keep - -@Keep -@Target(AnnotationTarget.CLASS) -@Retention(AnnotationRetention.RUNTIME) -public annotation class FirestoreClass - -@Keep -public interface FirestoreMetadata { - public fun create(key: String): T? - public fun isNullable(key: String): Boolean - public fun getBindableResource(key: String): Int? - public fun createInnerType(): T? - - public companion object { - public const val SUFFIX: String = "MetadataImpl" - } -} \ No newline at end of file diff --git a/firestore/src/main/kotlin/com/otaliastudios/firestore/Extensions.kt b/firestore/src/main/kotlin/com/otaliastudios/firestore/Extensions.kt deleted file mode 100644 index 778b205..0000000 --- a/firestore/src/main/kotlin/com/otaliastudios/firestore/Extensions.kt +++ /dev/null @@ -1,81 +0,0 @@ -package com.otaliastudios.firestore - -import com.google.android.gms.tasks.Task -import com.google.firebase.firestore.DocumentSnapshot -import com.otaliastudios.firestore.batch.FirestoreBatchWrite -import com.otaliastudios.firestore.batch.FirestoreBatchWriter -import kotlin.reflect.KClass - -@Suppress("UNCHECKED_CAST", "RedundantVisibilityModifier") -public fun DocumentSnapshot.toFirestoreDocument(type: KClass, cache: Boolean = true): T { - var needsCacheState = false - val result = if (cache) { - val cached = FirestoreDocument.CACHE.get(reference.id) as? T - if (cached == null) { - FirestoreLogger.i { "Id ${reference.id} asked for cache. Cache miss." } - val new = type.java.newInstance() - new.clearDirt() // Clear dirtyness from init(). - FirestoreDocument.CACHE.put(reference.id, new) - new.cacheState = FirestoreDocument.CacheState.FRESH - new - } else { - if (metadata.isFromCache) { - FirestoreLogger.i { "Id ${reference.id} asked for cache. Was found. Using CACHED_EQUAL because metadata.isFromCache." } - cached.cacheState = FirestoreDocument.CacheState.CACHED_EQUAL - } else { - FirestoreLogger.i { "Id ${reference.id} asked for cache. Was found. We'll see if something changed." } - needsCacheState = true - /* val map = mutableMapOf() - cached.collectAllValues(map, "") - cached.cacheState = if (map.any { get(it.key) != it.value }) { - FirestoreLogger.v("Id ${reference.id} asked for cache. Was found. Using CACHED_CHANGED because some values were different.") - FirestoreDocument.CacheState.CACHED_CHANGED - } else { - FirestoreLogger.v("Id ${reference.id} asked for cache. Was found. Using CACHED_EQUAL because everything matches.") - FirestoreDocument.CacheState.CACHED_EQUAL - } */ - } - cached - } - } else { - FirestoreLogger.i { "Id ${reference.id} created with no cache." } - val new = type.java.newInstance() - new.clearDirt() // Clear dirtyness from init(). - FirestoreDocument.CACHE.put(reference.id, new) - new.cacheState = FirestoreDocument.CacheState.FRESH - new - } - result.id = reference.id - result.collection = reference.parent.path - val changed = result.mergeValues(data!!, needsCacheState, reference.id) - if (needsCacheState) { - result.cacheState = if (changed) { - FirestoreLogger.v { "Id ${reference.id} Setting cache state to CACHED_CHANGED." } - FirestoreDocument.CacheState.CACHED_CHANGED - } else { - FirestoreLogger.v { "Id ${reference.id} Setting cache state to CACHED_EQUAL." } - FirestoreDocument.CacheState.CACHED_EQUAL - } - } - return result -} - -@Suppress("RedundantVisibilityModifier") -public inline fun DocumentSnapshot.toFirestoreDocument(cache: Boolean = true): T { - return toFirestoreDocument(T::class, cache) -} - -public fun firestoreListOf(vararg elements: T): FirestoreList { - return FirestoreList(elements.asList()) -} - -public fun firestoreMapOf(vararg pairs: Pair): FirestoreMap { - return FirestoreMap(pairs.toMap()) -} - -public fun batchWrite(updates: FirestoreBatchWriter.() -> Unit): Task { - return FirestoreBatchWrite().run { - perform(updates) - commit() - } -} \ No newline at end of file diff --git a/firestore/src/main/kotlin/com/otaliastudios/firestore/FirestoreClass.kt b/firestore/src/main/kotlin/com/otaliastudios/firestore/FirestoreClass.kt new file mode 100644 index 0000000..eddbefd --- /dev/null +++ b/firestore/src/main/kotlin/com/otaliastudios/firestore/FirestoreClass.kt @@ -0,0 +1,12 @@ +/* + * Copyright (c) 2018 Otalia Studios. Author: Mattia Iavarone. + */ + +package com.otaliastudios.firestore + +import androidx.annotation.Keep + +@Keep +@Target(AnnotationTarget.CLASS) +@Retention(AnnotationRetention.RUNTIME) +public annotation class FirestoreClass \ No newline at end of file diff --git a/firestore/src/main/kotlin/com/otaliastudios/firestore/FirestoreDocument.kt b/firestore/src/main/kotlin/com/otaliastudios/firestore/FirestoreDocument.kt index d9fffbc..4d1db36 100755 --- a/firestore/src/main/kotlin/com/otaliastudios/firestore/FirestoreDocument.kt +++ b/firestore/src/main/kotlin/com/otaliastudios/firestore/FirestoreDocument.kt @@ -4,15 +4,15 @@ package com.otaliastudios.firestore -import android.annotation.SuppressLint import android.os.Bundle -import android.util.LruCache import androidx.annotation.Keep -import com.google.android.gms.common.api.Batch import com.google.android.gms.tasks.Task import com.google.android.gms.tasks.Tasks import com.google.firebase.Timestamp import com.google.firebase.firestore.* +import com.otaliastudios.firestore.parcel.DocumentReferenceParceler +import com.otaliastudios.firestore.parcel.FieldValueParceler +import com.otaliastudios.firestore.parcel.TimestampParceler import kotlin.reflect.KClass /** @@ -51,11 +51,13 @@ public abstract class FirestoreDocument( @get:Keep @set:Keep public var updatedAt: Timestamp? by this - internal var cacheState: CacheState = CacheState.FRESH + internal var cacheState: FirestoreCacheState = FirestoreCacheState.FRESH + @Suppress("unused") @Exclude - public fun getCacheState(): CacheState = cacheState + public fun getCacheState(): FirestoreCacheState = cacheState + @Suppress("unused") @Exclude public fun delete(): Task { @Suppress("UNCHECKED_CAST") @@ -63,14 +65,15 @@ public abstract class FirestoreDocument( } @Exclude - internal fun delete(batch: WriteBatch): BatchOp { + internal fun delete(batch: WriteBatch): FirestoreBatchOp { batch.delete(getReference()) - return object: BatchOp { + return object: FirestoreBatchOp { override fun notifyFailure() {} override fun notifySuccess() {} } } + @Suppress("unused") @Exclude public fun save(): Task { return when { @@ -80,7 +83,7 @@ public abstract class FirestoreDocument( } @Exclude - internal fun save(batch: WriteBatch): BatchOp { + internal fun save(batch: WriteBatch): FirestoreBatchOp { return when { isNew() -> create(batch) else -> update(batch) @@ -112,9 +115,9 @@ public abstract class FirestoreDocument( // Add to cache NOW, then eventually revert. // This is because when reference.set() succeeds, any query listener is notified // before our onSuccessTask() is called. So a new item is created. - FirestoreDocument.CACHE.put(reference.id, this) + FirestoreCache[reference.id] = this return reference.set(map).addOnFailureListener { - FirestoreDocument.CACHE.remove(reference.id) + FirestoreCache.remove(reference.id) }.onSuccessTask { id = reference.id createdAt = Timestamp.now() @@ -125,13 +128,13 @@ public abstract class FirestoreDocument( } } - private fun update(batch: WriteBatch): BatchOp { + private fun update(batch: WriteBatch): FirestoreBatchOp { if (isNew()) throw IllegalStateException("Can not update a new object. Please call create().") val map = mutableMapOf() flattenValues(map, prefix = "", dirtyOnly = true) map["updatedAt"] = FieldValue.serverTimestamp() batch.update(getReference(), map) - return object: BatchOp { + return object: FirestoreBatchOp { override fun notifyFailure() {} override fun notifySuccess() { updatedAt = Timestamp.now() @@ -140,17 +143,17 @@ public abstract class FirestoreDocument( } } - private fun create(batch: WriteBatch): BatchOp { + private fun create(batch: WriteBatch): FirestoreBatchOp { if (!isNew()) throw IllegalStateException("Can not create an existing object.") val reference = requireReference() val map = collectValues(dirtyOnly = false).toMutableMap() map["createdAt"] = FieldValue.serverTimestamp() map["updatedAt"] = FieldValue.serverTimestamp() batch.set(reference, map) - FirestoreDocument.CACHE.put(reference.id, this) - return object: BatchOp { + FirestoreCache[reference.id] = this + return object: FirestoreBatchOp { override fun notifyFailure() { - FirestoreDocument.CACHE.remove(reference.id) + FirestoreCache.remove(reference.id) } override fun notifySuccess() { @@ -162,11 +165,7 @@ public abstract class FirestoreDocument( } } - internal interface BatchOp { - fun notifySuccess() - fun notifyFailure() - } - + @Suppress("unused") @Exclude public fun trySave(vararg updates: Pair): Task { if (isNew()) throw IllegalStateException("Can not trySave a new object. Please call save() first.") @@ -193,52 +192,6 @@ public abstract class FirestoreDocument( } } - override fun equals(other: Any?): Boolean { - if (this === other) return true - return other is FirestoreDocument && - other.id == this.id && - other.collection == this.collection && - super.equals(other) - } - - public companion object { - - @SuppressLint("StaticFieldLeak") - internal val FIRESTORE = FirebaseFirestore.getInstance().apply { - firestoreSettings = FirebaseFirestoreSettings.Builder().build() - } - - internal val CACHE = LruCache(100) - - private val METADATA_PROVIDERS = mutableMapOf() - - internal fun metadataProvider(klass: KClass<*>): FirestoreMetadata { - val name = klass.java.name - if (!METADATA_PROVIDERS.containsKey(name)) { - val classPackage = klass.java.`package`!!.name - val className = klass.java.simpleName - val metadata = Class.forName("$classPackage.$className${FirestoreMetadata.SUFFIX}") - METADATA_PROVIDERS[name] = metadata.newInstance() as FirestoreMetadata - } - return METADATA_PROVIDERS[name] as FirestoreMetadata - } - - init { - FirestoreParcelers.add(DocumentReference::class, DocumentReferenceParceler) - FirestoreParcelers.add(Timestamp::class, TimestampParceler) - FirestoreParcelers.add(FieldValue::class, FieldValueParceler) - } - - public fun getCached(id: String, type: KClass): T? { - @Suppress("UNCHECKED_CAST") - return CACHE.get(id) as? T - } - - public inline fun getCached(id: String): T? { - return getCached(id, T::class) - } - } - override fun onWriteToBundle(bundle: Bundle) { super.onWriteToBundle(bundle) bundle.putString("id", id) @@ -251,7 +204,22 @@ public abstract class FirestoreDocument( collection = bundle.getString("collection", null) } - public enum class CacheState { - FRESH, CACHED_EQUAL, CACHED_CHANGED + override fun equals(other: Any?): Boolean { + if (this === other) return true + return other is FirestoreDocument && + other.id == this.id && + other.collection == this.collection && + super.equals(other) + } + + public companion object { + @Deprecated(message = "Use FirestoreCache directly.", replaceWith = ReplaceWith("FirestoreCache.get")) + public fun getCached(id: String, type: KClass): T? + = FirestoreCache.get(id, type) + + @Suppress("unused") + @Deprecated(message = "Use FirestoreCache directly.", replaceWith = ReplaceWith("FirestoreCache.get")) + public inline fun getCached(id: String): T? + = FirestoreCache[id] } } diff --git a/firestore/src/main/kotlin/com/otaliastudios/firestore/FirestoreList.kt b/firestore/src/main/kotlin/com/otaliastudios/firestore/FirestoreList.kt index 63b820c..7844103 100644 --- a/firestore/src/main/kotlin/com/otaliastudios/firestore/FirestoreList.kt +++ b/firestore/src/main/kotlin/com/otaliastudios/firestore/FirestoreList.kt @@ -9,20 +9,30 @@ import android.os.Parcel import android.os.Parcelable import androidx.annotation.Keep import com.google.firebase.firestore.Exclude +import com.otaliastudios.firestore.parcel.readValue +import com.otaliastudios.firestore.parcel.writeValue /** - * A list implementation. Delegates to a mutable list. - * - * The point of list is dirtyness. + * Creates a [FirestoreList] for the given values. + */ +@Suppress("unused") +public fun firestoreListOf(vararg values: T): FirestoreList { + return FirestoreList(values.asList()) +} + +/** + * A [FirestoreList] can be used to represent firestore lists. + * Implements list methods by delegates to a mutable list under the hood. * - * When an item is inserted, removed, or when a inner FirestoreMap/List is changed, - * this list should be marked as dirty. + * Whenever an item is inserted, removed, or when a inner Map/List is changed, + * this list will be marked as dirty. */ @Keep public open class FirestoreList @JvmOverloads constructor( source: List? = null -) : /* ObservableList, MutableList by data, */Iterable, Parcelable { +) : Iterable, Parcelable { + private val log = FirestoreLogger("FirestoreList") private val data: MutableList = mutableListOf() @get:Exclude @@ -110,12 +120,12 @@ public open class FirestoreList @JvmOverloads constructor( } protected open fun onCreateFirestoreMap(): FirestoreMap { - val provider = FirestoreDocument.metadataProvider(this::class) + val provider = this::class.metadataProvider return provider.createInnerType>() ?: FirestoreMap() } protected open fun onCreateFirestoreList(): FirestoreList { - val provider = FirestoreDocument.metadataProvider(this::class) + val provider = this::class.metadataProvider return provider.createInnerType>() ?: FirestoreList() } @@ -174,36 +184,38 @@ public open class FirestoreList @JvmOverloads constructor( parcel.writeInt(hashcode) // Write class name - FirestoreLogger.i { "List $hashcode: writing class ${this::class.java.name}" } + log.i { "List $hashcode: writing class ${this::class.java.name}" } parcel.writeString(this::class.java.name) // Write size and dirtiness - FirestoreLogger.v { "List $hashcode: writing dirty $isDirty and size $size" } + log.v { "List $hashcode: writing dirty $isDirty and size $size" } parcel.writeInt(if (isDirty) 1 else 0) parcel.writeInt(size) // Write actual data for (value in data) { - FirestoreLogger.v { "List $hashcode: writing value $value" } - FirestoreParcelers.write(parcel, value, hashcode.toString()) + log.v { "List $hashcode: writing value $value" } + parcel.writeValue(value, hashcode.toString()) } // Extra bundle val bundle = Bundle() onWriteToBundle(bundle) - FirestoreLogger.v { "List $hashcode: writing extra bundle. Size is ${bundle.size()}" } + log.v { "List $hashcode: writing extra bundle. Size is ${bundle.size()}" } parcel.writeBundle(bundle) } public companion object { + private val LOG = FirestoreLogger("FirestoreList") + @Suppress("unused") @JvmField public val CREATOR: Parcelable.Creator> = object : Parcelable.ClassLoaderCreator> { override fun createFromParcel(source: Parcel): FirestoreList { // This should never be called by the framework. - FirestoreLogger.e { "List: received call to createFromParcel without classLoader." } + LOG.e { "List: received call to createFromParcel without classLoader." } return createFromParcel(source, FirestoreList::class.java.classLoader!!) } @@ -211,25 +223,25 @@ public open class FirestoreList @JvmOverloads constructor( val hashcode = parcel.readInt() // Read class name val klass = Class.forName(parcel.readString()!!) - FirestoreLogger.i { "List $hashcode: read class ${klass.simpleName}" } + LOG.i { "List $hashcode: read class ${klass.simpleName}" } @Suppress("UNCHECKED_CAST") val dataList = klass.newInstance() as FirestoreList // Read dirtyness and size dataList.isDirty = parcel.readInt() == 1 val count = parcel.readInt() - FirestoreLogger.v { "List $hashcode: read dirtyness ${dataList.isDirty} and size $count" } + LOG.v { "List $hashcode: read dirtyness ${dataList.isDirty} and size $count" } // Read actual data repeat(count) { - FirestoreLogger.v { "List $hashcode: reading value..." } - dataList.data.add(FirestoreParcelers.read(parcel, loader, hashcode.toString())!!) + LOG.v { "List $hashcode: reading value..." } + dataList.data.add(parcel.readValue(loader, hashcode.toString())!!) } // Extra bundle - FirestoreLogger.v { "List $hashcode: reading extra bundle." } + LOG.v { "List $hashcode: reading extra bundle." } val bundle = parcel.readBundle(loader)!! - FirestoreLogger.v { "List $hashcode: read extra bundle, size ${bundle.size()}" } + LOG.v { "List $hashcode: read extra bundle, size ${bundle.size()}" } dataList.onReadFromBundle(bundle) return dataList } diff --git a/firestore/src/main/kotlin/com/otaliastudios/firestore/FirestoreLogger.kt b/firestore/src/main/kotlin/com/otaliastudios/firestore/FirestoreLogger.kt index 211d9b5..11c19bb 100644 --- a/firestore/src/main/kotlin/com/otaliastudios/firestore/FirestoreLogger.kt +++ b/firestore/src/main/kotlin/com/otaliastudios/firestore/FirestoreLogger.kt @@ -1,50 +1,58 @@ +@file:Suppress("unused") + package com.otaliastudios.firestore import android.util.Log -import timber.log.Timber -public object FirestoreLogger { +/** + * Lazy logger used across the library. + * Use [setLevel] to configure the verbosity. + */ +public class FirestoreLogger internal constructor(private val tag: String) { - public const val VERBOSE: Int = Log.VERBOSE - public const val INFO: Int = Log.INFO - public const val WARN: Int = Log.WARN - public const val ERROR: Int = Log.ERROR + public companion object { + public const val VERBOSE: Int = Log.VERBOSE + public const val INFO: Int = Log.INFO + public const val WARN: Int = Log.WARN + public const val ERROR: Int = Log.ERROR - private var level = ERROR + private var level = ERROR - public fun setLevel(level: Int) { - this.level = level + @JvmStatic + public fun setLevel(level: Int) { + this.level = level + } } internal fun w(message: () -> String) { - if (level <= WARN) Timber.w(message()) + if (level <= WARN) Log.w(tag, message()) } internal fun w(throwable: Throwable, message: () -> String) { - if (level <= WARN) Timber.w(throwable, message()) + if (level <= WARN) Log.w(tag, message(), throwable) } internal fun e(message: () -> String) { - if (level <= ERROR) Timber.e(message()) + if (level <= ERROR) Log.e(tag, message()) } internal fun e(throwable: Throwable, message: () -> String) { - if (level <= ERROR) Timber.e(throwable, message()) + if (level <= ERROR) Log.e(tag, message(), throwable) } internal fun i(message: () -> String) { - if (level <= INFO) Timber.i(message()) + if (level <= INFO) Log.i(tag, message()) } - internal fun i(throwable: Throwable, message: String) { - if (level <= INFO) Timber.i(throwable, message) + internal fun i(throwable: Throwable, message: () -> String) { + if (level <= INFO) Log.i(tag, message(), throwable) } internal fun v(message: () -> String) { - if (level <= VERBOSE) Timber.v(message()) + if (level <= VERBOSE) Log.v(tag, message()) } internal fun v(throwable: Throwable, message: () -> String) { - if (level <= VERBOSE) Timber.v(throwable, message()) + if (level <= VERBOSE) Log.v(tag, message(), throwable) } } \ No newline at end of file diff --git a/firestore/src/main/kotlin/com/otaliastudios/firestore/FirestoreMap.kt b/firestore/src/main/kotlin/com/otaliastudios/firestore/FirestoreMap.kt index dd8af41..18132ce 100644 --- a/firestore/src/main/kotlin/com/otaliastudios/firestore/FirestoreMap.kt +++ b/firestore/src/main/kotlin/com/otaliastudios/firestore/FirestoreMap.kt @@ -10,18 +10,26 @@ import android.os.Parcelable import androidx.annotation.Keep import androidx.databinding.BaseObservable import com.google.firebase.firestore.Exclude -import java.util.LinkedHashMap +import com.otaliastudios.firestore.parcel.readValue +import com.otaliastudios.firestore.parcel.writeValue import kotlin.reflect.KProperty /** - * A map implementation. Delegates to a mutable map. - * Introduce dirtyness checking for childrens. + * Creates a [FirestoreMap] for the given key-value pairs. + */ +@Suppress("unused") +public fun firestoreMapOf(vararg pairs: Pair): FirestoreMap { + return FirestoreMap(pairs.toMap()) +} + +/** + * A [FirestoreMap] can be used to represent firestore JSON-like structures. + * Implements map methods by delegating to a mutable map under the hood. */ @Keep -public open class FirestoreMap( - source: Map? = null -) : BaseObservable(), /*MutableMap by data,*/ Parcelable { +public open class FirestoreMap(source: Map? = null) : BaseObservable(), Parcelable { + private val log = FirestoreLogger("FirestoreMap") private val data: MutableMap = mutableMapOf() private val dirty: MutableSet = mutableSetOf() @@ -98,7 +106,7 @@ public open class FirestoreMap( } protected open fun onCreateFirestoreMap(key: String): FirestoreMap { - val provider = FirestoreDocument.metadataProvider(this::class) + val provider = this::class.metadataProvider var candidate = provider.create>(key) candidate = candidate ?: provider.createInnerType() candidate = candidate ?: FirestoreMap() @@ -106,7 +114,7 @@ public open class FirestoreMap( } protected open fun onCreateFirestoreList(key: String): FirestoreList { - val provider = FirestoreDocument.metadataProvider(this::class) + val provider = this::class.metadataProvider var candidate = provider.create>(key) candidate = candidate ?: provider.createInnerType() candidate = candidate ?: FirestoreList() @@ -125,7 +133,7 @@ public open class FirestoreMap( } else { data[key] = result dirty.add(key) - val resource = FirestoreDocument.metadataProvider(this::class).getBindableResource(key) + val resource = this::class.metadataProvider.getBindableResource(key) if (resource != null) notifyPropertyChanged(resource) } } @@ -172,7 +180,7 @@ public open class FirestoreMap( var what = source[property.name] as R if (what == null) { - val provider = FirestoreDocument.metadataProvider(this::class) + val provider = this::class.metadataProvider if (!provider.isNullable(property.name)) { what = provider.create(property.name)!! source[property.name] = what @@ -240,7 +248,7 @@ public open class FirestoreMap( internal fun mergeValues(values: Map, checkChanges: Boolean, tag: String): Boolean { var changed = false for ((key, value) in values) { - FirestoreLogger.v { "$tag mergeValues: key $key with value $value, dirty: ${isDirty(key)}" } + log.v { "$tag mergeValues: key $key with value $value, dirty: ${isDirty(key)}" } if (isDirty(key)) continue if (value is Map<*, *> && value.keys.all { it is String }) { val child = get(key) ?: createFirestoreMap(key) as T // T @@ -258,7 +266,7 @@ public open class FirestoreMap( changed = changed || childChanged } else { if (checkChanges && !changed) { - FirestoreLogger.v { "$tag mergeValues: key $key comparing with value ${data[key]}" } + log.v { "$tag mergeValues: key $key comparing with value ${data[key]}" } changed = changed || value != data[key] } data[key] = value @@ -290,37 +298,39 @@ public open class FirestoreMap( parcel.writeInt(hashcode) // Write class name - FirestoreLogger.i { "Map $hashcode: writing class ${this::class.java.name}" } + log.i { "Map $hashcode: writing class ${this::class.java.name}" } parcel.writeString(this::class.java.name) // Write dirty data - FirestoreLogger.v { "Map $hashcode: writing dirty count ${dirty.size} and dirty keys ${dirty.toTypedArray().joinToString()} ${dirty.toTypedArray().size}." } + log.v { "Map $hashcode: writing dirty count ${dirty.size} and dirty keys ${dirty.toTypedArray().joinToString()} ${dirty.toTypedArray().size}." } parcel.writeInt(dirty.size) parcel.writeStringArray(dirty.toTypedArray()) - FirestoreLogger.v { "Map $hashcode: writing data size. $size" } + log.v { "Map $hashcode: writing data size. $size" } parcel.writeInt(size) for ((key, value) in data) { parcel.writeString(key) - FirestoreLogger.v { "Map $hashcode: writing value for key $key..." } - FirestoreParcelers.write(parcel, value, hashcode.toString()) + log.v { "Map $hashcode: writing value for key $key..." } + parcel.writeValue(value, hashcode.toString()) } val bundle = Bundle() onWriteToBundle(bundle) - FirestoreLogger.v { "Map $hashcode: writing extra bundle. Size is ${bundle.size()}" } + log.v { "Map $hashcode: writing extra bundle. Size is ${bundle.size()}" } parcel.writeBundle(bundle) } public companion object { + private val LOG = FirestoreLogger("FirestoreMap") + @Suppress("unused") @JvmField public val CREATOR: Parcelable.Creator> = object : Parcelable.ClassLoaderCreator> { override fun createFromParcel(source: Parcel): FirestoreMap { // This should never be called by the framework. - FirestoreLogger.e { "Map: received call to createFromParcel without classLoader." } + LOG.e { "Map: received call to createFromParcel without classLoader." } return createFromParcel(source, FirestoreMap::class.java.classLoader!!) } @@ -330,23 +340,23 @@ public open class FirestoreMap( // Read class and create the map object. val klass = Class.forName(parcel.readString()!!) - FirestoreLogger.i { "Map $hashcode: read class ${klass.simpleName}" } + LOG.i { "Map $hashcode: read class ${klass.simpleName}" } val firestoreMap = klass.newInstance() as FirestoreMap // Read dirty data val dirty = Array(parcel.readInt()) { "" } parcel.readStringArray(dirty) - FirestoreLogger.v { "Map $hashcode: read dirty count ${dirty.size} and array ${dirty.joinToString()}" } + LOG.v { "Map $hashcode: read dirty count ${dirty.size} and array ${dirty.joinToString()}" } // Read actual data val count = parcel.readInt() - FirestoreLogger.v { "Map $hashcode: read data size $count" } + LOG.v { "Map $hashcode: read data size $count" } val values = HashMap(count) repeat(count) { val key = parcel.readString()!! - FirestoreLogger.v { "Map $hashcode: reading value for key $key..." } - values[key] = FirestoreParcelers.read(parcel, loader, hashcode.toString()) + LOG.v { "Map $hashcode: reading value for key $key..." } + values[key] = parcel.readValue(loader, hashcode.toString()) } // Set both @@ -356,9 +366,9 @@ public open class FirestoreMap( firestoreMap.data.putAll(values) // Read the extra bundle - FirestoreLogger.v { "Map $hashcode: reading extra bundle." } + LOG.v { "Map $hashcode: reading extra bundle." } val bundle = parcel.readBundle(loader) - FirestoreLogger.v { "Map $hashcode: read extra bundle, size ${bundle?.size()}" } + LOG.v { "Map $hashcode: read extra bundle, size ${bundle?.size()}" } firestoreMap.onReadFromBundle(bundle!!) return firestoreMap } diff --git a/firestore/src/main/kotlin/com/otaliastudios/firestore/FirestoreParceler.kt b/firestore/src/main/kotlin/com/otaliastudios/firestore/FirestoreParceler.kt deleted file mode 100644 index 8678621..0000000 --- a/firestore/src/main/kotlin/com/otaliastudios/firestore/FirestoreParceler.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.otaliastudios.firestore - -import android.os.Parcel - -public interface FirestoreParceler { - - /** - * Writes the [T] instance state to the [parcel]. - */ - public fun write(data: T, parcel: Parcel, flags: Int) - - /** - * Reads the [T] instance state from the [parcel], constructs the new [T] instance and returns it. - */ - public fun create(parcel: Parcel): T -} \ No newline at end of file diff --git a/firestore/src/main/kotlin/com/otaliastudios/firestore/FirestoreParcelers.kt b/firestore/src/main/kotlin/com/otaliastudios/firestore/FirestoreParcelers.kt deleted file mode 100644 index d7fa66b..0000000 --- a/firestore/src/main/kotlin/com/otaliastudios/firestore/FirestoreParcelers.kt +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright (c) 2018 Otalia Studios. Author: Mattia Iavarone. - */ - -package com.otaliastudios.firestore - -import android.os.Parcel -import com.google.firebase.Timestamp -import com.google.firebase.firestore.DocumentReference -import com.google.firebase.firestore.FieldValue -import com.google.firebase.firestore.FirebaseFirestore -import kotlin.reflect.KClass - -/** - * Parceling engine. - */ -public object FirestoreParcelers { - - internal val MAP = mutableMapOf>() - - public fun add(klass: KClass, parceler: FirestoreParceler) { - MAP[klass.java.name] = parceler - } - - private const val NULL = 0 - private const val PARCELER = 1 - private const val VALUE = 2 - - internal fun write(parcel: Parcel, value: Any?, tag: String) { - if (value == null) { - FirestoreLogger.v { "$tag writeParcel: value is null." } - parcel.writeInt(NULL) - return - } - val klass = value::class.java.name - if (MAP.containsKey(klass)) { - FirestoreLogger.v { "$tag writeParcel: value $value will be written with parceler for class $klass." } - parcel.writeInt(PARCELER) - parcel.writeString(klass) - @Suppress("UNCHECKED_CAST") - val parceler = MAP[klass] as FirestoreParceler - parceler.write(value, parcel, 0) - return - } - try { - FirestoreLogger.v { "$tag writeParcel: value $value will be written with writeValue()." } - parcel.writeInt(VALUE) - parcel.writeValue(value) - } catch (e: Exception) { - FirestoreLogger.e(e) { "Could not write value $value. You need to add a FirestoreParceler." } - throw e - } - } - - internal fun read(parcel: Parcel, loader: ClassLoader, tag: String): Any? { - val what = parcel.readInt() - if (what == NULL) { - FirestoreLogger.v { "$tag readParcel: value is null." } - return null - } - if (what == PARCELER) { - val klass = parcel.readString()!! - FirestoreLogger.v { "$tag readParcel: value will be read by parceler $klass." } - @Suppress("UNCHECKED_CAST") - val parceler = MAP[klass] as FirestoreParceler - return parceler.create(parcel) - } - if (what == VALUE) { - val read = parcel.readValue(loader) - FirestoreLogger.v { "$tag readParcel: value was read by readValue: $read." } - return read - } - val e = IllegalStateException("$tag Error while reading parcel. Unexpected control int: $what") - FirestoreLogger.e(e) { e.message!! } - throw e - } - - -} \ No newline at end of file diff --git a/firestore/src/main/kotlin/com/otaliastudios/firestore/batch/FirestoreBatchWrite.kt b/firestore/src/main/kotlin/com/otaliastudios/firestore/batch/FirestoreBatchWrite.kt deleted file mode 100644 index bc1baf7..0000000 --- a/firestore/src/main/kotlin/com/otaliastudios/firestore/batch/FirestoreBatchWrite.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.otaliastudios.firestore.batch - -import com.google.android.gms.tasks.Task -import com.google.android.gms.tasks.Tasks -import com.otaliastudios.firestore.FirestoreDocument - -public class FirestoreBatchWrite internal constructor() { - - private val batch = FirestoreDocument.FIRESTORE.batch() - private val writer = FirestoreBatchWriter(batch) - - public fun perform(action: FirestoreBatchWriter.() -> Unit): FirestoreBatchWrite { - action(writer) - return this - } - - public fun commit(): Task { - return batch.commit().addOnSuccessListener { - writer.ops.forEach { it.notifySuccess() } - }.addOnFailureListener { - writer.ops.forEach { it.notifyFailure() } - }.onSuccessTask { - Tasks.forResult(Unit) - } - } -} \ No newline at end of file diff --git a/firestore/src/main/kotlin/com/otaliastudios/firestore/batch/FirestoreBatchWriter.kt b/firestore/src/main/kotlin/com/otaliastudios/firestore/batch/FirestoreBatchWriter.kt deleted file mode 100644 index 08fab46..0000000 --- a/firestore/src/main/kotlin/com/otaliastudios/firestore/batch/FirestoreBatchWriter.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.otaliastudios.firestore.batch - -import com.google.firebase.firestore.WriteBatch -import com.otaliastudios.firestore.FirestoreDocument - -public class FirestoreBatchWriter internal constructor(private val batch: WriteBatch) { - - internal val ops = mutableListOf() - - public fun save(document: FirestoreDocument) { - ops.add(document.save(batch)) - } - - public fun delete(document: FirestoreDocument) { - ops.add(document.delete(batch)) - } -} \ No newline at end of file diff --git a/firestore/src/main/kotlin/com/otaliastudios/firestore/batchWrite.kt b/firestore/src/main/kotlin/com/otaliastudios/firestore/batchWrite.kt new file mode 100644 index 0000000..00482e4 --- /dev/null +++ b/firestore/src/main/kotlin/com/otaliastudios/firestore/batchWrite.kt @@ -0,0 +1,65 @@ +package com.otaliastudios.firestore + +import com.google.android.gms.tasks.Task +import com.google.android.gms.tasks.Tasks +import com.google.firebase.firestore.WriteBatch + + +/** + * Initiates a batch write operation. + */ +@Suppress("unused") +public fun batchWrite(updates: FirestoreBatchWriter.() -> Unit): Task { + return FirestoreBatchWrite().run { + perform(updates) + commit() + } +} + +public class FirestoreBatchWriter internal constructor(private val batch: WriteBatch) { + + internal val ops = mutableListOf() + + /** + * Adds a document save to the batch operation. + */ + @Suppress("unused") + public fun save(document: FirestoreDocument) { + ops.add(document.save(batch)) + } + + + /** + * Adds a document deletion to the batch operation. + */ + @Suppress("unused") + public fun delete(document: FirestoreDocument) { + ops.add(document.delete(batch)) + } +} + +private class FirestoreBatchWrite { + + private val batch = FIRESTORE.batch() + private val writer = FirestoreBatchWriter(batch) + + fun perform(action: FirestoreBatchWriter.() -> Unit): FirestoreBatchWrite { + action(writer) + return this + } + + fun commit(): Task { + return batch.commit().addOnSuccessListener { + writer.ops.forEach { it.notifySuccess() } + }.addOnFailureListener { + writer.ops.forEach { it.notifyFailure() } + }.onSuccessTask { + Tasks.forResult(Unit) + } + } +} + +internal interface FirestoreBatchOp { + fun notifySuccess() + fun notifyFailure() +} \ No newline at end of file diff --git a/firestore/src/main/kotlin/com/otaliastudios/firestore/cache.kt b/firestore/src/main/kotlin/com/otaliastudios/firestore/cache.kt new file mode 100644 index 0000000..4bd3066 --- /dev/null +++ b/firestore/src/main/kotlin/com/otaliastudios/firestore/cache.kt @@ -0,0 +1,33 @@ +package com.otaliastudios.firestore + +import android.util.LruCache +import kotlin.reflect.KClass + +public object FirestoreCache { + // TODO make size configurable + private val cache = LruCache(100) + + @Suppress("UNCHECKED_CAST") + public fun get(id: String, type: KClass): T? { + val cached = cache[id] ?: return null + require(type.isInstance(cached)) { "Cached object is not of the given type: $type / ${cached::class}"} + return cached as T + } + + @Suppress("unused") + public inline operator fun get(id: String): T? { + return get(id, T::class) + } + + internal operator fun set(id: String, value: T) { + cache.put(id, value) + } + + internal fun remove(id: String) { + cache.remove(id) + } +} + +public enum class FirestoreCacheState { + FRESH, CACHED_EQUAL, CACHED_CHANGED +} \ No newline at end of file diff --git a/firestore/src/main/kotlin/com/otaliastudios/firestore/config.kt b/firestore/src/main/kotlin/com/otaliastudios/firestore/config.kt new file mode 100644 index 0000000..6101041 --- /dev/null +++ b/firestore/src/main/kotlin/com/otaliastudios/firestore/config.kt @@ -0,0 +1,19 @@ +package com.otaliastudios.firestore + +import android.util.LruCache +import com.google.firebase.firestore.ktx.firestore +import com.google.firebase.ktx.Firebase +import com.otaliastudios.firestore.parcel.DocumentReferenceParceler +import com.otaliastudios.firestore.parcel.FieldValueParceler +import com.otaliastudios.firestore.parcel.TimestampParceler +import com.otaliastudios.firestore.parcel.registerParceler + +// TODO make it configurable (FirebaseApp) +internal val FIRESTORE by lazy { + Firebase.firestore.also { + // One time setup + registerParceler(DocumentReferenceParceler) + registerParceler(TimestampParceler) + registerParceler(FieldValueParceler) + } +} \ No newline at end of file diff --git a/firestore/src/main/kotlin/com/otaliastudios/firestore/metadata.kt b/firestore/src/main/kotlin/com/otaliastudios/firestore/metadata.kt new file mode 100644 index 0000000..d85edfe --- /dev/null +++ b/firestore/src/main/kotlin/com/otaliastudios/firestore/metadata.kt @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2018 Otalia Studios. Author: Mattia Iavarone. + */ + +package com.otaliastudios.firestore + +import androidx.annotation.Keep +import kotlin.reflect.KClass + +@Keep +public interface FirestoreMetadata { + public fun create(key: String): T? + public fun isNullable(key: String): Boolean + public fun getBindableResource(key: String): Int? + public fun createInnerType(): T? + + public companion object { + public const val SUFFIX: String = "MetadataImpl" + } +} + +private val METADATA_PROVIDERS + = mutableMapOf() + +internal val KClass<*>.metadataProvider get(): FirestoreMetadata { + val name = java.name + if (!METADATA_PROVIDERS.containsKey(name)) { + val classPackage = java.`package`!!.name + val className = java.simpleName + val metadata = Class.forName("$classPackage.$className${FirestoreMetadata.SUFFIX}") + METADATA_PROVIDERS[name] = metadata.newInstance() as FirestoreMetadata + } + return METADATA_PROVIDERS[name] as FirestoreMetadata +} \ No newline at end of file diff --git a/firestore/src/main/kotlin/com/otaliastudios/firestore/DocumentReferenceParceler.kt b/firestore/src/main/kotlin/com/otaliastudios/firestore/parcel/DocumentReferenceParceler.kt similarity index 76% rename from firestore/src/main/kotlin/com/otaliastudios/firestore/DocumentReferenceParceler.kt rename to firestore/src/main/kotlin/com/otaliastudios/firestore/parcel/DocumentReferenceParceler.kt index 11c729a..b9e4550 100644 --- a/firestore/src/main/kotlin/com/otaliastudios/firestore/DocumentReferenceParceler.kt +++ b/firestore/src/main/kotlin/com/otaliastudios/firestore/parcel/DocumentReferenceParceler.kt @@ -1,12 +1,13 @@ -package com.otaliastudios.firestore +package com.otaliastudios.firestore.parcel import android.os.Parcel import com.google.firebase.firestore.DocumentReference import com.google.firebase.firestore.FirebaseFirestore +import com.otaliastudios.firestore.parcel.FirestoreParceler /** * Parcels a possibly null DocumentReference. - * Uses FirebaseFirestore.getInstance(). + * Note that it uses [FirebaseFirestore.getInstance] to do so. */ public object DocumentReferenceParceler: FirestoreParceler { diff --git a/firestore/src/main/kotlin/com/otaliastudios/firestore/FieldValueParceler.kt b/firestore/src/main/kotlin/com/otaliastudios/firestore/parcel/FieldValueParceler.kt similarity index 69% rename from firestore/src/main/kotlin/com/otaliastudios/firestore/FieldValueParceler.kt rename to firestore/src/main/kotlin/com/otaliastudios/firestore/parcel/FieldValueParceler.kt index 3b9606f..4e969e2 100644 --- a/firestore/src/main/kotlin/com/otaliastudios/firestore/FieldValueParceler.kt +++ b/firestore/src/main/kotlin/com/otaliastudios/firestore/parcel/FieldValueParceler.kt @@ -1,18 +1,18 @@ -package com.otaliastudios.firestore +package com.otaliastudios.firestore.parcel import android.os.Parcel import com.google.firebase.firestore.FieldValue +import com.otaliastudios.firestore.parcel.FirestoreParceler /** - * Parcels a FieldValue + * Parcels a [FieldValue]. */ public object FieldValueParceler: FirestoreParceler { override fun create(parcel: Parcel): FieldValue { - val what = parcel.readString() - when (what) { - "delete" -> return FieldValue.delete() - "timestamp" -> return FieldValue.serverTimestamp() + return when (val what = parcel.readString()) { + "delete" -> FieldValue.delete() + "timestamp" -> FieldValue.serverTimestamp() else -> throw RuntimeException("Unknown FieldValue value: $what") } } diff --git a/firestore/src/main/kotlin/com/otaliastudios/firestore/parcel/FirestoreParceler.kt b/firestore/src/main/kotlin/com/otaliastudios/firestore/parcel/FirestoreParceler.kt new file mode 100644 index 0000000..e2f4116 --- /dev/null +++ b/firestore/src/main/kotlin/com/otaliastudios/firestore/parcel/FirestoreParceler.kt @@ -0,0 +1,35 @@ +package com.otaliastudios.firestore.parcel + +import android.os.Parcel +import kotlin.reflect.KClass + +public interface FirestoreParceler { + + /** + * Writes the [T] instance state to the [parcel]. + */ + public fun write(data: T, parcel: Parcel, flags: Int) + + /** + * Reads the [T] instance state from the [parcel], constructs the new [T] instance and returns it. + */ + public fun create(parcel: Parcel): T + + public companion object { + /** + * Registers a new [FirestoreParceler]. + */ + @Suppress("unused") + public fun register(klass: KClass, parceler: FirestoreParceler) { + registerParceler(klass, parceler) + } + + /** + * Registers a new [FirestoreParceler]. + */ + @Suppress("unused") + public inline fun register(parceler: FirestoreParceler) { + registerParceler(T::class, parceler) + } + } +} \ No newline at end of file diff --git a/firestore/src/main/kotlin/com/otaliastudios/firestore/TimestampParceler.kt b/firestore/src/main/kotlin/com/otaliastudios/firestore/parcel/TimestampParceler.kt similarity index 81% rename from firestore/src/main/kotlin/com/otaliastudios/firestore/TimestampParceler.kt rename to firestore/src/main/kotlin/com/otaliastudios/firestore/parcel/TimestampParceler.kt index c941eaf..2612ae0 100644 --- a/firestore/src/main/kotlin/com/otaliastudios/firestore/TimestampParceler.kt +++ b/firestore/src/main/kotlin/com/otaliastudios/firestore/parcel/TimestampParceler.kt @@ -1,7 +1,8 @@ -package com.otaliastudios.firestore +package com.otaliastudios.firestore.parcel import android.os.Parcel import com.google.firebase.Timestamp +import com.otaliastudios.firestore.parcel.FirestoreParceler /** * Parcels a possibly null Timestamp. diff --git a/firestore/src/main/kotlin/com/otaliastudios/firestore/parcel/api.kt b/firestore/src/main/kotlin/com/otaliastudios/firestore/parcel/api.kt new file mode 100644 index 0000000..74b9dd9 --- /dev/null +++ b/firestore/src/main/kotlin/com/otaliastudios/firestore/parcel/api.kt @@ -0,0 +1,76 @@ +package com.otaliastudios.firestore.parcel + +import android.os.Parcel +import com.otaliastudios.firestore.FirestoreLogger +import kotlin.reflect.KClass + +private val PARCELERS = mutableMapOf>() + +/** + * Registers a new [FirestoreParceler]. + */ +public fun registerParceler(klass: KClass, parceler: FirestoreParceler) { + PARCELERS[klass.java.name] = parceler +} + +/** + * Registers a new [FirestoreParceler]. + */ +@Suppress("unused") +public inline fun registerParceler(parceler: FirestoreParceler) { + registerParceler(T::class, parceler) +} + +private val log = FirestoreLogger("Parcelers") +private const val NULL = 0 +private const val PARCELER = 1 +private const val VALUE = 2 + +internal fun Parcel.writeValue(value: Any?, tag: String) { + if (value == null) { + log.v { "$tag writeParcel: value is null." } + writeInt(NULL) + return + } + val klass = value::class.java.name + if (PARCELERS.containsKey(klass)) { + log.v { "$tag writeParcel: value $value will be written with parceler for class $klass." } + writeInt(PARCELER) + writeString(klass) + @Suppress("UNCHECKED_CAST") + val parceler = PARCELERS[klass] as FirestoreParceler + parceler.write(value, this, 0) + return + } + try { + log.v { "$tag writeParcel: value $value will be written with writeValue()." } + writeInt(VALUE) + writeValue(value) + } catch (e: Exception) { + log.e(e) { "Could not write value $value. You need to add a FirestoreParceler." } + throw e + } +} + +internal fun Parcel.readValue(loader: ClassLoader, tag: String): Any? { + val what = readInt() + if (what == NULL) { + log.v { "$tag readParcel: value is null." } + return null + } + if (what == PARCELER) { + val klass = readString()!! + log.v { "$tag readParcel: value will be read by parceler $klass." } + @Suppress("UNCHECKED_CAST") + val parceler = PARCELERS[klass] as FirestoreParceler + return parceler.create(this) + } + if (what == VALUE) { + val read = readValue(loader) + log.v { "$tag readParcel: value was read by readValue: $read." } + return read + } + val e = IllegalStateException("$tag Error while reading parcel. Unexpected control int: $what") + log.e(e) { e.message!! } + throw e +} diff --git a/firestore/src/main/kotlin/com/otaliastudios/firestore/snapshots.kt b/firestore/src/main/kotlin/com/otaliastudios/firestore/snapshots.kt new file mode 100644 index 0000000..52ec2d0 --- /dev/null +++ b/firestore/src/main/kotlin/com/otaliastudios/firestore/snapshots.kt @@ -0,0 +1,64 @@ +package com.otaliastudios.firestore + +import com.google.firebase.firestore.DocumentSnapshot +import kotlin.reflect.KClass + +private val log = FirestoreLogger("Snapshots") + +/** + * Converts the given [DocumentSnapshot] to a [FirestoreDocument]. + * The [cache] boolean tells whether we should inspect the cache before allocating a new object. + */ +@Suppress("unused") +public inline fun DocumentSnapshot.toFirestoreDocument(cache: Boolean = true): T { + return toFirestoreDocument(T::class, cache) +} + +/** + * Converts the given [DocumentSnapshot] to a [FirestoreDocument]. + * The [cache] boolean tells whether we should inspect the cache before allocating a new object. + */ +@Suppress("UNCHECKED_CAST") +public fun DocumentSnapshot.toFirestoreDocument(type: KClass, cache: Boolean = true): T { + var needsCacheState = false + val result = if (cache) { + val cached = FirestoreCache.get(reference.id, type) + if (cached == null) { + log.i { "Id ${reference.id} asked for cache. Cache miss." } + val new = type.java.newInstance() + new.clearDirt() // Clear dirtyness from init(). + FirestoreCache[reference.id] = new + new.cacheState = FirestoreCacheState.FRESH + new + } else { + if (metadata.isFromCache) { + log.i { "Id ${reference.id} asked for cache. Was found. Using CACHED_EQUAL because metadata.isFromCache." } + cached.cacheState = FirestoreCacheState.CACHED_EQUAL + } else { + log.i { "Id ${reference.id} asked for cache. Was found. We'll see if something changed." } + needsCacheState = true + } + cached + } + } else { + log.i { "Id ${reference.id} created with no cache." } + val new = type.java.newInstance() + new.clearDirt() // Clear dirtyness from init(). + FirestoreCache[reference.id] = new + new.cacheState = FirestoreCacheState.FRESH + new + } + result.id = reference.id + result.collection = reference.parent.path + val changed = result.mergeValues(data!!, needsCacheState, reference.id) + if (needsCacheState) { + result.cacheState = if (changed) { + log.v { "Id ${reference.id} Setting cache state to CACHED_CHANGED." } + FirestoreCacheState.CACHED_CHANGED + } else { + log.v { "Id ${reference.id} Setting cache state to CACHED_EQUAL." } + FirestoreCacheState.CACHED_EQUAL + } + } + return result +} From 7d25c1628828d434511009f9b455588bdae9b393 Mon Sep 17 00:00:00 2001 From: Mattia Iavarone Date: Tue, 8 Sep 2020 18:46:32 +0200 Subject: [PATCH 5/7] Update docs --- README.md | 8 ++++---- .../kotlin/com/otaliastudios/firestore/FirestoreClass.kt | 4 ++++ 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 86b89d0..b08417d 100644 --- a/README.md +++ b/README.md @@ -185,15 +185,15 @@ a parceler using `FirestoreDocument.registerParceler()`: class App : Application() { override fun onCreate() { - FirestoreDocument.registerParceler(GeoPoint::class, GeoPointParceler()) - FirestoreDocument.registerParceler(Whatever::class, WhateverParceler()) + registerParceler(GeoPointParceler) + registerParceler(WhateverParceler) } - class GeoPointParceler : FirestoreDocument.Parceler() { + object GeoPointParceler : FirestoreDocument.Parceler() { // ... } - class WhateverParceler : FirestoreDocument.Parceler() { + object WhateverParceler : FirestoreDocument.Parceler() { // ... } } diff --git a/firestore/src/main/kotlin/com/otaliastudios/firestore/FirestoreClass.kt b/firestore/src/main/kotlin/com/otaliastudios/firestore/FirestoreClass.kt index eddbefd..cd8e255 100644 --- a/firestore/src/main/kotlin/com/otaliastudios/firestore/FirestoreClass.kt +++ b/firestore/src/main/kotlin/com/otaliastudios/firestore/FirestoreClass.kt @@ -6,6 +6,10 @@ package com.otaliastudios.firestore import androidx.annotation.Keep +/** + * Identifies [FirestoreDocument], [FirestoreMap]s and [FirestoreList]s + * so that they can be processed by the annotation processor. + */ @Keep @Target(AnnotationTarget.CLASS) @Retention(AnnotationRetention.RUNTIME) From 4cb9bd4a29bcfbd75eba2a8af93b6c9a3ac67586 Mon Sep 17 00:00:00 2001 From: Mattia Iavarone Date: Tue, 8 Sep 2020 19:02:34 +0200 Subject: [PATCH 6/7] Catch exception #11 --- .../otaliastudios/firestore/FirestoreClass.kt | 3 +- .../otaliastudios/firestore/FirestoreList.kt | 16 ++++------- .../firestore/FirestoreLogger.kt | 2 +- .../otaliastudios/firestore/FirestoreMap.kt | 28 ++++++++----------- .../com/otaliastudios/firestore/metadata.kt | 24 ++++++++++------ 5 files changed, 37 insertions(+), 36 deletions(-) diff --git a/firestore/src/main/kotlin/com/otaliastudios/firestore/FirestoreClass.kt b/firestore/src/main/kotlin/com/otaliastudios/firestore/FirestoreClass.kt index cd8e255..0bb134e 100644 --- a/firestore/src/main/kotlin/com/otaliastudios/firestore/FirestoreClass.kt +++ b/firestore/src/main/kotlin/com/otaliastudios/firestore/FirestoreClass.kt @@ -13,4 +13,5 @@ import androidx.annotation.Keep @Keep @Target(AnnotationTarget.CLASS) @Retention(AnnotationRetention.RUNTIME) -public annotation class FirestoreClass \ No newline at end of file +public annotation class FirestoreClass +// Keep in sync with compiler! \ No newline at end of file diff --git a/firestore/src/main/kotlin/com/otaliastudios/firestore/FirestoreList.kt b/firestore/src/main/kotlin/com/otaliastudios/firestore/FirestoreList.kt index 7844103..a005fe4 100644 --- a/firestore/src/main/kotlin/com/otaliastudios/firestore/FirestoreList.kt +++ b/firestore/src/main/kotlin/com/otaliastudios/firestore/FirestoreList.kt @@ -104,29 +104,25 @@ public open class FirestoreList @JvmOverloads constructor( } private fun createFirestoreMap(): FirestoreMap { - val map = try { onCreateFirestoreMap() } catch (e: Exception) { - FirestoreMap() - } + val map = onCreateFirestoreMap() map.clearDirt() return map } private fun createFirestoreList(): FirestoreList { - val list = try { onCreateFirestoreList() } catch (e: Exception) { - FirestoreList() - } + val list = onCreateFirestoreList() list.clearDirt() return list } protected open fun onCreateFirestoreMap(): FirestoreMap { - val provider = this::class.metadataProvider - return provider.createInnerType>() ?: FirestoreMap() + val metadata = this::class.metadata + return metadata?.createInnerType>() ?: FirestoreMap() } protected open fun onCreateFirestoreList(): FirestoreList { - val provider = this::class.metadataProvider - return provider.createInnerType>() ?: FirestoreList() + val metadata = this::class.metadata + return metadata?.createInnerType>() ?: FirestoreList() } @Suppress("UNCHECKED_CAST") diff --git a/firestore/src/main/kotlin/com/otaliastudios/firestore/FirestoreLogger.kt b/firestore/src/main/kotlin/com/otaliastudios/firestore/FirestoreLogger.kt index 11c19bb..5ad2d0c 100644 --- a/firestore/src/main/kotlin/com/otaliastudios/firestore/FirestoreLogger.kt +++ b/firestore/src/main/kotlin/com/otaliastudios/firestore/FirestoreLogger.kt @@ -16,7 +16,7 @@ public class FirestoreLogger internal constructor(private val tag: String) { public const val WARN: Int = Log.WARN public const val ERROR: Int = Log.ERROR - private var level = ERROR + private var level = WARN @JvmStatic public fun setLevel(level: Int) { diff --git a/firestore/src/main/kotlin/com/otaliastudios/firestore/FirestoreMap.kt b/firestore/src/main/kotlin/com/otaliastudios/firestore/FirestoreMap.kt index 18132ce..a9e8280 100644 --- a/firestore/src/main/kotlin/com/otaliastudios/firestore/FirestoreMap.kt +++ b/firestore/src/main/kotlin/com/otaliastudios/firestore/FirestoreMap.kt @@ -90,33 +90,29 @@ public open class FirestoreMap(source: Map? = null) : BaseObservab private fun createFirestoreMap(key: String): FirestoreMap { - val map = try { onCreateFirestoreMap(key) } catch (e: Exception) { - FirestoreMap() - } + val map = onCreateFirestoreMap(key) map.clearDirt() return map } private fun createFirestoreList(key: String): FirestoreList { - val list = try { onCreateFirestoreList(key) } catch (e: Exception) { - FirestoreList() - } + val list = onCreateFirestoreList(key) list.clearDirt() return list } protected open fun onCreateFirestoreMap(key: String): FirestoreMap { - val provider = this::class.metadataProvider - var candidate = provider.create>(key) - candidate = candidate ?: provider.createInnerType() + val metadata = this::class.metadata + var candidate = metadata?.create>(key) + candidate = candidate ?: metadata?.createInnerType() candidate = candidate ?: FirestoreMap() return candidate } protected open fun onCreateFirestoreList(key: String): FirestoreList { - val provider = this::class.metadataProvider - var candidate = provider.create>(key) - candidate = candidate ?: provider.createInnerType() + val metadata = this::class.metadata + var candidate = metadata?.create>(key) + candidate = candidate ?: metadata?.createInnerType() candidate = candidate ?: FirestoreList() return candidate } @@ -133,7 +129,7 @@ public open class FirestoreMap(source: Map? = null) : BaseObservab } else { data[key] = result dirty.add(key) - val resource = this::class.metadataProvider.getBindableResource(key) + val resource = this::class.metadata?.getBindableResource(key) if (resource != null) notifyPropertyChanged(resource) } } @@ -180,9 +176,9 @@ public open class FirestoreMap(source: Map? = null) : BaseObservab var what = source[property.name] as R if (what == null) { - val provider = this::class.metadataProvider - if (!provider.isNullable(property.name)) { - what = provider.create(property.name)!! + val metadata = this::class.metadata + if (metadata != null && !metadata.isNullable(property.name)) { + what = metadata.create(property.name)!! source[property.name] = what // We don't want this to be dirty now! It was just retrieved, not really set. // If we leave it dirty, it would not be updated on next mergeValues(). diff --git a/firestore/src/main/kotlin/com/otaliastudios/firestore/metadata.kt b/firestore/src/main/kotlin/com/otaliastudios/firestore/metadata.kt index d85edfe..0c633ed 100644 --- a/firestore/src/main/kotlin/com/otaliastudios/firestore/metadata.kt +++ b/firestore/src/main/kotlin/com/otaliastudios/firestore/metadata.kt @@ -7,6 +7,8 @@ package com.otaliastudios.firestore import androidx.annotation.Keep import kotlin.reflect.KClass +// Keep in sync with compiler! +// And also proguard rules @Keep public interface FirestoreMetadata { public fun create(key: String): T? @@ -19,16 +21,22 @@ public interface FirestoreMetadata { } } -private val METADATA_PROVIDERS +private val log = FirestoreLogger("Metadata") + +private val METADATA = mutableMapOf() -internal val KClass<*>.metadataProvider get(): FirestoreMetadata { +internal val KClass<*>.metadata : FirestoreMetadata? get() { val name = java.name - if (!METADATA_PROVIDERS.containsKey(name)) { - val classPackage = java.`package`!!.name - val className = java.simpleName - val metadata = Class.forName("$classPackage.$className${FirestoreMetadata.SUFFIX}") - METADATA_PROVIDERS[name] = metadata.newInstance() as FirestoreMetadata + if (!METADATA.containsKey(name)) { + try { + val classPackage = java.`package`!!.name + val className = java.simpleName + val metadata = Class.forName("$classPackage.$className${FirestoreMetadata.SUFFIX}") + METADATA[name] = metadata.newInstance() as FirestoreMetadata + } catch (e: Exception) { + log.w(e) { "Error while fetching class metadata." } + } } - return METADATA_PROVIDERS[name] as FirestoreMetadata + return METADATA[name] } \ No newline at end of file From efc6e009669062960d163db8bb16c56d5e434fd9 Mon Sep 17 00:00:00 2001 From: Mattia Iavarone Date: Tue, 8 Sep 2020 19:14:42 +0200 Subject: [PATCH 7/7] Release v0.7.0 --- CHANGELOG.md | 16 ++++++++++++++++ README.md | 4 ++-- build.gradle.kts | 2 +- 3 files changed, 19 insertions(+), 3 deletions(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..af45bb2 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,16 @@ +Starting from v0.7.0, you can [support development](https://github.com/sponsors/natario1) through the GitHub Sponsors program. +Companies can share a tiny part of their revenue and get private support hours in return. Thanks! + +## v0.7.0 + +- New: Upgrade to Kotlin 1.4, Firestore 21.6.0 ([#12][12]) +- Breaking change: `FirestoreParcelers.add` is now `registerParceler` or `FirestoreParceler.register` ([#12][12]) +- Breaking change: `FirestoreDocument.CacheState` is now `FirestoreCacheState` ([#12][12]) +- Breaking change: parcelers moved to `com.otaliastudios.firestore.parcel.*` package ([#12][12]) +- Fix: do not crash exception when metadata is not present ([#12][12]) + + + +[natario1]: https://github.com/natario1 + +[12]: https://github.com/natario1/Firestore/pull/12 diff --git a/README.md b/README.md index b08417d..81a18ab 100644 --- a/README.md +++ b/README.md @@ -11,8 +11,8 @@ The lightweight, efficient wrapper for Firestore model data, written in Kotlin, with data-binding and Parcelable support. ```groovy -implementation 'com.otaliastudios:firestore:0.6.0' -kapt 'com.otaliastudios:firestore-compiler:0.6.0' +implementation 'com.otaliastudios:firestore:0.7.0' +kapt 'com.otaliastudios:firestore-compiler:0.7.0' ``` - Efficient and lightweight diff --git a/build.gradle.kts b/build.gradle.kts index 2e5f6fb..9ca7688 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -5,7 +5,7 @@ buildscript { extra["libDescription"] = "The efficient wrapper for Firestore model data." - extra["libVersion"] = "0.6.0" + extra["libVersion"] = "0.7.0" extra["libGroup"] = "com.otaliastudios" extra["githubUrl"] = "https://github.com/natario1/Firestore" extra["githubGit"] = "https://github.com/natario1/Firestore.git"