;
151 | }
152 |
153 | -keep class com.stripe.android.** { *; }
154 |
155 |
156 | -keepnames class kotlinx.** { *; }
157 |
158 | -ignorewarnings
159 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Attempt to set APP_HOME
10 | # Resolve links: $0 may be a link
11 | PRG="$0"
12 | # Need this for relative symlinks.
13 | while [ -h "$PRG" ] ; do
14 | ls=`ls -ld "$PRG"`
15 | link=`expr "$ls" : '.*-> \(.*\)$'`
16 | if expr "$link" : '/.*' > /dev/null; then
17 | PRG="$link"
18 | else
19 | PRG=`dirname "$PRG"`"/$link"
20 | fi
21 | done
22 | SAVED="`pwd`"
23 | cd "`dirname \"$PRG\"`/" >/dev/null
24 | APP_HOME="`pwd -P`"
25 | cd "$SAVED" >/dev/null
26 |
27 | APP_NAME="Gradle"
28 | APP_BASE_NAME=`basename "$0"`
29 |
30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
31 | DEFAULT_JVM_OPTS=""
32 |
33 | # Use the maximum available, or set MAX_FD != -1 to use that value.
34 | MAX_FD="maximum"
35 |
36 | warn () {
37 | echo "$*"
38 | }
39 |
40 | die () {
41 | echo
42 | echo "$*"
43 | echo
44 | exit 1
45 | }
46 |
47 | # OS specific support (must be 'true' or 'false').
48 | cygwin=false
49 | msys=false
50 | darwin=false
51 | nonstop=false
52 | case "`uname`" in
53 | CYGWIN* )
54 | cygwin=true
55 | ;;
56 | Darwin* )
57 | darwin=true
58 | ;;
59 | MINGW* )
60 | msys=true
61 | ;;
62 | NONSTOP* )
63 | nonstop=true
64 | ;;
65 | esac
66 |
67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
68 |
69 | # Determine the Java command to use to start the JVM.
70 | if [ -n "$JAVA_HOME" ] ; then
71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
72 | # IBM's JDK on AIX uses strange locations for the executables
73 | JAVACMD="$JAVA_HOME/jre/sh/java"
74 | else
75 | JAVACMD="$JAVA_HOME/bin/java"
76 | fi
77 | if [ ! -x "$JAVACMD" ] ; then
78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
79 |
80 | Please set the JAVA_HOME variable in your environment to match the
81 | location of your Java installation."
82 | fi
83 | else
84 | JAVACMD="java"
85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
86 |
87 | Please set the JAVA_HOME variable in your environment to match the
88 | location of your Java installation."
89 | fi
90 |
91 | # Increase the maximum file descriptors if we can.
92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
93 | MAX_FD_LIMIT=`ulimit -H -n`
94 | if [ $? -eq 0 ] ; then
95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
96 | MAX_FD="$MAX_FD_LIMIT"
97 | fi
98 | ulimit -n $MAX_FD
99 | if [ $? -ne 0 ] ; then
100 | warn "Could not set maximum file descriptor limit: $MAX_FD"
101 | fi
102 | else
103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
104 | fi
105 | fi
106 |
107 | # For Darwin, add options to specify how the application appears in the dock
108 | if $darwin; then
109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
110 | fi
111 |
112 | # For Cygwin, switch paths to Windows format before running java
113 | if $cygwin ; then
114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
116 | JAVACMD=`cygpath --unix "$JAVACMD"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Escape application args
158 | save () {
159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
160 | echo " "
161 | }
162 | APP_ARGS=$(save "$@")
163 |
164 | # Collect all arguments for the java command, following the shell quoting and substitution rules
165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
166 |
167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
169 | cd "$(dirname "$0")"
170 | fi
171 |
172 | exec "$JAVACMD" "$@"
173 |
--------------------------------------------------------------------------------
/presentation/src/main/kotlin/gluehome/common/presentation/ui/ProgressButton.kt:
--------------------------------------------------------------------------------
1 | package gluehome.common.presentation.ui
2 |
3 | import android.animation.Animator
4 | import android.animation.AnimatorListenerAdapter
5 | import android.animation.ValueAnimator
6 | import android.content.Context
7 | import android.graphics.Canvas
8 | import android.graphics.Color
9 | import android.graphics.ColorFilter
10 | import android.graphics.Paint
11 | import android.graphics.PixelFormat
12 | import android.graphics.Rect
13 | import android.graphics.RectF
14 | import android.graphics.drawable.Animatable
15 | import android.graphics.drawable.Drawable
16 | import android.util.AttributeSet
17 | import android.view.View
18 | import android.view.animation.DecelerateInterpolator
19 | import android.view.animation.Interpolator
20 | import android.view.animation.LinearInterpolator
21 | import gluehome.common.presentation.extensions.toDP
22 | import com.google.android.material.button.MaterialButton
23 |
24 | class ProgressButton : MaterialButton {
25 |
26 | private var animatedDrawable: CircularAnimationDrawable? = null
27 | private var currentText: CharSequence = ""
28 | private var state: State =
29 | State.IDLE
30 |
31 | constructor(context: Context) : super(context)
32 | constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
33 | constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
34 |
35 | override fun onDraw(canvas: Canvas) {
36 | super.onDraw(canvas)
37 | when (state) {
38 | State.LOADING -> drawIndeterminateProgress(canvas)
39 | State.IDLE -> stopIndeterminateProgress()
40 | }
41 | }
42 |
43 | fun startLoading() {
44 | isClickable = false
45 | if (state != State.IDLE) {
46 | return
47 | }
48 | state = State.LOADING
49 | currentText = text
50 | text = ""
51 | }
52 |
53 | fun stopLoading() {
54 | if (state != State.LOADING) {
55 | return
56 | }
57 | state = State.IDLE
58 | text = currentText
59 | isClickable = true
60 | }
61 |
62 | private fun drawIndeterminateProgress(canvas: Canvas) {
63 | if (animatedDrawable == null || !animatedDrawable!!.isRunning) {
64 | animatedDrawable =
65 | CircularAnimationDrawable(this, 10f, Color.WHITE)
66 |
67 | val padding = 15f.toDP(context)
68 |
69 | val offset = (width - height) / 2
70 | val left = offset + padding
71 | val right = width - offset - padding
72 | val bottom = height - padding
73 | val top = 0 + padding
74 |
75 | animatedDrawable!!.setBounds(left, top, right, bottom)
76 | animatedDrawable!!.callback = this
77 | animatedDrawable!!.start()
78 | } else {
79 | animatedDrawable!!.draw(canvas)
80 | }
81 | }
82 |
83 | private fun stopIndeterminateProgress() {
84 | if (animatedDrawable != null && animatedDrawable?.isRunning!!) {
85 | animatedDrawable?.stop()
86 | }
87 | }
88 |
89 | enum class State(val stateId: Int) {
90 | IDLE(0), LOADING(1)
91 | }
92 | }
93 |
94 | class CircularAnimationDrawable : Drawable, Animatable {
95 |
96 | private var valueAnimatorAngle: ValueAnimator? = null
97 | private var valueAnimatorSweep: ValueAnimator? = null
98 | private var valueAnimatorAlpha: ValueAnimator? = null
99 | private var angleInterpolator: Interpolator = LinearInterpolator()
100 | private var sweepInterpolator: Interpolator = DecelerateInterpolator()
101 |
102 | private var fBounds: RectF = RectF()
103 | private var paint: Paint? = null
104 | private var animatedView: View? = null
105 |
106 | private var borderWidth: Float = 0f
107 | private var currentGlobalAngle: Float = 0f
108 | private var currentSweepAngle: Float = 0f
109 | private var currentGlobalAngleOffset: Float = 0f
110 |
111 | private var modeAppearing = false
112 | private var running = false
113 |
114 | companion object {
115 | const val ANGLE_ANIMATOR_DURATION = 2000L
116 | const val SWEEP_ANIMATOR_DURATION = 900L
117 | const val ALPHA_ANIMATOR_DURATION = 250L
118 | const val MIN_SWEEP_ANGLE = 30f
119 | }
120 |
121 | constructor(view: View, width: Float, arcColor: Int) : super() {
122 | animatedView = view
123 | borderWidth = width
124 | paint = Paint().apply {
125 | isAntiAlias = true
126 | style = Paint.Style.STROKE
127 | strokeWidth = borderWidth
128 | color = arcColor
129 | }
130 | setupAnimations()
131 | }
132 |
133 | override fun draw(canvas: Canvas) {
134 | var startAngle = currentGlobalAngle - currentGlobalAngleOffset
135 | var sweepAngle = currentSweepAngle
136 | when (modeAppearing) {
137 | true -> {
138 | sweepAngle += MIN_SWEEP_ANGLE
139 | }
140 | false -> {
141 | startAngle += sweepAngle
142 | sweepAngle = 360f - sweepAngle - MIN_SWEEP_ANGLE
143 | }
144 | }
145 | paint?.let { canvas.drawArc(fBounds, startAngle, sweepAngle, false, it) }
146 | }
147 |
148 | override fun setAlpha(alpha: Int) {
149 | paint?.alpha = alpha
150 | }
151 |
152 | override fun getOpacity(): Int {
153 | return PixelFormat.TRANSPARENT
154 | }
155 |
156 | override fun setColorFilter(colorFilter: ColorFilter?) {
157 | paint?.colorFilter = colorFilter
158 | }
159 |
160 | override fun isRunning(): Boolean {
161 | return running
162 | }
163 |
164 | override fun start() {
165 | if (running) {
166 | return
167 | }
168 | running = true
169 | valueAnimatorAngle?.start()
170 | valueAnimatorSweep?.start()
171 | valueAnimatorAlpha?.start()
172 | }
173 |
174 | override fun stop() {
175 | if (!running) {
176 | return
177 | }
178 | running = false
179 | valueAnimatorAngle?.cancel()
180 | valueAnimatorSweep?.cancel()
181 | valueAnimatorAlpha?.cancel()
182 | }
183 |
184 | override fun onBoundsChange(bounds: Rect) {
185 | super.onBoundsChange(bounds)
186 | fBounds.left = bounds.left + borderWidth / 2f + .5f
187 | fBounds.right = bounds.right - borderWidth / 2f - .5f
188 | fBounds.top = bounds.top + borderWidth / 2f + .5f
189 | fBounds.bottom = bounds.bottom - borderWidth / 2f - .5f
190 | }
191 |
192 | private fun setupAnimations() {
193 | valueAnimatorAlpha = ValueAnimator.ofInt(0, 255).apply {
194 | interpolator = angleInterpolator
195 | duration =
196 | ALPHA_ANIMATOR_DURATION
197 | }
198 | valueAnimatorAlpha?.addUpdateListener {
199 | setAlpha(it.animatedValue as Int)
200 | animatedView?.invalidate()
201 | }
202 |
203 | valueAnimatorAngle = ValueAnimator.ofFloat(0f, 360f).apply {
204 | interpolator = angleInterpolator
205 | duration =
206 | ANGLE_ANIMATOR_DURATION
207 | repeatCount = ValueAnimator.INFINITE
208 | }
209 | valueAnimatorAngle?.addUpdateListener {
210 | currentGlobalAngle = it.animatedValue as Float
211 | animatedView?.invalidate()
212 | }
213 | valueAnimatorSweep = ValueAnimator.ofFloat(0F, 360f - 2 * MIN_SWEEP_ANGLE).apply {
214 | interpolator = sweepInterpolator
215 | duration =
216 | SWEEP_ANIMATOR_DURATION
217 | repeatCount = ValueAnimator.INFINITE
218 | }
219 | valueAnimatorSweep?.addUpdateListener {
220 | currentSweepAngle = it.animatedValue as Float
221 | invalidateSelf()
222 | }
223 | valueAnimatorSweep?.addListener(object : AnimatorListenerAdapter() {
224 | override fun onAnimationRepeat(animation: Animator) {
225 | toggleAppearingMode()
226 | }
227 | })
228 | }
229 |
230 | private fun toggleAppearingMode() {
231 | modeAppearing = !modeAppearing
232 | }
233 | }
234 |
--------------------------------------------------------------------------------
/firestore/src/main/kotlin/com/gluehome/common/threads/extensions/RxFirestore.kt:
--------------------------------------------------------------------------------
1 | package com.gluehome.common.threads.extensions
2 |
3 | import com.google.android.gms.tasks.Task
4 | import com.google.firebase.firestore.CollectionReference
5 | import com.google.firebase.firestore.DocumentReference
6 | import com.google.firebase.firestore.DocumentSnapshot
7 | import com.google.firebase.firestore.FirebaseFirestore
8 | import com.google.firebase.firestore.Query
9 | import com.google.firebase.firestore.QuerySnapshot
10 | import com.google.firebase.firestore.SetOptions
11 | import com.google.firebase.firestore.Transaction
12 | import com.google.firebase.firestore.WriteBatch
13 | import io.reactivex.BackpressureStrategy
14 | import io.reactivex.Completable
15 | import io.reactivex.Flowable
16 | import io.reactivex.Observable
17 | import io.reactivex.Single
18 |
19 | class NoSuchDocumentException : Exception("There is no document at the given DocumentReference")
20 |
21 | /**
22 | * Listens to changes at the given [DocumentReference] (receiver) and returns an [Observable] that
23 | * emits an item whenever there is a new [DocumentSnapshot].
24 | * The listener is removed when the [Observable]'s subscription is disposed.
25 | * The type needs to have a constructor that takes no argument in order to call [DocumentSnapshot.toObject].
26 | *
27 | * @receiver [DocumentReference] to listen to
28 | *
29 | * @throws [NoSuchDocumentException] if the document doesn't exist
30 | */
31 | inline fun DocumentReference.getObservable(): Observable {
32 | return Observable.create { emitter ->
33 | val listener = addSnapshotListener { documentSnapshot, firebaseFirestoreException ->
34 | documentSnapshot?.let {
35 | if (documentSnapshot.exists()) {
36 | try {
37 | emitter.onNext(documentSnapshot.toObject(T::class.java)!!)
38 | } catch (e: Exception) {
39 | emitter.onError(e)
40 | }
41 | } else {
42 | emitter.onError(com.gluehome.common.threads.extensions.NoSuchDocumentException())
43 | }
44 | }
45 | firebaseFirestoreException?.let { emitter.onError(it) }
46 | }
47 | emitter.setCancellable { listener.remove() }
48 | }
49 | }
50 |
51 | /**
52 | * Listens to changes at the given [DocumentReference] (receiver) and returns a [Flowable] that
53 | * emits an item whenever there is a new [DocumentSnapshot].
54 | * The listener is removed when the [Flowable]'s subscription is disposed.
55 | * The type needs to have a constructor that takes no argument in order to call [DocumentSnapshot.toObject].
56 | *
57 | * @receiver [DocumentReference] to listen to
58 | *
59 | * @throws [NoSuchDocumentException] if the document doesn't exist
60 | */
61 | inline fun DocumentReference.getFlowable(backpressureStrategy: BackpressureStrategy): Flowable {
62 | return Flowable.create({ emitter ->
63 | val listener = addSnapshotListener { documentSnapshot, firebaseFirestoreException ->
64 | documentSnapshot?.let {
65 | if (documentSnapshot.exists()) {
66 | try {
67 | val toObject = documentSnapshot.toObject(T::class.java)!!
68 | emitter.onNext(toObject)
69 | } catch (e: Exception) {
70 | emitter.onError(e)
71 | }
72 | } else {
73 | emitter.onError(com.gluehome.common.threads.extensions.NoSuchDocumentException())
74 | }
75 | }
76 | firebaseFirestoreException?.let { emitter.onError(it) }
77 | }
78 | emitter.setCancellable { listener.remove() }
79 | }, backpressureStrategy)
80 | }
81 |
82 | /**
83 | * Gets the value at the given [DocumentReference] (receiver) once and returns a [Single] that
84 | * emits an item if it exists or calls onError.
85 | * The type needs to have a constructor that takes no argument in order to call [DocumentSnapshot.toObject].
86 | *
87 | * @receiver [DocumentReference] to listen to
88 | *
89 | * @throws [NoSuchDocumentException] if the document doesn't exist
90 | */
91 | inline fun DocumentReference.getSingle(): Single {
92 | return Single.create { emitter ->
93 | get()
94 | .addOnSuccessListener {
95 | if (it.exists()) {
96 | try {
97 | emitter.onSuccess(it.toObject(T::class.java)!!)
98 | } catch (e: Exception) {
99 | emitter.onError(e)
100 | }
101 | } else {
102 | emitter.onError(com.gluehome.common.threads.extensions.NoSuchDocumentException())
103 | }
104 | }
105 | .addOnFailureListener { emitter.onError(it) }
106 | }
107 | }
108 |
109 | /**
110 | * Listens to changes at the given [CollectionReference] (receiver) and returns an [Observable] that
111 | * emits a list of items whenever there is a new [QuerySnapshot].
112 | * The listener is removed when the [Observable]'s subscription is disposed.
113 | * The type needs to have a constructor that takes no argument in order to call [QuerySnapshot.toObjects]
114 | *
115 | * @receiver [CollectionReference] to listen to
116 | *
117 | */
118 | inline fun CollectionReference.getObservable(): Observable> {
119 | return Observable.create { emitter ->
120 | val listener = addSnapshotListener { querySnapshot, firebaseFirestoreException ->
121 | querySnapshot?.let {
122 | try {
123 | emitter.onNext(it.toObjects(T::class.java))
124 | } catch (e: Exception) {
125 | emitter.onError(e)
126 | }
127 | }
128 | firebaseFirestoreException?.let { emitter.onError(it) }
129 | }
130 | emitter.setCancellable { listener.remove() }
131 | }
132 | }
133 |
134 | /**
135 | * Listens to changes at the given [CollectionReference] (receiver) and returns an [Flowable] that
136 | * emits a list of items whenever there is a new [QuerySnapshot].
137 | * The listener is removed when the [Flowable]'s subscription is disposed.
138 | * The type needs to have a constructor that takes no argument in order to call [QuerySnapshot.toObjects]
139 | *
140 | * @receiver [CollectionReference] to listen to
141 | * @param backpressureStrategy to use
142 | *
143 | */
144 | inline fun CollectionReference.getFlowable(backpressureStrategy: BackpressureStrategy): Flowable> {
145 | return Flowable.create({ emitter ->
146 | val listener = addSnapshotListener { querySnapshot, firebaseFirestoreException ->
147 | querySnapshot?.let {
148 | try {
149 | emitter.onNext(it.toObjects(T::class.java))
150 | } catch (e: Exception) {
151 | emitter.onError(e)
152 | }
153 | }
154 | firebaseFirestoreException?.let { emitter.onError(it) }
155 | }
156 | emitter.setCancellable { listener.remove() }
157 | }, backpressureStrategy)
158 | }
159 |
160 | /**
161 | * Listens to changes for the given [Query] (receiver) and returns an [Observable] that
162 | * emits a list of items whenever there is a new [QuerySnapshot].
163 | * The listener is removed when the [Observable]'s subscription is disposed.
164 | * The type needs to have a constructor that takes no argument in order to call [QuerySnapshot.toObjects]
165 | *
166 | * @receiver [Query] to listen to
167 | *
168 | */
169 | inline fun Query.getObservable(): Observable> {
170 | return Observable.create { emitter ->
171 | val listener = addSnapshotListener { querySnapshot, firebaseFirestoreException ->
172 | querySnapshot?.let {
173 | try {
174 | emitter.onNext(it.toObjects(T::class.java))
175 | } catch (e: Exception) {
176 | emitter.onError(e)
177 | }
178 | }
179 | firebaseFirestoreException?.let { emitter.onError(it) }
180 | }
181 | emitter.setCancellable { listener.remove() }
182 | }
183 | }
184 |
185 | /**
186 | * Listens to changes for the given [Query] (receiver) and returns a [Flowable] that
187 | * emits a list of items whenever there is a new [QuerySnapshot].
188 | * The listener is removed when the [Flowable]'s subscription is disposed.
189 | * The type needs to have a constructor that takes no argument in order to call [QuerySnapshot.toObjects]
190 | *
191 | * @receiver [Query] to listen to
192 | * @param backpressureStrategy to use
193 | *
194 | */
195 | inline fun Query.getFlowable(backpressureStrategy: BackpressureStrategy): Flowable> {
196 | return Flowable.create({ emitter ->
197 | val listener = addSnapshotListener { querySnapshot, firebaseFirestoreException ->
198 | querySnapshot?.let {
199 | try {
200 | emitter.onNext(it.toObjects(T::class.java))
201 | } catch (e: Exception) {
202 | emitter.onError(e)
203 | }
204 | }
205 | firebaseFirestoreException?.let { emitter.onError(it) }
206 | }
207 | emitter.setCancellable { listener.remove() }
208 | }, backpressureStrategy)
209 | }
210 |
211 | /**
212 | * Gets the current value at the given [CollectionReference] (receiver) and returns a [Single] that
213 | * emits the list of items found at that [CollectionReference].
214 | * The type needs to have a constructor that takes no argument in order to call [QuerySnapshot.toObjects]
215 | *
216 | * @receiver [CollectionReference] to listen to
217 | *
218 | */
219 | inline fun CollectionReference.getSingle(): Single> {
220 | return Single.create { emitter ->
221 | get()
222 | .addOnSuccessListener {
223 | try {
224 | emitter.onSuccess(it.toObjects(T::class.java))
225 | } catch (e: Exception) {
226 | emitter.onError(e)
227 | }
228 | }
229 | .addOnFailureListener { emitter.onError(it) }
230 | }
231 | }
232 |
233 | /**
234 | * Gets the current value for the given [Query] (receiver) and returns a [Single] that
235 | * emits the list of items found for that [Query].
236 | * The type needs to have a constructor that takes no argument in order to call [QuerySnapshot.toObjects].
237 | *
238 | * @receiver [Query] to listen to
239 | *
240 | */
241 | inline fun Query.getSingle(): Single> {
242 | return Single.create { emitter ->
243 | get()
244 | .addOnSuccessListener {
245 | try {
246 | emitter.onSuccess(it.toObjects(T::class.java))
247 | } catch (e: Exception) {
248 | emitter.onError(e)
249 | }
250 | }
251 | .addOnFailureListener { emitter.onError(it) }
252 | }
253 | }
254 |
255 | /**
256 | * Set's the parameter [item] at the given [DocumentReference] (receiver) and returns a
257 | * completable that completes when the transaction is complete or calls onError otherwise.
258 | *
259 | * @param item to be set at the [DocumentReference]
260 | * @receiver [DocumentReference] to set the item to
261 | */
262 | fun DocumentReference.setDocument(item: T): Completable {
263 | return Completable.create { emitter ->
264 | set(item)
265 | .addOnCompleteListener { emitter.onComplete() }
266 | .addOnFailureListener { emitter.onError(it) }
267 | }
268 | }
269 |
270 | fun DocumentReference.setDocumentAndMerge(item: T): Completable {
271 | return Completable.create { emitter ->
272 | set(item, SetOptions.merge())
273 | .addOnCompleteListener { emitter.onComplete() }
274 | .addOnFailureListener { emitter.onError(it) }
275 | }
276 | }
277 |
278 | /**
279 | * Delete's the document at the given [DocumentReference] (receiver) and returns a
280 | * completable that completes when the transaction is complete or calls onError otherwise.
281 | *
282 | * @receiver [DocumentReference] to delete
283 | */
284 | fun DocumentReference.deleteDocument(): Completable {
285 | return Completable.create { emitter ->
286 | delete()
287 | .addOnCompleteListener { emitter.onComplete() }
288 | .addOnFailureListener { emitter.onError(it) }
289 | }
290 | }
291 |
292 | fun Task.getCompletable(): Completable {
293 | return Completable.create { emitter ->
294 | addOnSuccessListener { emitter.onComplete() }
295 | addOnFailureListener { emitter.onError(it) }
296 | }
297 | }
298 |
299 | /**
300 | * Adds the given [item] to the collection at the [CollectionReference] (receiver) and returns a
301 | * [Single] that emits the [DocumentReference] for the added item or emits an error if the item
302 | * wasn't added to the collection.
303 | *
304 | * @receiver [CollectionReference] to add the item to
305 | */
306 | fun CollectionReference.addDocumentSingle(item: T): Single {
307 | return Single.create { emitter ->
308 | add(item)
309 | .addOnSuccessListener { emitter.onSuccess(it) }
310 | .addOnFailureListener { emitter.onError(it) }
311 | }
312 | }
313 |
314 | /**
315 | * Updates the document at the given [DocumentReference] (receiver) with a field specified by
316 | * [field] and the the new value of [newValue] and returns a completable which completes if the
317 | * operation is successful or calls onError otherwise.
318 | *
319 | * @param [field] to update - [String] name of the field in Firestore
320 | * @param [newValue] updated value of any of the types supported by Firestore
321 | */
322 | fun DocumentReference.updateDocumentCompletable(field: String, newValue: Any): Completable {
323 | return Completable.create { emitter ->
324 | update(field, newValue)
325 | .addOnSuccessListener { emitter.onComplete() }
326 | .addOnFailureListener { emitter.onError(it) }
327 | }
328 | }
329 |
330 | /**
331 | * Updates the document at the given [DocumentReference] (receiver) with a set of new values specified
332 | * in a map with the fields (Strings of names of the fields to be updated) and the new values of any type
333 | * supported by firestore as the map's value. Returns a completable which completes if the
334 | * operation is successful or calls onError otherwise.
335 | *
336 | * @param updatedValues [Map] of field names (keys) and updated values (values)
337 | */
338 | fun DocumentReference.updateDocumentCompletable(updatedValues: Map): Completable {
339 | return Completable.create { emitter ->
340 | update(updatedValues)
341 | .addOnSuccessListener { emitter.onComplete() }
342 | .addOnFailureListener { emitter.onError(it) }
343 | }
344 | }
345 |
346 | /**
347 | * Runs a [Transaction] specified by [transaction] and returns a single that emits a value of [ReturnType]
348 | * if the transaction completes successfully or calls onError otherwise.
349 | * User [Void] as a return type if no value should be returned.
350 | *
351 | * @param transaction to be run
352 | */
353 | fun FirebaseFirestore.runTransactionSingle(transaction: Transaction.Function): Single {
354 | return Single.create { emitter ->
355 | runTransaction(transaction)
356 | .addOnSuccessListener { emitter.onSuccess(it) }
357 | .addOnFailureListener { emitter.onError(it) }
358 | }
359 | }
360 |
361 | /**
362 | * Commits the given [WriteBatch] (receiver) and returns a completable that completes if the operation
363 | * is successful or calls onError otherwise.
364 | *
365 | * @receiver [WriteBatch] to execute
366 | */
367 | fun WriteBatch.getCompletable(): Completable {
368 | return Completable.create { emitter ->
369 | commit()
370 | .addOnSuccessListener { emitter.onComplete() }
371 | .addOnFailureListener { emitter.onError(it) }
372 | }
373 | }
374 |
375 | /**
376 | * Increment's a value at the given [DocumentReference] (receiver) by [increment] and returns a
377 | * Single that emits the new value if the [Transaction] completes successfully or calls onError
378 | * otherwise
379 | *
380 | * @param fieldName of the field to be incremented
381 | * @param increment number to increment the field by
382 | *
383 | * @receiver [DocumentReference] to update
384 | */
385 | fun DocumentReference.incrementField(fieldName: String, increment: Long = 1): Single {
386 | return FirebaseFirestore.getInstance()
387 | .runTransactionSingle(Transaction.Function {
388 | val docSnapshot = it.get(this)
389 | val newValue = docSnapshot.getLong(fieldName)!! + increment
390 | it.update(this, fieldName, newValue)
391 | newValue
392 | })
393 | }
394 |
--------------------------------------------------------------------------------