├── data ├── .gitignore ├── src │ ├── main │ │ ├── AndroidManifest.xml │ │ └── kotlin │ │ │ └── gluehome │ │ │ └── common │ │ │ └── data │ │ │ ├── network │ │ │ └── HttpLogFilter.kt │ │ │ └── DeviceUniqueKeyGenerator.kt │ └── test │ │ └── kotlin │ │ └── gluehome │ │ └── common │ │ └── data │ │ └── network │ │ └── HttpLogFilterTest.kt ├── proguard-rules.pro └── build.gradle ├── domain ├── .gitignore ├── src │ ├── main │ │ └── kotlin │ │ │ └── com │ │ │ └── gluehome │ │ │ └── common │ │ │ └── domain │ │ │ ├── framework │ │ │ ├── interactor │ │ │ │ ├── NoParams.kt │ │ │ │ ├── CompletableUseCase.kt │ │ │ │ ├── RxFlowableUseCase.kt │ │ │ │ ├── SingleUseCase.kt │ │ │ │ ├── FlowableUseCase.kt │ │ │ │ ├── BaseUseCase.kt │ │ │ │ ├── ObservableUseCase.kt │ │ │ │ └── UseCase.kt │ │ │ ├── threads │ │ │ │ └── CoroutineThreads.kt │ │ │ ├── date │ │ │ │ └── TimestampProvider.kt │ │ │ ├── Observable.kt │ │ │ ├── Disposer.kt │ │ │ ├── functional │ │ │ │ └── Either.kt │ │ │ └── DateMapper.kt │ │ │ ├── extension │ │ │ ├── CharExtensions.kt │ │ │ ├── Misc.kt │ │ │ └── DateExtensions.kt │ │ │ ├── utils │ │ │ ├── RandomUtils.kt │ │ │ ├── TimeDelta.kt │ │ │ └── Random.kt │ │ │ ├── logs │ │ │ └── GlueLogger.kt │ │ │ └── exceptions │ │ │ └── Failure.kt │ └── test │ │ └── kotlin │ │ └── com │ │ └── gluehome │ │ └── common │ │ └── domain │ │ ├── EitherTest.kt │ │ └── DateMapperTest.kt ├── proguard-rules.pro └── build.gradle ├── logger ├── .gitignore ├── src │ ├── main │ │ ├── AndroidManifest.xml │ │ └── kotlin │ │ │ └── com │ │ │ └── gluehome │ │ │ └── common │ │ │ └── data │ │ │ └── log │ │ │ ├── MyDebugTree.kt │ │ │ ├── LoggerExtraInfo.kt │ │ │ └── SematextTree.kt │ └── test │ │ └── kotlin │ │ └── com │ │ └── gluehome │ │ └── common │ │ └── data │ │ └── log │ │ └── LoggerExtraInfoTest.kt ├── proguard-rules.pro └── build.gradle ├── firestore ├── .gitignore ├── src │ └── main │ │ ├── AndroidManifest.xml │ │ └── kotlin │ │ └── com │ │ └── gluehome │ │ └── common │ │ └── threads │ │ └── extensions │ │ ├── CoroutineFirestore.kt │ │ └── RxFirestore.kt ├── proguard-rules.pro └── build.gradle ├── rx-threads ├── .gitignore ├── src │ ├── main │ │ └── kotlin │ │ │ └── com │ │ │ └── gluehome │ │ │ └── common │ │ │ └── threads │ │ │ └── RxThreads.kt │ └── test │ │ └── kotlin │ │ └── com │ │ └── gluehome │ │ └── common │ │ └── threads │ │ └── TestRxThreads.kt ├── proguard-rules.pro └── build.gradle ├── presentation ├── .gitignore ├── src │ └── main │ │ ├── AndroidManifest.xml │ │ └── kotlin │ │ └── gluehome │ │ └── common │ │ └── presentation │ │ ├── extensions │ │ ├── Strings.kt │ │ ├── LiveDataExtensions.kt │ │ ├── DialogExtensions.kt │ │ ├── snack.kt │ │ ├── RecyclerViewExtensions.kt │ │ ├── Misc.kt │ │ ├── utils.kt │ │ ├── ViewModelExtension.kt │ │ └── ViewExtension.kt │ │ ├── framework │ │ ├── UIState.kt │ │ ├── threads │ │ │ └── AndroidCoroutineThreads.kt │ │ └── archcomponents │ │ │ ├── BaseViewModel.kt │ │ │ └── HybridLiveEvent.kt │ │ └── ui │ │ ├── SpacesItemDecoration.kt │ │ ├── RoundRectCornerImageView.kt │ │ └── ProgressButton.kt ├── build.gradle └── proguard-rules.pro ├── rx-threads-android ├── .gitignore ├── src │ ├── main │ │ ├── AndroidManifest.xml │ │ └── kotlin │ │ │ └── com │ │ │ └── gluehome │ │ │ └── common │ │ │ └── threads │ │ │ └── AndroidRxThreads.kt │ └── test │ │ └── kotlin │ │ └── com │ │ └── gluehome │ │ └── common │ │ └── threads │ │ └── TestRxThreads.kt ├── proguard-rules.pro └── build.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── settings.gradle ├── .gitignore ├── gradle.properties ├── README.md ├── gradlew.bat └── gradlew /data/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /domain/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /logger/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /firestore/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /rx-threads/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /presentation/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /rx-threads-android/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /rx-threads-android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GlueHome/common-android/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /data/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | -------------------------------------------------------------------------------- /logger/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | -------------------------------------------------------------------------------- /presentation/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /domain/src/main/kotlin/com/gluehome/common/domain/framework/interactor/NoParams.kt: -------------------------------------------------------------------------------- 1 | package com.gluehome.common.domain.framework.interactor 2 | 3 | class NoParams 4 | -------------------------------------------------------------------------------- /firestore/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':domain' 2 | include ':logger' 3 | include ':data' 4 | include ':presentation' 5 | include ':rx-threads' 6 | include ':rx-threads-android' 7 | 8 | -------------------------------------------------------------------------------- /rx-threads/src/main/kotlin/com/gluehome/common/threads/RxThreads.kt: -------------------------------------------------------------------------------- 1 | package com.gluehome.common.threads 2 | 3 | import io.reactivex.Scheduler 4 | 5 | interface RxThreads { 6 | fun executionThread(): Scheduler 7 | fun postExecutionThread(): Scheduler 8 | } 9 | -------------------------------------------------------------------------------- /domain/src/main/kotlin/com/gluehome/common/domain/extension/CharExtensions.kt: -------------------------------------------------------------------------------- 1 | package com.gluehome.common.domain.extension 2 | 3 | fun Char.isDigit(): Boolean { 4 | return (0..9).map { it.toString() }.map { it == this.toString() }.reduce { acc, b -> acc || b } 5 | } 6 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Thu Nov 14 11:47:29 EST 2024 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /domain/src/main/kotlin/com/gluehome/common/domain/framework/threads/CoroutineThreads.kt: -------------------------------------------------------------------------------- 1 | package com.gluehome.common.domain.framework.threads 2 | 3 | import kotlin.coroutines.CoroutineContext 4 | 5 | interface CoroutineThreads { 6 | fun ui(): CoroutineContext 7 | fun io(): CoroutineContext 8 | } 9 | -------------------------------------------------------------------------------- /domain/src/main/kotlin/com/gluehome/common/domain/utils/RandomUtils.kt: -------------------------------------------------------------------------------- 1 | package com.gluehome.common.domain.utils 2 | 3 | class RandomUtils { 4 | companion object { 5 | fun randomNumberBetween(x: Int, y: Int): Int = (x..y).random() 6 | fun randomNumberBetween(x: Long, y: Long): Long = (x..y).random() 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /domain/src/main/kotlin/com/gluehome/common/domain/logs/GlueLogger.kt: -------------------------------------------------------------------------------- 1 | package com.gluehome.common.domain.logs 2 | 3 | interface GlueLogger { 4 | fun d(message: String) 5 | fun w(message: String) 6 | fun e(message: String, exception: Exception) 7 | fun e(exception: Exception) 8 | fun e(message: String, vararg objs: Unit) 9 | fun e(exception: Throwable?) 10 | } 11 | -------------------------------------------------------------------------------- /presentation/src/main/kotlin/gluehome/common/presentation/extensions/Strings.kt: -------------------------------------------------------------------------------- 1 | package gluehome.common.presentation.extensions 2 | 3 | import android.util.Patterns 4 | 5 | fun String.Companion.empty() = "" 6 | 7 | fun String?.or(orString: String): String = this ?: orString 8 | 9 | fun String.isEmail(): Boolean { 10 | return Patterns.EMAIL_ADDRESS.matcher(this).matches() 11 | } 12 | -------------------------------------------------------------------------------- /rx-threads/src/test/kotlin/com/gluehome/common/threads/TestRxThreads.kt: -------------------------------------------------------------------------------- 1 | package com.gluehome.common.threads 2 | 3 | import io.reactivex.Scheduler 4 | import io.reactivex.schedulers.Schedulers 5 | 6 | class TestRxThreads : RxThreads { 7 | override fun executionThread(): Scheduler = Schedulers.trampoline() 8 | override fun postExecutionThread(): Scheduler = Schedulers.trampoline() 9 | } 10 | -------------------------------------------------------------------------------- /logger/src/main/kotlin/com/gluehome/common/data/log/MyDebugTree.kt: -------------------------------------------------------------------------------- 1 | package com.gluehome.common.data.log 2 | 3 | import timber.log.Timber 4 | 5 | class MyDebugTree : Timber.DebugTree() { 6 | override fun createStackElementTag(element: StackTraceElement): String? { 7 | return "[${super.createStackElementTag(element)}.${element.methodName} @ line:${element.lineNumber}] " 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /presentation/src/main/kotlin/gluehome/common/presentation/framework/UIState.kt: -------------------------------------------------------------------------------- 1 | package gluehome.common.presentation.framework 2 | 3 | import com.gluehome.common.domain.exceptions.Failure 4 | 5 | sealed class UIState { 6 | object Loading : UIState() 7 | object Empty : UIState() 8 | data class Problem(val error: Failure) : UIState() 9 | data class Success(val data: T) : UIState() 10 | } 11 | -------------------------------------------------------------------------------- /rx-threads-android/src/test/kotlin/com/gluehome/common/threads/TestRxThreads.kt: -------------------------------------------------------------------------------- 1 | package com.gluehome.common.threads 2 | 3 | import io.reactivex.Scheduler 4 | import io.reactivex.schedulers.Schedulers 5 | 6 | class TestRxThreads : RxThreads { 7 | override fun executionThread(): Scheduler = Schedulers.trampoline() 8 | override fun postExecutionThread(): Scheduler = Schedulers.trampoline() 9 | } 10 | -------------------------------------------------------------------------------- /rx-threads-android/src/main/kotlin/com/gluehome/common/threads/AndroidRxThreads.kt: -------------------------------------------------------------------------------- 1 | package com.gluehome.common.threads 2 | 3 | import io.reactivex.android.schedulers.AndroidSchedulers 4 | import io.reactivex.schedulers.Schedulers 5 | 6 | class AndroidRxThreads : RxThreads { 7 | override fun postExecutionThread() = AndroidSchedulers.mainThread() 8 | override fun executionThread() = Schedulers.io() 9 | } 10 | -------------------------------------------------------------------------------- /presentation/src/main/kotlin/gluehome/common/presentation/framework/threads/AndroidCoroutineThreads.kt: -------------------------------------------------------------------------------- 1 | package gluehome.common.presentation.framework.threads 2 | 3 | import com.gluehome.common.domain.framework.threads.CoroutineThreads 4 | import kotlinx.coroutines.Dispatchers 5 | 6 | class AndroidCoroutineThreads : CoroutineThreads { 7 | override fun ui() = Dispatchers.Main 8 | override fun io() = Dispatchers.IO 9 | } 10 | -------------------------------------------------------------------------------- /presentation/src/main/kotlin/gluehome/common/presentation/framework/archcomponents/BaseViewModel.kt: -------------------------------------------------------------------------------- 1 | package gluehome.common.presentation.framework.archcomponents 2 | 3 | import androidx.lifecycle.ViewModel 4 | import com.gluehome.common.domain.exceptions.Failure 5 | 6 | open class BaseViewModel : ViewModel() { 7 | var failure = HybridLiveEvent() 8 | 9 | fun handleFailure(failure: Failure) { 10 | this.failure.postValue(failure, false) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /domain/src/main/kotlin/com/gluehome/common/domain/framework/date/TimestampProvider.kt: -------------------------------------------------------------------------------- 1 | package com.gluehome.common.domain.framework.date 2 | 3 | import java.text.SimpleDateFormat 4 | import java.util.* 5 | 6 | class TimestampProvider { 7 | 8 | fun generateIsoFormattedTimestamp(date: Date): String { 9 | return SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.UK) 10 | .apply { 11 | timeZone = TimeZone.getTimeZone("UTC") 12 | }.format(date) 13 | } 14 | } -------------------------------------------------------------------------------- /data/src/main/kotlin/gluehome/common/data/network/HttpLogFilter.kt: -------------------------------------------------------------------------------- 1 | package gluehome.common.data.network 2 | 3 | class HttpLogFilter { 4 | 5 | private val includeList = listOf( 6 | "glue-correlation-id: ", 7 | "<-- 2", 8 | "{", 9 | "[", 10 | "--> GET", 11 | "--> POST", 12 | "--> DELETE", 13 | "--> PUT", 14 | "WWW-Authenticate", 15 | "--> PATCH" 16 | ) 17 | 18 | fun shouldBeRemoved(word: String) = !includeList.any { word.startsWith(it, ignoreCase = true) } 19 | } -------------------------------------------------------------------------------- /domain/src/main/kotlin/com/gluehome/common/domain/framework/Observable.kt: -------------------------------------------------------------------------------- 1 | package com.gluehome.common.domain.framework 2 | 3 | class Observable { 4 | 5 | private var callback: ((Type, Boolean) -> Unit)? = null 6 | 7 | fun postValue(value: Type, sticky: Boolean = true) { 8 | callback?.invoke(value, sticky) 9 | } 10 | 11 | fun postNonStickyValue(value: Type) { 12 | callback?.invoke(value, false) 13 | } 14 | 15 | fun observe(callback: (Type, Boolean) -> Unit) { 16 | this.callback = callback 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /domain/src/main/kotlin/com/gluehome/common/domain/extension/Misc.kt: -------------------------------------------------------------------------------- 1 | package com.gluehome.common.domain.extension 2 | 3 | fun Boolean?.isNullOrFalse(): Boolean { 4 | return this == null || this == false 5 | } 6 | 7 | fun Any?.isNull(): Boolean { 8 | return this == null 9 | } 10 | 11 | fun Any?.isNullOrEmpty(): Boolean { 12 | return this == null || (this is String && this.isEmpty()) 13 | } 14 | 15 | fun Map.filterEmptyValues() = this.filter { it.value.isNotEmpty() } 16 | fun HashMap.filterEmptyValues() = this.filter { !it.value.isNullOrEmpty() } 17 | -------------------------------------------------------------------------------- /domain/src/main/kotlin/com/gluehome/common/domain/utils/TimeDelta.kt: -------------------------------------------------------------------------------- 1 | package com.gluehome.common.domain.utils 2 | 3 | import java.util.Date 4 | 5 | /** 6 | * Calculate time differences 7 | */ 8 | class TimeDelta(private val startTime: Date = Date()) { 9 | 10 | private lateinit var endTime: Date 11 | 12 | fun finish(endTime: Date = Date()) { 13 | this.endTime = endTime 14 | } 15 | 16 | /** 17 | * Diference 18 | */ 19 | var delta: Long = 0 20 | get() = endTime.time - startTime.time 21 | 22 | var deltaInSeconds: Long = 0 23 | get() = delta / 1000 24 | } 25 | -------------------------------------------------------------------------------- /domain/src/main/kotlin/com/gluehome/common/domain/framework/interactor/CompletableUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.gluehome.common.domain.framework.interactor 2 | 3 | import com.gluehome.common.threads.RxThreads 4 | import io.reactivex.Completable 5 | 6 | abstract class CompletableUseCase(private val threads: RxThreads) where Type : Any { 7 | 8 | abstract fun run(params: Params): Completable 9 | 10 | open operator fun invoke(params: Params): Completable { 11 | return run(params) 12 | .subscribeOn(threads.executionThread()) 13 | .observeOn(threads.postExecutionThread()) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /presentation/src/main/kotlin/gluehome/common/presentation/extensions/LiveDataExtensions.kt: -------------------------------------------------------------------------------- 1 | package gluehome.common.presentation.extensions 2 | 3 | import com.gluehome.common.domain.framework.Observable 4 | import gluehome.common.presentation.framework.archcomponents.HybridLiveEvent 5 | 6 | fun HybridLiveEvent.observe(observable: Observable) { 7 | observable.observe { value, sticky -> 8 | this.postValue(value, sticky) 9 | } 10 | } 11 | 12 | fun liveDataObserving(observable: Observable): HybridLiveEvent { 13 | return HybridLiveEvent().apply { 14 | observe(observable) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /domain/src/main/kotlin/com/gluehome/common/domain/framework/interactor/RxFlowableUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.gluehome.common.domain.framework.interactor 2 | 3 | import com.gluehome.common.threads.RxThreads 4 | import io.reactivex.Flowable 5 | 6 | abstract class RxFlowableUseCase(private val threads: RxThreads) where Type : Any { 7 | 8 | abstract fun run(params: Params): Flowable 9 | 10 | open operator fun invoke(params: Params): Flowable { 11 | return run(params) 12 | .subscribeOn(threads.executionThread()) 13 | .observeOn(threads.postExecutionThread()) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /domain/src/main/kotlin/com/gluehome/common/domain/framework/interactor/SingleUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.gluehome.common.domain.framework.interactor 2 | 3 | import com.gluehome.common.threads.RxThreads 4 | import io.reactivex.Single 5 | 6 | abstract class SingleUseCase( 7 | private val threads: RxThreads 8 | ) where Type : Any { 9 | 10 | abstract fun run(params: Params): Single 11 | 12 | open operator fun invoke(params: Params): Single { 13 | return run(params) 14 | .subscribeOn(threads.executionThread()) 15 | .observeOn(threads.postExecutionThread()) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.ap_ 3 | *.iml 4 | 5 | # Files for the Dalvik VM 6 | *.dex 7 | 8 | # Java class files 9 | *.class 10 | 11 | # Generated files 12 | bin/ 13 | gen/ 14 | generator/out/ 15 | 16 | # Gradle files 17 | .gradle/ 18 | build/ 19 | 20 | # Local configuration file (sdk path, etc) 21 | local.properties 22 | 23 | .idea/ 24 | !.idea/codeStyleSettings.xml 25 | 26 | .DS_Store 27 | 28 | *.trace 29 | captures/ 30 | projectFilesBackup/ 31 | *.xcbkptlist 32 | *.xcuserstate 33 | ios/Pods 34 | 35 | ios/KMultiplatform.xcodeproj/xcuserdata/ 36 | 37 | fastlane/README.md 38 | fastlane/report.xml 39 | 40 | 41 | android/mapping.txt 42 | android/unused.txt 43 | -------------------------------------------------------------------------------- /domain/src/main/kotlin/com/gluehome/common/domain/utils/Random.kt: -------------------------------------------------------------------------------- 1 | package com.gluehome.common.domain.utils 2 | 3 | import java.util.Random 4 | import java.util.concurrent.ThreadLocalRandom 5 | 6 | fun generateRandom(between: LongRange): Long { 7 | return between.shuffled().last() 8 | } 9 | 10 | fun generateRandom(between: IntRange): Int { 11 | return between.shuffled().last() 12 | } 13 | 14 | fun ClosedRange.random() = 15 | Random().nextInt((endInclusive + 1) - start) + start 16 | 17 | fun ClosedRange.random() = ThreadLocalRandom.current().nextLong(start, endInclusive) 18 | 19 | fun List.getRandomElement() = this[Random().nextInt(this.size)] 20 | -------------------------------------------------------------------------------- /domain/src/main/kotlin/com/gluehome/common/domain/framework/Disposer.kt: -------------------------------------------------------------------------------- 1 | package com.gluehome.common.domain.framework 2 | 3 | import io.reactivex.disposables.CompositeDisposable 4 | import io.reactivex.disposables.Disposable 5 | 6 | class Disposer private constructor() { 7 | 8 | private object Holder { 9 | val INSTANCE = Disposer() 10 | } 11 | 12 | companion object { 13 | val instance: Disposer by lazy { Holder.INSTANCE } 14 | } 15 | 16 | private val compositeDisposable = CompositeDisposable() 17 | 18 | fun add(disposable: Disposable) { 19 | compositeDisposable.add(disposable) 20 | } 21 | 22 | fun dispose() { 23 | compositeDisposable.clear() 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /domain/src/main/kotlin/com/gluehome/common/domain/exceptions/Failure.kt: -------------------------------------------------------------------------------- 1 | package com.gluehome.common.domain.exceptions 2 | 3 | /** 4 | * Base Class for handling errors/failures/exceptions. 5 | * Every feature specific failure should extend [FeatureFailure] class. 6 | */ 7 | sealed class Failure(val exception: Exception = Exception("Failure")) { 8 | object None : Failure() 9 | object NetworkConnection : Failure() 10 | object ServerError : Failure() 11 | 12 | /** * Extend this class for feature specific failures.*/ 13 | open class FeatureFailure(featureException: Exception = Exception("Feature failure")) : Failure(featureException) 14 | 15 | override fun equals(other: Any?): Boolean { 16 | return other is Failure 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /presentation/src/main/kotlin/gluehome/common/presentation/extensions/DialogExtensions.kt: -------------------------------------------------------------------------------- 1 | package gluehome.common.presentation.extensions 2 | 3 | import android.content.Context 4 | import androidx.appcompat.app.AlertDialog 5 | 6 | fun Context.alertDialog( 7 | title: String, 8 | message: String, 9 | positiveMessage: String, 10 | positiveAction: () -> Unit = {}, 11 | negativeMessage: String = "", 12 | negativeAction: () -> Unit = {} 13 | ) { 14 | AlertDialog.Builder(this).apply { 15 | setTitle(title) 16 | setMessage(message) 17 | setPositiveButton(positiveMessage) { _, _ -> positiveAction() } 18 | setNegativeButton(negativeMessage) { _, _ -> negativeAction() } 19 | create() 20 | show() 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /logger/src/main/kotlin/com/gluehome/common/data/log/LoggerExtraInfo.kt: -------------------------------------------------------------------------------- 1 | package com.gluehome.common.data.log 2 | 3 | class LoggerExtraInfo private constructor() { 4 | 5 | private object Holder { 6 | val INSTANCE = LoggerExtraInfo() 7 | } 8 | 9 | companion object { 10 | val instance: LoggerExtraInfo by lazy { Holder.INSTANCE } 11 | } 12 | 13 | private var extra = mutableMapOf() 14 | 15 | fun getAll() = extra 16 | 17 | fun add(key: String, value: Any) { 18 | extra.put(key, value) 19 | } 20 | 21 | fun remove(key: String) { 22 | extra.remove(key) 23 | } 24 | 25 | fun add(ext: Map) { 26 | ext.forEach { (k, v) -> add(k, v) } 27 | } 28 | 29 | fun clear() { 30 | extra = mutableMapOf() 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /domain/src/main/kotlin/com/gluehome/common/domain/framework/interactor/FlowableUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.gluehome.common.domain.framework.interactor 2 | 3 | import com.gluehome.common.domain.exceptions.Failure 4 | import com.gluehome.common.domain.framework.functional.Either 5 | import com.gluehome.common.threads.RxThreads 6 | import io.reactivex.Flowable 7 | 8 | /** 9 | * FLOWABLE USE CASE 10 | */ 11 | abstract class FlowableUseCase(private val threads: RxThreads) where Type : Any { 12 | 13 | abstract fun run(params: Params): Flowable> 14 | 15 | open operator fun invoke(params: Params): Flowable> { 16 | return run(params) 17 | .subscribeOn(threads.executionThread()) 18 | .observeOn(threads.postExecutionThread()) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /domain/src/main/kotlin/com/gluehome/common/domain/framework/interactor/BaseUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.gluehome.common.domain.framework.interactor 2 | 3 | import com.gluehome.common.domain.exceptions.Failure 4 | import com.gluehome.common.domain.framework.functional.Either 5 | import kotlinx.coroutines.CoroutineScope 6 | import kotlinx.coroutines.async 7 | import kotlinx.coroutines.launch 8 | 9 | abstract class BaseUseCase where Type : Any { 10 | 11 | abstract suspend fun run(params: Params): Either 12 | 13 | open operator fun invoke( 14 | scope: CoroutineScope, 15 | params: Params, 16 | onResult: (Either) -> Unit = {} 17 | ) { 18 | val backgroundJob = scope.async { run(params) } 19 | scope.launch { onResult(backgroundJob.await()) } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /data/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /domain/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /logger/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /firestore/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /rx-threads/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /domain/src/main/kotlin/com/gluehome/common/domain/framework/interactor/ObservableUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.gluehome.common.domain.framework.interactor 2 | 3 | import com.gluehome.common.domain.exceptions.Failure 4 | import com.gluehome.common.domain.framework.functional.Either 5 | import com.gluehome.common.threads.RxThreads 6 | import io.reactivex.Observable 7 | 8 | abstract class ObservableUseCase(private val threads: RxThreads) where Type : Any { 9 | 10 | lateinit var observable: Observable> 11 | 12 | abstract fun run(params: Params): Observable> 13 | 14 | open operator fun invoke(params: Params): Observable> { 15 | return run(params) 16 | .subscribeOn(threads.executionThread()) 17 | .observeOn(threads.postExecutionThread()) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /rx-threads-android/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /logger/src/test/kotlin/com/gluehome/common/data/log/LoggerExtraInfoTest.kt: -------------------------------------------------------------------------------- 1 | package com.gluehome.common.data.log 2 | 3 | import org.amshove.kluent.shouldBe 4 | import org.junit.After 5 | import org.junit.Test 6 | 7 | class LoggerExtraInfoTest { 8 | 9 | val instance = LoggerExtraInfo.instance 10 | 11 | @Test 12 | fun `Test adding items with a map`() { 13 | instance.add(mapOf("password" to 12345)) 14 | instance.getAll().size shouldBe 1 15 | } 16 | 17 | @Test 18 | fun `Test adding items key pair`() { 19 | instance.add("password", 12345) 20 | instance.getAll().size shouldBe 1 21 | } 22 | 23 | @Test 24 | fun `CLearing test`() { 25 | instance.add(mapOf("password" to 12345)) 26 | instance.clear() 27 | instance.getAll().size shouldBe 0 28 | } 29 | 30 | @After 31 | fun tearDown() { 32 | instance.clear() 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx1536m 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # Kotlin code style for this project: "official" or "obsolete": 15 | kotlin.code.style=official 16 | android.useAndroidX=true 17 | android.enableJetifier=true 18 | -------------------------------------------------------------------------------- /presentation/src/main/kotlin/gluehome/common/presentation/extensions/snack.kt: -------------------------------------------------------------------------------- 1 | package gluehome.common.presentation.extensions 2 | 3 | import android.view.View 4 | import com.google.android.material.snackbar.Snackbar 5 | 6 | inline fun View.snack(messageRes: Int, length: Int = Snackbar.LENGTH_LONG, f: Snackbar.() -> Unit) { 7 | snack(resources.getString(messageRes), length, f) 8 | } 9 | 10 | inline fun View.snack(message: String, length: Int = Snackbar.LENGTH_LONG, f: Snackbar.() -> Unit) { 11 | val snack = Snackbar.make(this, message, length) 12 | snack.f() 13 | snack.show() 14 | } 15 | 16 | fun Snackbar.action(actionRes: Int, color: Int? = null, listener: (View) -> Unit) { 17 | action(view.resources.getString(actionRes), color, listener) 18 | } 19 | 20 | fun Snackbar.action(action: String, color: Int? = null, listener: (View) -> Unit) { 21 | setAction(action, listener) 22 | color?.let { setActionTextColor(color) } 23 | } 24 | -------------------------------------------------------------------------------- /data/src/main/kotlin/gluehome/common/data/DeviceUniqueKeyGenerator.kt: -------------------------------------------------------------------------------- 1 | package gluehome.common.data 2 | 3 | import android.content.Context 4 | import java.util.* 5 | 6 | class DeviceUniqueKeyGenerator(private val context: Context) { 7 | 8 | private var uniqueID: String? = null 9 | 10 | companion object { 11 | private const val PREF_UNIQUE_ID = "PREF_UNIQUE_ID" 12 | } 13 | 14 | @Synchronized 15 | fun getUniqueId(): String { 16 | if (uniqueID == null) { 17 | val sharedPrefs = context.getSharedPreferences(PREF_UNIQUE_ID, Context.MODE_PRIVATE) 18 | uniqueID = sharedPrefs.getString(PREF_UNIQUE_ID, null) 19 | if (uniqueID == null) { 20 | uniqueID = UUID.randomUUID().toString() 21 | val editor = sharedPrefs.edit() 22 | editor.putString(PREF_UNIQUE_ID, uniqueID) 23 | editor.apply() 24 | } 25 | } 26 | return uniqueID ?: "" 27 | } 28 | } -------------------------------------------------------------------------------- /domain/src/test/kotlin/com/gluehome/common/domain/EitherTest.kt: -------------------------------------------------------------------------------- 1 | package com.gluehome.common.domain 2 | 3 | import com.gluehome.common.domain.framework.functional.Either 4 | import org.junit.Assert.assertFalse 5 | import org.junit.Assert.assertTrue 6 | import org.junit.Test 7 | 8 | class EitherTest { 9 | 10 | @Test fun `Either Right should return correct type`() { 11 | val result = Either.Right("Sarah Connor") 12 | 13 | assertTrue (result.isRight) 14 | assertFalse (result.isLeft) 15 | result.either({}, 16 | { right -> 17 | assertTrue(right == "Sarah Connor") 18 | }) 19 | } 20 | 21 | @Test fun `Either Left should return correct type`() { 22 | val result = Either.Left("Sarah Connor") 23 | 24 | assertTrue (result.isLeft ) 25 | assertFalse ( result.isRight ) 26 | result.either({ left -> 27 | assertTrue( left == "Sarah Connor") 28 | }, {}) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /presentation/src/main/kotlin/gluehome/common/presentation/extensions/RecyclerViewExtensions.kt: -------------------------------------------------------------------------------- 1 | package gluehome.common.presentation.extensions 2 | 3 | import androidx.recyclerview.widget.DiffUtil 4 | import androidx.recyclerview.widget.RecyclerView 5 | 6 | fun RecyclerView.Adapter<*>.autoNotify(oldList: List, newList: List, compare: (T, T) -> Boolean) { 7 | 8 | val diff = DiffUtil.calculateDiff(object : DiffUtil.Callback() { 9 | override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { 10 | return compare(oldList[oldItemPosition], newList[newItemPosition]) 11 | } 12 | 13 | override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { 14 | return oldList[oldItemPosition] == newList[newItemPosition] 15 | } 16 | 17 | override fun getOldListSize() = oldList.size 18 | override fun getNewListSize() = newList.size 19 | }) 20 | diff.dispatchUpdatesTo(this) 21 | } 22 | -------------------------------------------------------------------------------- /rx-threads/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'java-library' 2 | apply plugin: 'kotlin' 3 | apply plugin: 'maven-publish' 4 | 5 | dependencies { 6 | implementation fileTree(dir: 'libs', include: ['*.jar']) 7 | 8 | implementation "org.jetbrains.kotlin:kotlin-stdlib:${Versions.kotlin}" 9 | api 'io.reactivex.rxjava2:rxjava:2.2.8' 10 | 11 | // coroutines 12 | // api "org.jetbrains.kotlinx:kotlinx-coroutines-core:${Versions.coroutines}" 13 | 14 | // tests 15 | testImplementation 'junit:junit:4.12' 16 | testImplementation 'org.amshove.kluent:kluent-android:1.72' 17 | testImplementation 'com.nhaarman.mockitokotlin2:mockito-kotlin:2.1.0' 18 | } 19 | 20 | publishing { 21 | publications { 22 | mavenJava(MavenPublication) { 23 | groupId = 'com.github.gluehome.common-android' 24 | artifactId = 'rx-threads' 25 | version = '1.3.14' 26 | afterEvaluate { 27 | from components.java 28 | } 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /domain/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'java-library' 2 | apply plugin: 'kotlin' 3 | apply plugin: 'maven-publish' 4 | 5 | 6 | dependencies { 7 | implementation fileTree(dir: 'libs', include: ['*.jar']) 8 | implementation project(':rx-threads') 9 | 10 | implementation "org.jetbrains.kotlin:kotlin-stdlib:${Versions.kotlin}" 11 | 12 | // coroutines 13 | api "org.jetbrains.kotlinx:kotlinx-coroutines-core:${Versions.coroutines}" 14 | 15 | // tests 16 | testImplementation 'junit:junit:4.12' 17 | testImplementation 'org.amshove.kluent:kluent-android:1.72' 18 | testImplementation 'com.nhaarman.mockitokotlin2:mockito-kotlin:2.1.0' 19 | } 20 | 21 | 22 | configurations { 23 | testOutput 24 | } 25 | publishing { 26 | publications { 27 | mavenJava(MavenPublication) { 28 | groupId = 'com.github.gluehome.common-android' 29 | artifactId = 'domain' 30 | version = '1.3.14' 31 | afterEvaluate { 32 | from components.java 33 | } 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /presentation/src/main/kotlin/gluehome/common/presentation/ui/SpacesItemDecoration.kt: -------------------------------------------------------------------------------- 1 | package gluehome.common.presentation.ui 2 | 3 | import android.graphics.Rect 4 | import android.view.View 5 | import androidx.recyclerview.widget.RecyclerView 6 | 7 | class SpacesItemDecoration : RecyclerView.ItemDecoration { 8 | 9 | private val top: Int 10 | private val right: Int 11 | private val bottom: Int 12 | private val left: Int 13 | 14 | constructor(space: Int) : super() { 15 | this.top = space 16 | this.right = space 17 | this.bottom = space 18 | this.left = space 19 | } 20 | 21 | constructor(top: Int, right: Int, bottom: Int, left: Int) : super() { 22 | this.top = top 23 | this.right = right 24 | this.bottom = bottom 25 | this.left = left 26 | } 27 | 28 | override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) { 29 | outRect.left = left 30 | outRect.right = right 31 | outRect.bottom = bottom 32 | 33 | // Add top margin only for the first item to avoid double space between items 34 | if (parent.getChildAdapterPosition(view) == 0) { 35 | outRect.top = top 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /domain/src/main/kotlin/com/gluehome/common/domain/framework/interactor/UseCase.kt: -------------------------------------------------------------------------------- 1 | package com.gluehome.common.domain.framework.interactor 2 | 3 | import com.gluehome.common.domain.exceptions.Failure 4 | import com.gluehome.common.domain.framework.functional.Either 5 | import com.gluehome.common.domain.framework.threads.CoroutineThreads 6 | import kotlinx.coroutines.CoroutineScope 7 | import kotlinx.coroutines.async 8 | import kotlinx.coroutines.launch 9 | 10 | /** 11 | * Abstract class for a Use Case (Interactor in terms of Clean Architecture). 12 | * This abstraction represents an execution unit for different use cases (this means than any use 13 | * case in the application should implement this contract). 14 | * 15 | * By convention each [UseCase] implementation will execute its job in a background thread 16 | * (kotlin coroutine) and will post the result in the UI thread. 17 | */ 18 | abstract class UseCase(private val threads: CoroutineThreads) where Type : Any { 19 | 20 | abstract suspend fun run(params: Params): Either 21 | 22 | open operator fun invoke(params: Params, onResult: (Either) -> Unit = {}) { 23 | val job = CoroutineScope(threads.io()).async { run(params) } 24 | CoroutineScope(threads.ui()).launch { onResult(job.await()) } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /domain/src/main/kotlin/com/gluehome/common/domain/extension/DateExtensions.kt: -------------------------------------------------------------------------------- 1 | package com.gluehome.common.domain.extension 2 | 3 | import java.util.Calendar 4 | import java.util.Date 5 | 6 | fun Date.isWeekend(): Boolean { 7 | val cal = Calendar.getInstance().apply { 8 | time = this@isWeekend 9 | } 10 | 11 | return (cal.get(Calendar.DAY_OF_WEEK) == Calendar.SATURDAY || cal.get(Calendar.DAY_OF_WEEK) == Calendar.SUNDAY) 12 | } 13 | 14 | fun Date.isFriday(): Boolean { 15 | val cal = Calendar.getInstance().apply { 16 | time = this@isFriday 17 | } 18 | 19 | return cal.get(Calendar.DAY_OF_WEEK) == Calendar.FRIDAY 20 | } 21 | 22 | fun Date.addDays(days: Int): Date { 23 | val cal = Calendar.getInstance().apply { 24 | time = this@addDays 25 | } 26 | 27 | cal.add(Calendar.DATE, days) 28 | return cal.time 29 | } 30 | 31 | fun Date.toCalendar(): Calendar { 32 | val cal = Calendar.getInstance() 33 | cal.time = this 34 | return cal 35 | } 36 | 37 | fun Long.toDate(): Date = Date(this) 38 | fun Date.toLong(): Long = this.time 39 | 40 | fun Long.toDateWithHour(hour: Int): Date { 41 | val cal = this.toDate().toCalendar() 42 | cal.set(Calendar.HOUR_OF_DAY, hour) 43 | return cal.time 44 | } 45 | 46 | fun Long?.or(defaultValue: Long = 0): Long = this ?: defaultValue 47 | -------------------------------------------------------------------------------- /presentation/src/main/kotlin/gluehome/common/presentation/ui/RoundRectCornerImageView.kt: -------------------------------------------------------------------------------- 1 | package gluehome.common.presentation.ui 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.Context 5 | import android.graphics.Canvas 6 | import android.graphics.Path 7 | import android.graphics.RectF 8 | import android.util.AttributeSet 9 | import androidx.appcompat.widget.AppCompatImageView 10 | 11 | class RoundRectCornerImageView : AppCompatImageView { 12 | 13 | private val radius = 18.0f 14 | private var path: Path? = null 15 | private var rect: RectF? = null 16 | 17 | constructor(context: Context) : super(context) { 18 | init() 19 | } 20 | 21 | constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { 22 | init() 23 | } 24 | 25 | constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super(context, attrs, defStyle) { 26 | init() 27 | } 28 | 29 | private fun init() { 30 | path = Path() 31 | } 32 | 33 | @SuppressLint("DrawAllocation") 34 | override fun onDraw(canvas: Canvas) { 35 | rect = RectF(0f, 0f, this.width.toFloat(), this.height.toFloat()) 36 | path!!.addRoundRect(rect!!, radius, radius, Path.Direction.CW) 37 | canvas.clipPath(path!!) 38 | super.onDraw(canvas) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /firestore/src/main/kotlin/com/gluehome/common/threads/extensions/CoroutineFirestore.kt: -------------------------------------------------------------------------------- 1 | package com.gluehome.common.threads.extensions 2 | 3 | import com.google.firebase.firestore.DocumentReference 4 | import com.google.firebase.firestore.SetOptions 5 | import kotlin.coroutines.resume 6 | import kotlin.coroutines.resumeWithException 7 | import kotlin.coroutines.suspendCoroutine 8 | 9 | /** 10 | * Updates the document at the given [DocumentReference] (receiver) with a set of new values specified 11 | * in a map with the fields (Strings of names of the fields to be updated) and the new values of any type 12 | * supported by firestore as the map's value. Returns a completable which completes if the 13 | * operation is successful or calls onError otherwise. 14 | * 15 | * @param updatedValues [Map] of field names (keys) and updated values (values) 16 | */ 17 | suspend fun DocumentReference.updateDocumentCoroutines(updatedValues: Map): Boolean { 18 | return suspendCoroutine { cont -> 19 | update(updatedValues) 20 | .addOnSuccessListener { cont.resume(true) } 21 | .addOnFailureListener { cont.resumeWithException(it) } 22 | } 23 | } 24 | 25 | suspend fun DocumentReference.setDocumentAndMergeCoroutines(updatedValues: Map): Boolean { 26 | return suspendCoroutine { cont -> 27 | set(updatedValues, SetOptions.merge()) 28 | .addOnSuccessListener { cont.resume(true) } 29 | .addOnFailureListener { cont.resumeWithException(it) } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /data/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'kotlin-kapt' 4 | apply plugin: 'maven-publish' 5 | 6 | 7 | android { 8 | namespace = "gluehome.common.data" 9 | compileSdkVersion Versions.compile_sdk 10 | 11 | compileOptions { 12 | sourceCompatibility JavaVersion.VERSION_17 13 | targetCompatibility JavaVersion.VERSION_17 14 | } 15 | 16 | 17 | defaultConfig { 18 | minSdkVersion Versions.min_sdk 19 | targetSdkVersion Versions.target_sdk 20 | } 21 | 22 | buildTypes { 23 | debug {} 24 | release {} 25 | } 26 | 27 | sourceSets { 28 | main.java.srcDirs += 'src/main/kotlin' 29 | main.java.srcDirs += [file("$buildDir/generated/source/kapt/main")] 30 | test.java.srcDirs += 'src/test/kotlin' 31 | } 32 | 33 | lintOptions { 34 | abortOnError false 35 | xmlReport true 36 | } 37 | } 38 | 39 | dependencies { 40 | implementation "org.jetbrains.kotlin:kotlin-stdlib:${Versions.kotlin}" 41 | 42 | testImplementation 'org.amshove.kluent:kluent:1.72' 43 | testImplementation 'org.amshove.kluent:kluent-android:1.72' 44 | testImplementation 'junit:junit:4.12' 45 | 46 | } 47 | 48 | publishing { 49 | publications { 50 | mavenJava(MavenPublication) { 51 | groupId = 'com.github.gluehome.common-android' 52 | artifactId = 'data' 53 | version = '1.3.14' 54 | afterEvaluate { 55 | from components.release 56 | } 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /domain/src/main/kotlin/com/gluehome/common/domain/framework/functional/Either.kt: -------------------------------------------------------------------------------- 1 | package com.gluehome.common.domain.framework.functional 2 | 3 | /** 4 | * Represents a value of one of two possible types (a disjoint union). 5 | * Instances of [Either] are either an instance of [Left] or [Right]. 6 | * FP Convention dictates that [Left] is used for "failure" 7 | * and [Right] is used for "success". 8 | * 9 | * @see Left 10 | * @see Right 11 | */ 12 | sealed class Either { 13 | /** * Represents the left side of [Either] class which by convention is a "Failure". */ 14 | data class Left(val left: L) : Either() 15 | 16 | /** * Represents the right side of [Either] class which by convention is a "Success". */ 17 | data class Right(val right: R) : Either() 18 | 19 | val isRight get() = this is Right 20 | val isLeft get() = this is Left 21 | 22 | fun left(left: L) = Left(left) 23 | fun right(right: R) = Right(right) 24 | 25 | fun either(fnL: (L) -> Any, fnR: (R) -> Any): Any = 26 | when (this) { 27 | is Left -> fnL(left) 28 | is Right -> fnR(right) 29 | } 30 | } 31 | 32 | fun ((A) -> B).c(f: (B) -> C): (A) -> C = { 33 | f(this(it)) 34 | } 35 | 36 | fun Either.flatMap(fn: (R) -> Either): Either = 37 | when (this) { 38 | is Either.Left -> Either.Left(left) 39 | is Either.Right -> fn(right) 40 | } 41 | 42 | fun Either.map(fn: (R) -> (T)): Either = this.flatMap(fn.c(::right)) 43 | -------------------------------------------------------------------------------- /presentation/src/main/kotlin/gluehome/common/presentation/extensions/Misc.kt: -------------------------------------------------------------------------------- 1 | package gluehome.common.presentation.extensions 2 | 3 | import android.app.Activity 4 | import android.content.Context 5 | import android.os.Bundle 6 | import android.util.TypedValue 7 | import android.view.View 8 | import android.view.inputmethod.InputMethodManager 9 | 10 | fun androidx.fragment.app.Fragment.hideKeyboard() { 11 | activity!!.hideKeyboard(view!!) 12 | } 13 | 14 | //fun AppCompatActivity.hideKeyboard() { 15 | // hideKeyboard(if (currentFocus == null) View(this) else currentFocus) 16 | //} 17 | 18 | fun Context.hideKeyboard(view: View) { 19 | val inputMethodManager = getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager 20 | inputMethodManager.hideSoftInputFromWindow(view.windowToken, 0) 21 | } 22 | 23 | fun Float.toDP(context: Context): Int { 24 | return TypedValue.applyDimension( 25 | TypedValue.COMPLEX_UNIT_DIP, 26 | this@toDP, 27 | context.resources.displayMetrics 28 | ).toInt() 29 | } 30 | 31 | fun Bundle.add(key: String, value: Any): Bundle { 32 | when (value) { 33 | is Int -> this.putInt(key, value) 34 | is Long -> this.putLong(key, value) 35 | is CharSequence -> this.putCharSequence(key, value) 36 | is Char -> this.putChar(key, value) 37 | is String -> this.putString(key, value) 38 | is Float -> this.putFloat(key, value) 39 | is Double -> this.putDouble(key, value) 40 | is Short -> this.putShort(key, value) 41 | is Boolean -> this.putBoolean(key, value) 42 | } 43 | return this 44 | } 45 | 46 | -------------------------------------------------------------------------------- /logger/src/main/kotlin/com/gluehome/common/data/log/SematextTree.kt: -------------------------------------------------------------------------------- 1 | package com.gluehome.common.data.log 2 | 3 | import android.util.Log 4 | import com.sematext.logseneandroid.Logsene 5 | import com.sematext.logseneandroid.Utils 6 | import org.json.JSONObject 7 | import timber.log.Timber 8 | 9 | class SematextTree( 10 | private val logsene: Logsene, 11 | private val loggerExtraInfo: LoggerExtraInfo, 12 | private val shouldLogDebug: Boolean 13 | ) : Timber.Tree() { 14 | 15 | override fun log(priority: Int, tag: String?, message: String, t: Throwable?) { 16 | 17 | if (!shouldLogDebug && priority == Log.DEBUG) { 18 | return 19 | } 20 | 21 | val fullInfo = mutableMapOf( 22 | "level" to mapPriorityToText(priority), 23 | "message" to message 24 | ) 25 | if (priority == Log.ERROR && t != null) { 26 | 27 | fullInfo["exception"] = t.javaClass.toString() 28 | fullInfo["message"] = t.message ?: "empty error message" 29 | fullInfo["localized_message"] = t.localizedMessage ?: "empty localizedMessage" 30 | fullInfo["stacktrace"] = Utils.getStackTrace(t) 31 | } 32 | 33 | val extra = loggerExtraInfo.getAll() 34 | 35 | extra.forEach { fullInfo[it.key] = it.value } 36 | 37 | logsene.event(JSONObject(fullInfo as Map<*, *>)) 38 | } 39 | 40 | private fun mapPriorityToText(priority: Int): String { 41 | return when (priority) { 42 | Log.INFO -> "info" 43 | Log.WARN -> "warn" 44 | Log.ERROR -> "error" 45 | else -> "debug" 46 | } 47 | } 48 | 49 | 50 | } 51 | -------------------------------------------------------------------------------- /presentation/src/main/kotlin/gluehome/common/presentation/framework/archcomponents/HybridLiveEvent.kt: -------------------------------------------------------------------------------- 1 | package gluehome.common.presentation.framework.archcomponents 2 | 3 | import androidx.annotation.MainThread 4 | import androidx.lifecycle.LifecycleOwner 5 | import androidx.lifecycle.MutableLiveData 6 | import androidx.lifecycle.Observer 7 | import java.util.concurrent.atomic.AtomicBoolean 8 | 9 | /** 10 | * A lifecycle-aware observable that sends only new updates after subscription, used for events like 11 | * navigation and Snackbar messages. 12 | * 13 | * 14 | * This avoids a common problem with events: on configuration change (like rotation) an update 15 | * can be emitted if the observer is active. This LiveData only calls the observable if there's an 16 | * explicit call to setValue() or call(). 17 | * 18 | * 19 | * Note that only one observer is going to be notified of changes. 20 | */ 21 | class HybridLiveEvent : MutableLiveData() { 22 | 23 | private val pending = AtomicBoolean(false) 24 | private val sticky = AtomicBoolean(false) 25 | 26 | override fun observe(owner: LifecycleOwner, observer: Observer) { 27 | // Observe the internal MutableLiveData 28 | super.observe(owner, Observer { t -> 29 | if (sticky.get() || pending.compareAndSet(true, false)) { 30 | observer.onChanged(t) 31 | } 32 | }) 33 | } 34 | 35 | @MainThread 36 | override fun setValue(t: T?) { 37 | pending.set(true) 38 | super.setValue(t) 39 | } 40 | 41 | override fun postValue(value: T) { 42 | postValue(value, false) 43 | } 44 | 45 | fun postValue(t: T?, sticky: Boolean = false) { 46 | this.sticky.set(sticky) 47 | super.postValue(t) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # common-android [![](https://jitpack.io/v/GlueHome/common-android.svg)](https://jitpack.io/#GlueHome/common-android) 2 | > Common android/poko classes/utils for `clean architecture` with `MVVM` + `Coroutines`/`RxKotlin` 3 | 4 | # Installation 5 | 6 | main `build.gradle`: 7 | ```groovy 8 | allprojects { repositories { maven { url 'https://jitpack.io' } } } 9 | ``` 10 | 11 | main `build.gradle`: 12 | 13 | ```groovy 14 | dependencies { 15 | implementation "com.github.gluehome.common-android:data:${Versions.common}" 16 | implementation "com.github.gluehome.common-android:firestore:${Versions.common}" 17 | implementation "com.github.gluehome.common-android:domain:${Versions.common}" 18 | implementation "com.github.gluehome.common-android:rx-threads:${Versions.common}" 19 | implementation "com.github.gluehome.common-android:rx-threads-android:${Versions.common}" 20 | implementation "com.github.gluehome.common-android:presentation:${Versions.common}" 21 | } 22 | ``` 23 | 24 | # Presentation 25 | 26 | ## ViewModel and Observable extensions 27 | 28 | ```kotlin 29 | 30 | import com.gluehome.common.presentation.extensions.* 31 | 32 | class HomeFragment : BaseFragment() { 33 | 34 | override fun layoutId() = R.layout.delivery_list_fragment 35 | 36 | @Inject lateinit var viewModelFactory: ViewModelProvider.Factory 37 | private lateinit var viewModel: HomeViewModel 38 | 39 | override fun onCreate(savedInstanceState: Bundle?) { 40 | super.onCreate(savedInstanceState) 41 | appComponent.inject(this) 42 | 43 | viewModel = viewModel(viewModelFactory) { 44 | observe(deliveriesState, ::onDeliveriesStateChanged) 45 | observe(setupCompletionState, ::onSetupCompletionChanged) 46 | observe(failure, ::onFailure) 47 | } 48 | } 49 | } 50 | ``` -------------------------------------------------------------------------------- /domain/src/main/kotlin/com/gluehome/common/domain/framework/DateMapper.kt: -------------------------------------------------------------------------------- 1 | package com.gluehome.common.domain.framework 2 | 3 | import com.gluehome.common.domain.extension.toDate 4 | import java.text.ParseException 5 | import java.text.SimpleDateFormat 6 | import java.util.Calendar 7 | import java.util.Date 8 | import java.util.Locale 9 | 10 | class DateMapper { 11 | 12 | // Thursday, August 14th 13 | fun formatDate(date: Date, format: String): String { 14 | return try { 15 | SimpleDateFormat(format, Locale.getDefault()).format(date) 16 | } catch (exception: Exception) { 17 | "Unable to parse date" 18 | } 19 | } 20 | 21 | fun dateToCardHeaderTitle(date: Date): String { 22 | val cal = Calendar.getInstance() 23 | cal.time = date 24 | val day = cal.get(Calendar.DATE) 25 | 26 | return when (day) { 27 | 1, 21, 31 -> customFormat("st").format(date) 28 | 2, 22 -> customFormat("nd").format(date) 29 | 3, 23 -> customFormat("rd").format(date) 30 | else -> customFormat("th").format(date) 31 | } 32 | } 33 | 34 | private fun customFormat(ordinal: String): SimpleDateFormat { 35 | return SimpleDateFormat("EEEE, MMMM d'$ordinal'", Locale.getDefault()) 36 | } 37 | 38 | fun toDayMonthYearTimestamp(stringDate: String): Date { 39 | return transformStringDateInIntoTimestamp(stringDate, "dd/MM/yy").toDate() 40 | } 41 | 42 | private fun transformStringDateInIntoTimestamp(stringDate: String, dateFormat: String): Long { 43 | val date: Date 44 | val formatter = SimpleDateFormat(dateFormat, Locale.getDefault()) 45 | 46 | date = try { 47 | formatter.parse(stringDate) 48 | } catch (e: ParseException) { 49 | e.printStackTrace() 50 | Date() 51 | } 52 | return date.time 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /rx-threads-android/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin: 'kotlin-platform-android' 3 | apply plugin: 'kotlin-kapt' 4 | apply plugin: 'maven-publish' 5 | 6 | android { 7 | namespace = "com.gluehome.common.threads" 8 | compileSdkVersion Versions.compile_sdk 9 | 10 | compileOptions { 11 | sourceCompatibility JavaVersion.VERSION_17 12 | targetCompatibility JavaVersion.VERSION_17 13 | } 14 | 15 | testOptions.unitTests.all { 16 | testLogging { 17 | events 'passed', 'skipped', 'failed', 'standardOut', 'standardError' 18 | outputs.upToDateWhen { 19 | false 20 | } 21 | showStandardStreams = true 22 | } 23 | } 24 | 25 | defaultConfig { 26 | minSdkVersion Versions.min_sdk 27 | targetSdkVersion Versions.target_sdk 28 | } 29 | 30 | buildTypes { 31 | debug {} 32 | release {} 33 | } 34 | 35 | sourceSets { 36 | main.java.srcDirs += 'src/main/kotlin' 37 | main.java.srcDirs += [file("$buildDir/generated/source/kapt/main")] 38 | test.java.srcDirs += 'src/test/kotlin' 39 | androidTest.java.srcDirs += 'src/androidTest/kotlin' 40 | } 41 | 42 | lintOptions { 43 | abortOnError false 44 | xmlReport true 45 | } 46 | } 47 | 48 | dependencies { 49 | 50 | implementation "org.jetbrains.kotlin:kotlin-stdlib:${Versions.kotlin}" 51 | api project(':rx-threads') 52 | 53 | // Rx 54 | api 'io.reactivex.rxjava2:rxjava:2.2.8' 55 | api 'io.reactivex.rxjava2:rxandroid:2.1.1' 56 | 57 | } 58 | 59 | publishing { 60 | publications { 61 | mavenJava(MavenPublication) { 62 | groupId = 'com.github.gluehome.common-android' 63 | artifactId = 'rx-threads-android' 64 | version = '1.3.14' 65 | afterEvaluate { 66 | from components.release 67 | } 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /presentation/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'kotlin-kapt' 4 | apply plugin: 'maven-publish' 5 | 6 | android { 7 | namespace = "com.gluehome.common.presentation" 8 | compileSdkVersion Versions.compile_sdk 9 | 10 | compileOptions { 11 | sourceCompatibility JavaVersion.VERSION_17 12 | targetCompatibility JavaVersion.VERSION_17 13 | } 14 | 15 | defaultConfig { 16 | minSdkVersion Versions.min_sdk 17 | targetSdkVersion Versions.target_sdk 18 | } 19 | 20 | buildTypes { 21 | debug {} 22 | release {} 23 | } 24 | 25 | sourceSets { 26 | main.java.srcDirs += 'src/main/kotlin' 27 | main.java.srcDirs += [file("$buildDir/generated/source/kapt/main")] 28 | test.java.srcDirs += 'src/test/kotlin' 29 | } 30 | 31 | lintOptions { 32 | abortOnError false 33 | xmlReport true 34 | } 35 | } 36 | 37 | dependencies { 38 | 39 | implementation project(':domain') 40 | implementation project(':logger') 41 | 42 | // core 43 | implementation "org.jetbrains.kotlin:kotlin-stdlib:${Versions.kotlin}" 44 | implementation 'androidx.core:core-ktx:1.0.2' 45 | 46 | // rx 47 | api 'io.reactivex.rxjava2:rxandroid:2.1.1' 48 | 49 | // dagger 50 | implementation 'com.google.dagger:dagger:2.22.1' 51 | kapt 'com.google.dagger:dagger-compiler:2.22.1' 52 | 53 | // design + compat 54 | implementation 'androidx.appcompat:appcompat:1.0.2' 55 | 56 | // arch components 57 | implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0' 58 | 59 | // UI 60 | implementation 'com.google.android.material:material:1.0.0' 61 | implementation 'androidx.browser:browser:1.0.0' 62 | } 63 | 64 | publishing { 65 | publications { 66 | mavenJava(MavenPublication) { 67 | groupId = 'com.github.gluehome.common-android' 68 | artifactId = 'presentation' 69 | version = '1.3.14' 70 | afterEvaluate { 71 | from components.release 72 | } 73 | } 74 | } 75 | } -------------------------------------------------------------------------------- /logger/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'kotlin-kapt' 4 | apply plugin: 'maven-publish' 5 | 6 | android { 7 | namespace = "com.gluehome.common.logger" 8 | 9 | compileSdkVersion Versions.compile_sdk 10 | 11 | compileOptions { 12 | sourceCompatibility JavaVersion.VERSION_17 13 | targetCompatibility JavaVersion.VERSION_17 14 | } 15 | 16 | testOptions.unitTests.all { 17 | testLogging { 18 | events 'passed', 'skipped', 'failed', 'standardOut', 'standardError' 19 | outputs.upToDateWhen { 20 | false 21 | } 22 | showStandardStreams = true 23 | } 24 | } 25 | 26 | defaultConfig { 27 | minSdkVersion Versions.min_sdk 28 | targetSdkVersion Versions.target_sdk 29 | } 30 | 31 | buildTypes { 32 | debug {} 33 | release {} 34 | } 35 | 36 | sourceSets { 37 | main.java.srcDirs += 'src/main/kotlin' 38 | main.java.srcDirs += [file("$buildDir/generated/source/kapt/main")] 39 | test.java.srcDirs += 'src/test/kotlin' 40 | androidTest.java.srcDirs += 'src/androidTest/kotlin' 41 | } 42 | 43 | lintOptions { 44 | abortOnError false 45 | xmlReport true 46 | } 47 | } 48 | 49 | dependencies { 50 | 51 | implementation "org.jetbrains.kotlin:kotlin-stdlib:${Versions.kotlin}" 52 | 53 | // logs 54 | api 'com.github.GlueHome:sematext-logsene-android:2.4.2' 55 | api 'com.jakewharton.timber:timber:4.7.1' 56 | 57 | // tests 58 | testImplementation 'junit:junit:4.12' 59 | testImplementation 'org.amshove.kluent:kluent-android:1.72' 60 | testImplementation 'com.nhaarman.mockitokotlin2:mockito-kotlin:2.1.0' 61 | } 62 | 63 | 64 | publishing { 65 | publications { 66 | mavenJava(MavenPublication) { 67 | groupId = 'com.github.gluehome.common-android' 68 | artifactId = 'logger' 69 | version = '1.3.14' 70 | afterEvaluate { 71 | from components.release 72 | } 73 | } 74 | } 75 | } -------------------------------------------------------------------------------- /presentation/src/main/kotlin/gluehome/common/presentation/extensions/utils.kt: -------------------------------------------------------------------------------- 1 | package gluehome.common.presentation.extensions 2 | 3 | import android.content.Context 4 | import android.os.Build 5 | import android.text.Html 6 | import android.text.Spanned 7 | import android.view.LayoutInflater 8 | import android.view.View 9 | import android.view.ViewGroup 10 | import android.widget.EditText 11 | import android.widget.Toast 12 | import androidx.appcompat.app.AppCompatActivity 13 | import androidx.fragment.app.Fragment 14 | import java.util.Date 15 | import java.util.concurrent.ThreadLocalRandom 16 | 17 | val EditText.string 18 | get() = this.text.toString() 19 | 20 | /** 21 | * Extension function on any list that will return a list of unique random picks 22 | * from the list. If the specified number of elements you want is larger than the 23 | * number of elements in the list it returns null 24 | */ 25 | fun List.getRandomElements(numberOfElements: Int): List? { 26 | if (numberOfElements > this.size) { 27 | return null 28 | } 29 | return this.shuffled().take(numberOfElements) 30 | } 31 | 32 | fun Date.randomFromDateToDate(endDate: Date): Date { 33 | return Date(ThreadLocalRandom.current().nextLong(this.time, endDate.time)) 34 | } 35 | 36 | fun ViewGroup.inflate(layoutResource: Int): View { 37 | return LayoutInflater.from(this.context).inflate(layoutResource, this, false) 38 | } 39 | 40 | fun String.toDrawableResource(context: Context): Int { 41 | return context.resources.getIdentifier(this, "drawable", context.packageName) 42 | } 43 | 44 | fun Context.toast(message: String, length: Int = Toast.LENGTH_SHORT) { 45 | Toast.makeText(this, message, length).show() 46 | } 47 | 48 | fun AppCompatActivity.toast(message: String, length: Int = Toast.LENGTH_SHORT) { 49 | this.applicationContext.toast(message, length) 50 | } 51 | 52 | fun Fragment.toast(message: String, length: Int = Toast.LENGTH_SHORT) { 53 | this.context?.toast(message, length) 54 | } 55 | 56 | fun String.toHTML(): Spanned { 57 | return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { 58 | Html.fromHtml(this, Html.FROM_HTML_MODE_LEGACY) 59 | } else { 60 | Html.fromHtml(this) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /presentation/src/main/kotlin/gluehome/common/presentation/extensions/ViewModelExtension.kt: -------------------------------------------------------------------------------- 1 | package gluehome.common.presentation.extensions 2 | 3 | import androidx.appcompat.app.AppCompatActivity 4 | import androidx.fragment.app.Fragment 5 | import androidx.fragment.app.FragmentActivity 6 | import androidx.lifecycle.LifecycleOwner 7 | import androidx.lifecycle.LiveData 8 | import androidx.lifecycle.Observer 9 | import androidx.lifecycle.ViewModel 10 | import androidx.lifecycle.ViewModelProvider 11 | import androidx.lifecycle.ViewModelProviders 12 | import dagger.MapKey 13 | import javax.inject.Inject 14 | import javax.inject.Provider 15 | import javax.inject.Singleton 16 | import kotlin.reflect.KClass 17 | 18 | @Suppress("UNCHECKED_CAST") 19 | @Singleton 20 | class ViewModelFactory @Inject constructor(private val viewModels: MutableMap, Provider>) : 21 | ViewModelProvider.Factory { 22 | override fun create(modelClass: Class): T = viewModels[modelClass]?.get() as T 23 | } 24 | 25 | @Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER) 26 | @kotlin.annotation.Retention(AnnotationRetention.RUNTIME) 27 | @MapKey 28 | annotation class ViewModelKey(val value: KClass) 29 | 30 | inline fun AppCompatActivity.viewModel( 31 | factory: ViewModelProvider.Factory, 32 | body: T.() -> Unit 33 | ): T { 34 | val vm = ViewModelProviders.of(this, factory)[T::class.java] 35 | vm.body() 36 | return vm 37 | } 38 | 39 | inline fun Fragment.viewModel(factory: ViewModelProvider.Factory, body: T.() -> Unit): T { 40 | val vm = ViewModelProviders.of(this, factory)[T::class.java] 41 | vm.body() 42 | return vm 43 | } 44 | 45 | typealias f = (T) -> Unit 46 | 47 | inline fun FragmentActivity.getViewModel(viewModelFactory: ViewModelProvider.Factory): T { 48 | return ViewModelProviders.of(this, viewModelFactory)[T::class.java] 49 | } 50 | 51 | inline fun FragmentActivity.withViewModel( 52 | viewModelFactory: ViewModelProvider.Factory, 53 | body: T.() -> Unit 54 | ): T { 55 | val vm = getViewModel(viewModelFactory) 56 | vm.body() 57 | return vm 58 | } 59 | 60 | fun > LifecycleOwner.observe(liveData: L, body: (T?) -> Unit) { 61 | liveData.observe(this, Observer(body)) 62 | } 63 | -------------------------------------------------------------------------------- /firestore/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin: 'kotlin-platform-android' 3 | apply plugin: 'kotlin-android-extensions' 4 | apply plugin: 'kotlin-kapt' 5 | 6 | apply plugin: 'com.github.dcendents.android-maven' 7 | group = 'com.github.gluehome.common-android' 8 | 9 | android { 10 | compileSdkVersion Versions.compile_sdk 11 | buildToolsVersion Versions.build_tools 12 | 13 | compileOptions { 14 | sourceCompatibility JavaVersion.VERSION_17 15 | targetCompatibility JavaVersion.VERSION_17 16 | } 17 | 18 | testOptions.unitTests.all { 19 | testLogging { 20 | events 'passed', 'skipped', 'failed', 'standardOut', 'standardError' 21 | outputs.upToDateWhen { 22 | false 23 | } 24 | showStandardStreams = true 25 | } 26 | } 27 | 28 | defaultConfig { 29 | minSdkVersion Versions.min_sdk 30 | targetSdkVersion Versions.target_sdk 31 | } 32 | 33 | buildTypes { 34 | debug {} 35 | release {} 36 | } 37 | 38 | sourceSets { 39 | main.java.srcDirs += 'src/main/kotlin' 40 | main.java.srcDirs += [file("$buildDir/generated/source/kapt/main")] 41 | test.java.srcDirs += 'src/test/kotlin' 42 | androidTest.java.srcDirs += 'src/androidTest/kotlin' 43 | } 44 | 45 | lintOptions { 46 | abortOnError false 47 | xmlReport true 48 | } 49 | } 50 | 51 | dependencies { 52 | 53 | implementation "org.jetbrains.kotlin:kotlin-stdlib:${Versions.kotlin}" 54 | 55 | // coroutines 56 | api "org.jetbrains.kotlinx:kotlinx-coroutines-android:${Versions.coroutines}" 57 | 58 | // Rx 59 | api 'io.reactivex.rxjava2:rxjava:2.2.8' 60 | 61 | // firebase 62 | api 'com.google.firebase:firebase-firestore:18.2.0' 63 | 64 | } 65 | 66 | // build a jar with source files 67 | task sourcesJar(type: Jar) { 68 | from android.sourceSets.main.java.srcDirs 69 | classifier = 'sources' 70 | } 71 | 72 | task javadoc(type: Javadoc) { 73 | failOnError false 74 | source = android.sourceSets.main.java.sourceFiles 75 | classpath += project.files(android.getBootClasspath().join(File.pathSeparator)) 76 | classpath += configurations.compile 77 | } 78 | 79 | // build a jar with javadoc 80 | task javadocJar(type: Jar, dependsOn: javadoc) { 81 | classifier = 'javadoc' 82 | from javadoc.destinationDir 83 | } 84 | 85 | artifacts { 86 | archives sourcesJar 87 | archives javadocJar 88 | } 89 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /data/src/test/kotlin/gluehome/common/data/network/HttpLogFilterTest.kt: -------------------------------------------------------------------------------- 1 | package gluehome.common.data.network 2 | 3 | import org.amshove.kluent.shouldBe 4 | import org.junit.Test 5 | 6 | class HttpLogFilterTest { 7 | 8 | private val filter = HttpLogFilter() 9 | 10 | @Test 11 | fun `Text that should be removed`() { 12 | filter.shouldBeRemoved("alt-svc: clear") shouldBe true 13 | filter.shouldBeRemoved("content-type: application/json; charset=utf-8") shouldBe true 14 | filter.shouldBeRemoved("via: 1.1 google") shouldBe true 15 | filter.shouldBeRemoved("date: Wed, 08 Jan 2020 23:44:24 GMT") shouldBe true 16 | filter.shouldBeRemoved("x-cloud-trace-context: 8e3c87937a2a92d92c297231d965f6b8/3841932400416855268;o=1") shouldBe true 17 | filter.shouldBeRemoved("If-Modified-Since: Wed, 08 Jan 2020 23:44:24 GMT") shouldBe true 18 | filter.shouldBeRemoved("Host: mobile.gluehome.com") shouldBe true 19 | filter.shouldBeRemoved("Accept-Encoding: gzip") shouldBe true 20 | filter.shouldBeRemoved("User-Agent: Glue/v1.0.3-build-561-(2020-01-08)-20b3df3 (Android Pie 9.0, API 28; ONEPLUS A6010)") shouldBe true 21 | filter.shouldBeRemoved("Accept: application/json") shouldBe true 22 | filter.shouldBeRemoved("Connection: Keep-Alive") shouldBe true 23 | filter.shouldBeRemoved("content-length: 0") shouldBe true 24 | filter.shouldBeRemoved("server: openresty/1.15.8.1") shouldBe true 25 | filter.shouldBeRemoved("content-encoding: gzip") shouldBe true 26 | filter.shouldBeRemoved("strict-transport-security: max-age=15724800; includeSubDomains") shouldBe true 27 | filter.shouldBeRemoved("vary: Accept-Encoding") shouldBe true 28 | filter.shouldBeRemoved("Host: mobile-dev.gluehome.net") shouldBe true 29 | filter.shouldBeRemoved("cf-ray: 58b25308") shouldBe true 30 | filter.shouldBeRemoved("nel: {\"report_to\":\"cf-nel\",\"max_age\":604800}") shouldBe true 31 | filter.shouldBeRemoved("report-to: {\"endpoints\":[{\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report?lkg-colo=21&lkg-time=1601659851\"}],\"group\":\"cf-nel\",\"max_age\":604800}") shouldBe true 32 | filter.shouldBeRemoved("<-- END HTTP") shouldBe true 33 | } 34 | 35 | @Test 36 | fun `Text that should be kept`() { 37 | filter.shouldBeRemoved("glue-correlation-id: a24a80ac") shouldBe false 38 | filter.shouldBeRemoved("<-- 201 https://mobile.gluehome.com/api/v1/locks/3a4858eb-5268-44fc-bd60-944adcc3ba07/events (161ms)") shouldBe false 39 | filter.shouldBeRemoved("<-- 200 https://mobile.gluehome.com/api/v1/locks/3a4858eb-5268-44fc-bd60-944adcc3ba07 (91ms)") shouldBe false 40 | filter.shouldBeRemoved("--> POST https://mobile.gluehome.com/api/v1/locks/3a4858eb-5268-44fc-bd60-944adcc3ba07/events h2") shouldBe false 41 | filter.shouldBeRemoved("--> GET https://mobile.gluehome.com/api/v1/locks/3a4858eb-5268-44fc-bd60-944adcc3ba07 h2") shouldBe false 42 | filter.shouldBeRemoved("WWW-Authenticate: Bearer realm=\"example\"") shouldBe false 43 | filter.shouldBeRemoved("{\"id\":\"029cdccd-5056-4303-916a-adbdd17d7bbb\",\"name\":\"Cesars v5 lock 👩‍❤️‍💋‍👨\",\"isNotif") shouldBe false 44 | filter.shouldBeRemoved("[{\"id\":\"2091c1fa-efe9-4dc2-ba8c-61a8179ff836\",\"eventTime\":\"2020-04-28T17:16:46") shouldBe false 45 | } 46 | } -------------------------------------------------------------------------------- /presentation/src/main/kotlin/gluehome/common/presentation/extensions/ViewExtension.kt: -------------------------------------------------------------------------------- 1 | package gluehome.common.presentation.extensions 2 | 3 | import android.content.ClipData 4 | import android.content.ClipboardManager 5 | import android.content.Context 6 | import android.graphics.Typeface.BOLD 7 | import android.net.Uri 8 | import android.text.Spannable 9 | import android.text.SpannableString 10 | import android.text.style.ForegroundColorSpan 11 | import android.text.style.StyleSpan 12 | import android.view.View 13 | import android.view.ViewGroup 14 | import android.widget.LinearLayout 15 | import androidx.browser.customtabs.CustomTabsIntent 16 | import androidx.core.content.ContextCompat 17 | import androidx.core.view.ViewCompat 18 | import com.google.android.material.snackbar.Snackbar 19 | import com.google.android.material.textfield.TextInputEditText 20 | 21 | fun View.show() { 22 | visibility = View.VISIBLE 23 | } 24 | 25 | fun View.invisible() { 26 | visibility = View.INVISIBLE 27 | } 28 | 29 | fun View.hide() { 30 | visibility = View.GONE 31 | } 32 | 33 | fun TextInputEditText.makeCopyOnClick(viewContainer: View, bgInt: Int) { 34 | this.setOnClickListener { 35 | showSnackAndCopy(viewContainer, this.string, bgInt) 36 | } 37 | } 38 | 39 | fun View.makeCopyOnClick(viewContainer: View, textToCopy: String, bgInt: Int) { 40 | this.setOnClickListener { 41 | showSnackAndCopy(viewContainer, textToCopy, bgInt) 42 | } 43 | } 44 | 45 | private fun View.showSnackAndCopy( 46 | viewContainer: View, 47 | textToCopy: String, 48 | bgInt: Int 49 | ) { 50 | Snackbar.make(viewContainer, "'$textToCopy' copied to the clipboard", Snackbar.LENGTH_SHORT).apply { 51 | config(viewContainer.context, bgInt) 52 | show() 53 | } 54 | 55 | context?.applicationContext?.copyToClipboard(textToCopy) 56 | } 57 | 58 | fun Context.copyToClipboard(text: CharSequence) { 59 | val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager 60 | val clip = ClipData.newPlainText("label", text) 61 | clipboard.setPrimaryClip(clip) 62 | } 63 | 64 | fun Snackbar.config(context: Context, bgDrawable: Int) { 65 | val params = this.view.layoutParams as ViewGroup.MarginLayoutParams 66 | params.setMargins(12, 12, 12, 12) 67 | this.view.layoutParams = params 68 | 69 | this.view.background = context.getDrawable(bgDrawable) 70 | 71 | ViewCompat.setElevation(this.view, 6f) 72 | } 73 | 74 | fun View.makeCustomTabWith(url: String) { 75 | this.setOnClickListener { 76 | val builder: CustomTabsIntent.Builder = CustomTabsIntent.Builder() 77 | val customTabsIntent = builder.build() 78 | customTabsIntent.launchUrl(context, Uri.parse(url)) 79 | } 80 | } 81 | 82 | fun View.weight(value: Float) { 83 | val params = this.layoutParams as LinearLayout.LayoutParams 84 | params.weight = value 85 | this.layoutParams = params 86 | } 87 | 88 | fun Context.makeSpannable(template: Int, contentColor: Int, content: String = ""): SpannableString { 89 | return this.makeSpannable(this.getString(template, content), contentColor, content) 90 | } 91 | 92 | fun Context.makeSpannable(template: String, contentColor: Int, content: String = ""): SpannableString { 93 | val spannableString = SpannableString(template) 94 | 95 | val startIndex: Int 96 | val endIndex: Int 97 | 98 | if (content.isEmpty()) { 99 | startIndex = 0 100 | endIndex = spannableString.toString().length 101 | } else { 102 | startIndex = spannableString.toString().indexOf(content, 0, true) 103 | endIndex = startIndex + content.length 104 | } 105 | 106 | val foregroundColorSpan = ForegroundColorSpan(ContextCompat.getColor(this, contentColor)) 107 | 108 | spannableString.setSpan(foregroundColorSpan, startIndex, endIndex, 0) 109 | spannableString.setSpan(StyleSpan(BOLD), startIndex, endIndex, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) 110 | 111 | return spannableString 112 | } 113 | -------------------------------------------------------------------------------- /domain/src/test/kotlin/com/gluehome/common/domain/DateMapperTest.kt: -------------------------------------------------------------------------------- 1 | package com.gluehome.common.domain 2 | 3 | import com.gluehome.common.domain.framework.DateMapper 4 | import org.amshove.kluent.shouldBeEqualTo 5 | import org.junit.Before 6 | import org.junit.Test 7 | 8 | class DateMapperTest { 9 | 10 | lateinit var cut: DateMapper 11 | 12 | @Before 13 | fun setUp() { 14 | cut = DateMapper() 15 | } 16 | 17 | @Test 18 | fun `when timestamp is given a format, it should return the right string conversion`() { 19 | // given 20 | val dateStr = "22/08/2018" 21 | 22 | // when 23 | val stringDate = cut.formatDate(cut.toDayMonthYearTimestamp(dateStr), "EEEE, MMMM d") 24 | 25 | // then 26 | "Wednesday, August 22" shouldBeEqualTo stringDate 27 | } 28 | 29 | @Test 30 | fun `when timestamp for 22nd is given, it should return a friendly string conversion`() { 31 | // given 32 | val dateStr = "22/08/2018" 33 | 34 | // when 35 | val stringDate = cut.dateToCardHeaderTitle(cut.toDayMonthYearTimestamp(dateStr)) 36 | 37 | // then 38 | "Wednesday, August 22nd" shouldBeEqualTo stringDate 39 | } 40 | 41 | @Test 42 | fun `when timestamp for 21st is given, it should return a friendly string conversion`() { 43 | // given 44 | val dateStr = "21/08/2018" 45 | 46 | // when 47 | val stringDate = cut.dateToCardHeaderTitle(cut.toDayMonthYearTimestamp(dateStr)) 48 | 49 | // then 50 | "Tuesday, August 21st" shouldBeEqualTo stringDate 51 | } 52 | 53 | @Test 54 | fun `when timestamp for 3rd is given, it should return a friendly string conversion`() { 55 | // given 56 | val dateStr = "03/08/2018" 57 | 58 | // when 59 | val stringDate = cut.dateToCardHeaderTitle(cut.toDayMonthYearTimestamp(dateStr)) 60 | 61 | // then 62 | "Friday, August 3rd" shouldBeEqualTo stringDate 63 | } 64 | 65 | @Test 66 | fun `when timestamp for 31st is given, it should return a friendly string conversion`() { 67 | // given 68 | val dateStr = "31/08/2018" 69 | 70 | // when 71 | val stringDate = cut.dateToCardHeaderTitle(cut.toDayMonthYearTimestamp(dateStr)) 72 | 73 | // then 74 | "Friday, August 31st" shouldBeEqualTo stringDate 75 | } 76 | 77 | @Test 78 | fun `when timestamp for 11th is given, it should return a friendly string conversion`() { 79 | // given 80 | val dateStr = "11/08/2018" 81 | 82 | // when 83 | val stringDate = cut.dateToCardHeaderTitle(cut.toDayMonthYearTimestamp(dateStr)) 84 | 85 | // then 86 | "Saturday, August 11th" shouldBeEqualTo stringDate 87 | } 88 | 89 | @Test 90 | fun `when timestamp for 12th is given, it should return a friendly string conversion`() { 91 | // given 92 | val dateStr = "12/08/2018" 93 | 94 | // when 95 | val stringDate = cut.dateToCardHeaderTitle(cut.toDayMonthYearTimestamp(dateStr)) 96 | 97 | // then 98 | "Sunday, August 12th" shouldBeEqualTo stringDate 99 | } 100 | 101 | @Test 102 | fun `when timestamp for 13th is given, it should return a friendly string conversion`() { 103 | // given 104 | val dateStr = "13/08/2018" 105 | 106 | // when 107 | val stringDate = cut.dateToCardHeaderTitle(cut.toDayMonthYearTimestamp(dateStr)) 108 | 109 | // then 110 | "Monday, August 13th" shouldBeEqualTo stringDate 111 | } 112 | 113 | @Test 114 | fun `when timestamp for 23rd is given, it should return a friendly string conversion`() { 115 | // given 116 | val dateStr = "23/08/2018" 117 | 118 | // when 119 | val stringDate = cut.dateToCardHeaderTitle(cut.toDayMonthYearTimestamp(dateStr)) 120 | 121 | // then 122 | "Thursday, August 23rd" shouldBeEqualTo stringDate 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /presentation/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | -keepattributes Signature 2 | -keepattributes *Annotation* 3 | 4 | -keepclassmembernames class kotlinx.** { 5 | volatile ; 6 | } 7 | 8 | -keep class com.stripe.android.** { *; } 9 | 10 | 11 | 12 | -keep class * implements android.os.Parcelable { 13 | public static final android.os.Parcelable$Creator *; 14 | } 15 | 16 | -keep class org.parceler.Parceler$$Parcels 17 | 18 | -dontwarn sun.misc.** 19 | -dontwarn sun.misc.Unsafe 20 | -dontwarn javax.annotation.** 21 | 22 | -dontwarn kotlin.coroutines.** 23 | -dontnote kotlin.coroutines.** 24 | 25 | -dontwarn retrofit2.adapter.rxjava.CompletableHelper$** 26 | 27 | -keep class retrofit.** { *; } 28 | -dontwarn retrofit.** 29 | -dontwarn retrofit2.Platform* 30 | 31 | -dontwarn com.viewpagerindicator.** 32 | 33 | -dontwarn okio.** 34 | 35 | -keep class com.gluehome.backend.glue.** { *; } 36 | -keepclassmembers class com.gluehome.backend.glue.** { *; } 37 | 38 | # Fix weird issues when serializing Date objects 39 | -keepclassmembers class * implements java.io.Serializable { 40 | private static final java.io.ObjectStreamField[] serialPersistentFields; 41 | private void writeObject(java.io.ObjectOutputStream); 42 | private void readObject(java.io.ObjectInputStream); 43 | java.lang.Object writeReplace(); 44 | java.lang.Object readResolve(); 45 | } 46 | 47 | # Fix warnings for stuff in bonapputils 48 | -dontwarn java.lang.invoke** 49 | -dontwarn com.squareup.** 50 | 51 | 52 | -dontwarn android.support.v8.renderscript.* 53 | -keepclassmembers class android.support.v8.renderscript.RenderScript { 54 | native *** rsn*(...); 55 | native *** n*(...); 56 | } 57 | 58 | -keepattributes Signature 59 | -keepattributes InnerClasses 60 | 61 | -renamesourcefileattribute SourceFile 62 | -keepattributes SourceFile,LineNumberTable 63 | -keepattributes EnclosingMethod 64 | 65 | -dontwarn okhttp3.** 66 | -dontwarn javax.annotation.** 67 | -dontwarn org.conscrypt.** 68 | # A resource is loaded with a relative path so the package of this class must be preserved. 69 | -keepnames class okhttp3.internal.publicsuffix.PublicSuffixDatabase 70 | 71 | -printusage unused.txt 72 | 73 | # Joda Library 74 | -dontwarn org.joda.convert.** 75 | 76 | -dontwarn rx.** 77 | -dontnote rx.** 78 | -dontwarn io.reactivex.** 79 | -dontnote io.reactivex.** 80 | -dontnote com.crashlytics.** 81 | 82 | # NETWORK 83 | -keep class com.google.gson.** { *; } 84 | -keep class com.google.inject.** { *; } 85 | -keep class javax.inject.** { *; } 86 | -keep class retrofit.** { *; } 87 | 88 | -keep class com.gluehome.inhome.data.network.** { *; } 89 | -keep class com.gluehome.inhome.data.features.** { *; } 90 | -keep class com.gluehome.domain.user.** { *; } 91 | 92 | -dontwarn com.instabug.** 93 | 94 | -dontnote android.net.http.* 95 | -dontnote org.apache.commons.codec.** 96 | -dontnote org.apache.http.** 97 | 98 | -keep class * implements android.os.Parcelable { 99 | public static final android.os.Parcelable$Creator *; 100 | } 101 | 102 | # crashlytics 103 | -keepattributes *Annotation* 104 | -keepattributes SourceFile,LineNumberTable 105 | -keep public class * extends java.lang.Exception 106 | -keep class com.crashlytics.** { *; } 107 | -dontwarn com.crashlytics.** 108 | -printmapping mapping.txt 109 | 110 | -keep public class com.google.android.gms.* { public *; } 111 | -dontwarn com.google.android.gms.** 112 | 113 | 114 | -keep class * extends java.util.ListResourceBundle { 115 | protected Object[][] getContents(); 116 | } 117 | 118 | -keep public class com.google.android.gms.common.internal.safeparcel.SafeParcelable { 119 | public static final *** NULL; 120 | } 121 | 122 | -keepnames @com.google.android.gms.common.annotation.KeepName class * 123 | -keepclassmembernames class * { 124 | @com.google.android.gms.common.annotation.KeepName *; 125 | } 126 | 127 | -keepnames class * implements android.os.Parcelable { 128 | public static final ** CREATOR; 129 | } 130 | 131 | -dontwarn sun.misc.Unsafe 132 | -dontwarn javax.annotation.** 133 | 134 | -dontwarn javax.annotation.** 135 | 136 | -dontwarn dagger.** 137 | -dontwarn com.google.common.** 138 | -dontwarn com.google.googlejavaformat.** 139 | 140 | -dontnote com.google.android.gms.** 141 | 142 | # Prevent proguard from stripping interface information from TypeAdapterFactory, 143 | # JsonSerializer, JsonDeserializer instances (so they can be used in @JsonAdapter) 144 | -keep class * implements com.google.gson.TypeAdapterFactory 145 | -keep class * implements com.google.gson.JsonSerializer 146 | -keep class * implements com.google.gson.JsonDeserializer 147 | 148 | # https://github.com/Kotlin/kotlinx.coroutines 149 | -keepclassmembernames class kotlinx.** { 150 | volatile ; 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 | --------------------------------------------------------------------------------