Commit 5f48d403 authored by Vladislav Bogdashkin's avatar Vladislav Bogdashkin 🎣

Merge branch 'feature/estates_flow_upgrade' into develop

parents e6aa7f16 d3b59966
......@@ -175,6 +175,10 @@ dependencies {
//RxKotlin
implementation("io.reactivex.rxjava2:rxkotlin:$rxKotlinVersion")
//Arch Lifecycle
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
//Tests
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test:runner:1.2.0'
......
......@@ -102,6 +102,7 @@ abstract class BigantoBaseController<VS : BigantoBaseViewState,V: BigantoBaseCon
}
override fun handleBack(): Boolean {
detachDisposable.clear()
router.popController(this)
return true
// return super.handleBack()
......
......@@ -11,11 +11,11 @@ import com.biganto.visual.roompark.data.repository.mapper.fromRaw
import com.biganto.visual.roompark.data.repository.mapper.fromRawList
import com.biganto.visual.roompark.domain.contract.DealContract
import com.biganto.visual.roompark.domain.custom_exception.CustomApiException
import com.biganto.visual.roompark.domain.model.DealModel
import com.biganto.visual.roompark.domain.model.EstateModel
import com.biganto.visual.roompark.domain.model.fromEntity
import io.reactivex.Completable
import io.reactivex.Observable
import io.reactivex.rxkotlin.Observables
import io.reactivex.schedulers.Schedulers
import timber.log.Timber
import javax.inject.Inject
......@@ -37,72 +37,32 @@ class EstateRepository @Inject constructor(
Timber.d("Estate Repository Created $this")
}
override fun fetchFavorites(user:UserEntity): Observable<Iterable<EstateEntity>> =
override fun fetchFavorites(user:UserEntity): Observable<List<EstateEntity>> =
Observables.zip(
api.getFavorites(user.authToken)
.doOnError(Timber::e)
.map { fromRawList(it, ::fromRaw) }
.doOnNext {
it.forEach { estate ->
estate.setFavorite(true)
estate.user = user
}
}
.flatMap(db::upsertEstates)
.doOnNext{ db.refreshUser(user) }
override fun fetchDeals(user:UserEntity): Observable<List<EstateEntity>> =
api.getDeals(user.authToken)//api.getDeals(user.authToken)
.doOnError(Timber::e)
.map { List(it.size){index -> fromRaw(it[index],user)} }
.flatMap(db::upsertDeals)
.map { it.map {deal -> deal.estate as EstateEntity } }
.doOnNext { db.refreshUser(user) }
private val getFavoritesApi: Observable<List<EstateEntity>> =
local.recentUser()
.flatMap {
when (it) {
is UserState.Authenticated -> db.fetchUser(it.uuid.toInt())
else -> throw CustomApiException.NotAuthorizedException()
}
}
.flatMap { user ->
api.getFavorites(user.authToken)
,db.getUserFavorites(user.uuid)
.toList().toObservable()
.doOnError(Timber::e)
.map { fromRawList(it, ::fromRaw) }
.doOnNext {
it.forEach { estate ->
){apiList,dbList ->
apiList.forEach { estate ->
estate.setFavorite(true)
estate.user = user
}
}
.doOnNext(db::blockingUpsert)
.doOnNext{ db.refreshUser(user) }
}
dbList
.filterNotNull()
.filter { dbFav ->!apiList.map{it.id}.contains(dbFav.id) }
.let{ db.deleteEstate(it) }
private val getFavoritesDb: Observable<List<EstateEntity>> =
local.recentUser()
.flatMap {
when (it) {
is UserState.Authenticated -> db.fetchUser(it.uuid.toInt()).take(1)
else -> throw CustomApiException.NotAuthorizedException()
}
return@zip apiList
}
.flatMap {
db.getUserFavorites(it.uuid)
.doOnError(Timber::e)
.toList().toObservable()
}
.flatMap(db::upsertEstates)
.map { it.toList() }
.doOnNext{ db.refreshUser(user) }
override fun getFavorites(): Observable<List<EstateModel>> {
return Observable.mergeDelayError(
arrayListOf(
getFavoritesApi, getFavoritesDb
)
).map { fromEntity(it, ::fromEntity) }
.doOnError(Timber::e)
}
override fun fetchDeals(user:UserEntity): Observable<List<DealEntity>> =
getDealsApi(user)
private fun fetchEstateDb(id: Int) = db.getEstate(id)
......@@ -113,23 +73,29 @@ class EstateRepository @Inject constructor(
private val getDealsApi: Observable<List<DealEntity>> =
local.recentUser()
.doOnError (Timber::e)
.flatMap {
when (it) {
is UserState.Authenticated -> db.fetchUser(it.uuid.toInt())
else -> throw CustomApiException.NotAuthorizedException()
}
}
.doOnError (Timber::e)
.flatMap { user ->
api.getDeals(user.authToken)//api.getDeals(user.authToken)
override fun getDealsApi(user: UserEntity): Observable<List<DealEntity>> =
Observables.zip(
api.getDeals(user.authToken)
.doOnError(Timber::e)
.map { List(it.size){index -> fromRaw(it[index],user)} }
,db.getUserDeals(user.uuid)
.toList().toObservable()
.doOnError(Timber::e)
){apiList,dbList ->
apiList.forEach { deal ->
dbList?.firstOrNull { dbDeal-> dbDeal?.id == deal.id }?.let {
deal.setRead(it.read)
}
deal.user = user
}
dbList
.filterNotNull()
.filter { dbFav ->!apiList.map{it.id}.contains(dbFav.id) }
.let{ db.deleteDeal(it) }
return@zip apiList
}
.doOnNext(db::blockingUpsert)
.doOnNext { db.refreshUser(user) }
}
private val getDealsDb: Observable<List<DealEntity>> =
local.recentUser()
......@@ -142,21 +108,11 @@ class EstateRepository @Inject constructor(
.map { it.deals?.map {deal -> deal as DealEntity } }
override fun getDeals(): Observable<List<DealModel>> {
return Observable.mergeDelayError(
arrayListOf(
getDealsDb,
getDealsApi
)
)
.map { fromEntity(it, ::fromEntity) }
.doOnError(Timber::e)
.subscribeOn (Schedulers.io())
}
override fun setDealRead(dealId: String): Completable =
db.setDealReadState(dealId,true)
.doOnError { Timber.e(it) }
......
......@@ -67,7 +67,9 @@ class SubscriptionRepository @Inject constructor(
apiSubs.forEach { apiSub ->
val cachedSub =
userSubs?.firstOrNull { s -> s.topic == apiSub.topic && s.number == apiSub.estate_id }
userSubs?.firstOrNull { s ->
s.topic == apiSub.topic && s.number == apiSub.estate_id
}
as SubscriptionEntity?
?: SubscriptionEntity()
.apply {
......@@ -75,10 +77,15 @@ class SubscriptionRepository @Inject constructor(
setTopic(apiSub.topic)
setNumber(apiSub.estate_id)
}
cachedSub.setState(apiSub.state)
cachedSub.setState(apiSub.active)
newSubList.add(cachedSub)
}
userSubs?.let { db.deleteSubscriptions(it) }
userSubs?.filter { !newSubList.map { s ->s.id }.contains(it.id) }?.toList()
?.let {
Timber.w("to delete; ${it.size}")
db.deleteSubscriptions(it)
}
return db.upsert(newSubList)
.map {list -> list.map {
fromEntity(
......@@ -101,7 +108,7 @@ class SubscriptionRepository @Inject constructor(
,topicName = topic
,topicId = topic_id)
.flatMap { saveSubscribtions(user,it.subscriptions?: arrayListOf()) }
.doOnNext { db.refreshUser(user) }
.doOnNext { db.refreshUser(user).blockingFirst() }
override fun unSubscribeTopicResult(
user:UserEntity,
......
package com.biganto.visual.roompark.data.memcache
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleObserver
import androidx.lifecycle.OnLifecycleEvent
import androidx.lifecycle.ProcessLifecycleOwner
import com.jakewharton.rxrelay2.Relay
import com.jakewharton.rxrelay2.ReplayRelay
import io.reactivex.Observable
import io.reactivex.disposables.CompositeDisposable
import timber.log.Timber
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.TimeUnit
/**
* Created by Vladislav Bogdashkin on 19.10.2018.
*/
/**
* @param[maxSize] max entries count before automatic save && flush
* @param[flushInterval] time without any actions with cache in SECONDS before automatic save && flush
* */
abstract class EntityCache<K,V>
constructor(
private val maxSize: Int = 300,
private val flushInterval: Long = TimeUnit.MINUTES.toSeconds(1)
) : Cache<K,V> {
private val disposable = CompositeDisposable()
protected abstract fun saveDelegate (values:List<V>)
protected abstract fun readDelegate (key:K) : V?
protected abstract fun deleteDelegate (value:V)
protected val notifier: Relay<V> = ReplayRelay.create<V>().toSerialized()
init {
disposable.addAll(
Observable.interval(flushInterval, flushInterval, TimeUnit.SECONDS)
.doOnTerminate { clear() }
.doOnDispose { clear() }
.subscribe { clear() }
)
}
override fun contains(key: K): Boolean = keyMap.containsKey(key)
protected val locker = Any()
private var lastFlushTime = System.nanoTime()
private val keyMap = ConcurrentHashMap<K, V>()
override val size: Int
get() = keyMap.size
override val toList
get() = keyMap.toList()
fun deleteEntity(key: K){
synchronized(locker) {
deleteItem(key)
}
}
private fun deleteItem(key:K){
synchronized(locker) {
lastFlushTime = System.nanoTime()
var toDelete = keyMap.remove(key)
if (toDelete == null)
toDelete = readDelegate(key)
if (toDelete == null) return // -> нет в хранилище
deleteDelegate(value = toDelete)
}
}
fun deleteEntitys(keys: List<K>){
synchronized(locker) {
keys.forEach { deleteItem(it)}
}
}
override fun set(key: K, value: V?) {
synchronized(locker) {
lastFlushTime = System.nanoTime()
if (keyMap.size > maxSize)
clear()
value?.let {
keyMap[key]=it
notifier.accept(it)
}
}
}
override fun remove(key: K) = keyMap.remove(key)
override fun get(key: K): V? {
synchronized(locker) {
lastFlushTime = System.nanoTime()
return keyMap[key] ?: readDelegate(key)
}
}
override fun saveAll(){
synchronized(locker)
{
if (keyMap.size > 0) {
Timber.d("Going to save items: ${keyMap.values.size}")
saveDelegate(keyMap.values.toList())
}
}
}
override fun clear() {
if (keyMap.size == 0) return
synchronized(locker)
{
saveAll()
keyMap.clear()
}
}
override fun removeAll(): List<V>? {
val values= keyMap.values.toList()
keyMap.clear()
return values
}
private fun recycle() {
if (keyMap.size<=0) return
val shouldRecycle = System.nanoTime() - lastFlushTime >= TimeUnit.MILLISECONDS.toNanos(flushInterval)
if (!shouldRecycle) return
keyMap.clear()
}
}
interface Cache<K,V> {
val size: Int
val toList:List<Pair<K,V>>
operator fun set(key: K, value: V?)
fun contains(key: K): Boolean
operator fun get(key: K): V?
fun remove(key: K): V?
fun removeAll(): List<V>?
fun clear()
fun saveAll()
}
/**
* Suppress warning Leaking This as we sure to make singletone instance of object in single-thread
* more info and
* @see <a href="https://stackoverflow.com/questions/3921616/leaking-this-in-constructor-warning">discussion</a>
*/
@Suppress("LeakingThis")
abstract class LifeCycleCache<K,V>(size:Int, secondsToFlush:Long)
: EntityCache<K, V>(maxSize=size,flushInterval = secondsToFlush)
, LifecycleObserver
{
init {
ProcessLifecycleOwner.get().lifecycle
.addObserver(this)
}
@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
fun onAppPaused() {
saveAll()
}
}
......@@ -23,7 +23,7 @@ data class SubscriptionStatusRaw(
val topic:String,
val estate_id: String,
@Expose
val state: Boolean = true
val active: Boolean = true
)
data class DealRaw(
......@@ -41,6 +41,7 @@ data class EstateRaw(
val id:Int,
val type:String,
val number:String,
val available:Boolean,
val common_info: CommonInfoRaw,
val plan_png:PlanRaw?,
val plan_jpg:PlanRaw?,
......
......@@ -32,7 +32,7 @@ interface IDb {
fun getPhotos(albumId: Int): Observable<GalleryPhotoEntity>
fun getPhoto(photoId: Int): Observable<GalleryPhotoEntity>
fun getAlbum(albumId: Int): Observable<ImageAlbumEntity>
fun getUserFavorites(uuid: Int): Observable<EstateEntity>
fun getUserFavorites(uuid: Int): Observable<EstateEntity?>
fun fetchAllUsers(): Observable<List<UserEntity>>
fun getEstate(estateId: Int): Observable<EstateEntity>
fun upsertEstate(entity: EstateEntity)
......@@ -78,4 +78,9 @@ interface IDb {
fun upsertEstates(entity: List<EstateEntity>): Observable<Iterable<EstateEntity>>?
fun upsertDeals(entity: List<DealEntity>): Observable<Iterable<DealEntity>>
fun deleteSubscriptions(entities: List<Subscription>)
fun deleteEstate(entity: List<EstateEntity>)
fun deleteEstate(entity: EstateEntity)
fun getUserDeals(uuid: Int): Observable<DealEntity?>
fun deleteDeal(entity: DealEntity)
fun deleteDeal(entity: List<DealEntity>)
}
\ No newline at end of file
......@@ -30,7 +30,7 @@ import javax.inject.Inject
*/
private const val DATABASE_VERSION = 14
private const val DATABASE_VERSION = 15
@Module
class DbModule{
......@@ -190,12 +190,17 @@ class RequeryRepository @Inject constructor(
.get()
.observableResult()
override fun getUserFavorites(uuid: Int): Observable<EstateEntity> =
override fun getUserFavorites(uuid: Int): Observable<EstateEntity?> =
store.select(EstateEntity::class)
.where(EstateEntity.USER_ID.eq(uuid))
.and(EstateEntity.FAVORITE.eq(true))
.get().observable()
override fun getUserDeals(uuid: Int): Observable<DealEntity?> =
store.select(DealEntity::class)
.where(DealEntity.USER_ID.eq(uuid))
.get().observable()
override fun setArticleReadState(id:Int,state:Boolean): Completable =
store.update(ArticleEntity::class)
.set(ArticleEntity.READ,state)
......@@ -409,6 +414,14 @@ class RequeryRepository @Inject constructor(
.where(TourFileJunctionEntity.TOUR.eq(tourId))
.get()
override fun deleteEstate(entity:EstateEntity) = deleteBlocking(entity)
override fun deleteDeal(entity:DealEntity) = deleteBlocking(entity)
override fun deleteEstate(entity:List<EstateEntity>) = deleteBlocking(entity)
override fun deleteDeal(entity:List<DealEntity>) = deleteBlocking(entity)
override fun deleteFile(entity:FileEntity) = deleteBlocking(entity)
override fun deleteFiles(entity:List<FileEntity>) = deleteBlocking(entity)
......
......@@ -15,6 +15,7 @@ interface Estate : Persistable {
val id: Int
val type: String
val number: String
val available: Boolean
@get:Nullable
val sectionBegin: Int?
@get:Nullable
......
......@@ -126,6 +126,7 @@ fun fromRaw(raw:EstateRaw):EstateEntity{
entity.setId(raw.id)
entity.setType(raw.type)
entity.setNumber(raw.number)
entity.setAvailable(raw.available)
entity.setSectionBegin(raw.common_info.section_begin)
entity.setSectionEnd(raw.common_info.section_end)
entity.setPlanJpgUrl(raw.plan_jpg?.url)
......
......@@ -3,7 +3,6 @@ package com.biganto.visual.roompark.domain.contract
import com.biganto.visual.roompark.data.repository.db.requrey.model.DealEntity
import com.biganto.visual.roompark.data.repository.db.requrey.model.EstateEntity
import com.biganto.visual.roompark.data.repository.db.requrey.model.UserEntity
import com.biganto.visual.roompark.domain.model.DealModel
import com.biganto.visual.roompark.domain.model.EstateModel
import io.reactivex.Completable
import io.reactivex.Observable
......@@ -15,11 +14,11 @@ import io.reactivex.Observable
interface DealContract{
fun getFavorites() : Observable<List<EstateModel>>
// fun getFavorites() : Observable<List<EstateModel>>
fun getEstate(estateId: Int): Observable<EstateModel>
fun getDeals(): Observable<List<DealModel>>
fun fetchEstate(building: Int, number: Int): Observable<EstateModel>
fun setDealRead(dealId: String): Completable
fun fetchDeals(user: UserEntity): Observable<List<EstateEntity>>
fun fetchFavorites(user: UserEntity): Observable<Iterable<EstateEntity>>
fun fetchDeals(user: UserEntity): Observable<List<DealEntity>>
fun fetchFavorites(user: UserEntity): Observable<List<EstateEntity>>
fun getDealsApi(user: UserEntity): Observable<List<DealEntity>>
}
\ No newline at end of file
......@@ -6,7 +6,6 @@ import com.biganto.visual.roompark.domain.model.SubscriptionModel
import com.biganto.visual.roompark.domain.model.SubscriptionTopic
import com.biganto.visual.roompark.domain.use_case.DealseUseCase
import com.biganto.visual.roompark.domain.use_case.SubscriptionUseCase
import io.reactivex.Completable
import io.reactivex.Observable
import timber.log.Timber
import javax.inject.Inject
......@@ -21,8 +20,7 @@ class DealInteractor @Inject constructor(
){
fun getDeal(id:String): Observable<DealModel> =
useCase.getDeals()
.doOnNext { Timber.d("$it") }
useCase.prefetchDeal()
.map {deals -> deals.first { it.id==id } }
fun getStatusList() = Observable.just(statusList.sortedBy{ it.orderId })
......@@ -101,4 +99,4 @@ class DealInteractor @Inject constructor(
)
)
}
}
\ No newline at end of file
}
\ No newline at end of file
package com.biganto.visual.roompark.domain.interactor
import com.biganto.visual.roompark.domain.model.DealModel
import com.biganto.visual.roompark.domain.model.StatusModel
import com.biganto.visual.roompark.domain.use_case.DealseUseCase
import io.reactivex.Observable
......@@ -13,7 +14,9 @@ class DealsInteractor @Inject constructor(
val useCase:DealseUseCase
){
fun fetchDeals() = useCase.getDeals()
fun fetchDeals(): Observable<List<DealModel>> = useCase.prefetchDeal()
fun getDealsApi(): Observable<List<DealModel>> = useCase.getDeals()
// Single.just(arrayListOf(dealFlat, dealParkign))
fun getStatusList() = Observable.just(statusList.sortedBy{ it.orderId })
......
......@@ -15,6 +15,9 @@ class FavoritesInteractor @Inject constructor(
private val estateUseCase: EstateUseCase
) {
fun cachedFavorites() =
estateUseCase.prefetchFavorites()
fun getFavoritesForCurrentUser() =
estateUseCase.fetchFavorites()
// Single.just(parkingEstateSample )
......@@ -52,6 +55,7 @@ class FavoritesInteractor @Inject constructor(
albumId = 10,
multitourId = null,
url = null
,availableStatus = true
),
EstateModel(
id = 1905,
......@@ -81,7 +85,7 @@ class FavoritesInteractor @Inject constructor(
albumId = 10,
multitourId = null,
url = null
),
,availableStatus = true),
EstateModel(
id = 1774,
type = FlatType.valueOf("flat".toUpperCase()),
......@@ -118,6 +122,7 @@ class FavoritesInteractor @Inject constructor(
albumId = 10,
url = null,
multitourId = 5790
,availableStatus = true
// ,explications = arrayListOf<ExplicationListModel>(
// ExplicationListModel(
// planId = 0,
......
......@@ -51,6 +51,7 @@ data class EstateModel(
val id:Int,
val type:FlatType,
val number:String,
val availableStatus:Boolean,
val sectionBegin:Int?=null,
val sectionEnd:Int?=null,
val planPNG:PlanModel?,
......@@ -90,6 +91,7 @@ fun fromEntity(entity:EstateEntity): EstateModel {
id = entity.id,
type = FlatType.valueOf(entity.type.toUpperCase()),
number = entity.number,
availableStatus = entity.available,
sectionBegin = entity.sectionBegin,
sectionEnd = entity.sectionEnd,
planPNG = null,
......
package com.biganto.visual.roompark.domain.use_case
import com.biganto.visual.roompark.data.repository.db.requrey.model.DealEntity
import com.biganto.visual.roompark.domain.contract.AuthContract
import com.biganto.visual.roompark.domain.contract.DealContract
import com.biganto.visual.roompark.domain.model.DealModel
import com.biganto.visual.roompark.domain.model.fromEntity
import io.reactivex.Observable
import timber.log.Timber
import javax.inject.Inject
/**
......@@ -8,10 +14,23 @@ import javax.inject.Inject
*/
class DealseUseCase @Inject constructor(
private val contract: DealContract
private val contract: DealContract,
private val authContract: AuthContract
) {
fun getDeals() = contract.getDeals()
fun prefetchDeal(): Observable<List<DealModel>> =
authContract.currentUser()
.map {user ->
user.deals?.asSequence()
?.map { it as DealEntity }
?.filterNotNull()?.toList()
}
.map { fromEntity(it, ::fromEntity) }
fun getDeals(): Observable<List<DealModel>> =
authContract.currentUser()
.flatMap(contract::getDealsApi)
.map { fromEntity(it, ::fromEntity) }
fun setDealRead(id:String) = contract.setDealRead(id)
......
package com.biganto.visual.roompark.domain.use_case
import com.biganto.visual.roompark.data.repository.db.requrey.model.EstateEntity
import com.biganto.visual.roompark.domain.contract.AuthContract
import com.biganto.visual.roompark.domain.contract.DealContract
import com.biganto.visual.roompark.domain.contract.FlatPlanContract
import com.biganto.visual.roompark.domain.model.EstateModel
import com.biganto.visual.roompark.domain.model.fromEntity
import io.reactivex.Observable
import javax.inject.Inject
/**
......@@ -10,10 +15,23 @@ import javax.inject.Inject
class EstateUseCase @Inject constructor(
private val contract: DealContract,
private val planContract: FlatPlanContract
private val planContract: FlatPlanContract,
private val authContract: AuthContract
) {
fun fetchFavorites() = contract.getFavorites()
fun prefetchFavorites() =
authContract.currentUser()
.map {user -> user.estates?.asSequence()
?.map { it as EstateEntity }
?.filter{ it.favorite }?.filterNotNull()?.toList()
}
.map { fromEntity(it, ::fromEntity) }
fun fetchFavorites(): Observable<List<EstateModel>> =
authContract.currentUser()
.flatMap (contract::fetchFavorites)
.map { fromEntity(it, ::fromEntity) }
fun getEstate(estateId: Int) = contract.getEstate(estateId)
......
......@@ -21,7 +21,10 @@ class PlanTypesUseCase @Inject constructor(
) {
private fun fetchUserEstates(user:UserEntity): Observable<List<EstateEntity>> =
Observables.zip(dealContract.fetchDeals(user),dealContract.fetchFavorites(user))
Observables.zip(
dealContract.fetchDeals(user)
.map { it.map {deal -> deal.estate as EstateEntity } }
,dealContract.fetchFavorites(user))
{t1,t2 -> t1+t2}
private val fetchAllPlanTypes =
......
......@@ -12,7 +12,7 @@ import com.biganto.visual.roompark.domain.model.SubscriptionTopic
import com.biganto.visual.roompark.domain.model.TitledSubscriptionModel
import com.biganto.visual.roompark.domain.model.fromEntity
import io.reactivex.Observable
import io.reactivex.functions.BiFunction
import io.reactivex.rxkotlin.Observables
import io.reactivex.schedulers.Schedulers
import timber.log.Timber
import javax.inject.Inject
......@@ -31,10 +31,9 @@ class SubscriptionUseCase @Inject constructor(
fun subscribeTopic(subId: Int, topic: SubscriptionTopic): Observable<List<TitledSubscriptionModel>> =
Observable.zip(auth.currentUser(), utils.getDeviceId()
, BiFunction<UserEntity,String,SubscribeRequestModel> {
Observables.zip(auth.currentUser(), utils.getDeviceId()){
user, token -> SubscribeRequestModel(user,token)
})
}
.flatMap {requestModel ->
subscription.subscribeTopicResult(
requestModel.user
......@@ -50,10 +49,9 @@ class SubscriptionUseCase @Inject constructor(
fun unSubscribeTopic(subId: Int, topic: SubscriptionTopic): Observable<List<TitledSubscriptionModel>> =
Observable.zip(auth.currentUser(), utils.getDeviceId()
, BiFunction<UserEntity,String,SubscribeRequestModel> {
Observables.zip(auth.currentUser(), utils.getDeviceId()){
user, token -> SubscribeRequestModel(user,token)
})
}
.flatMap {requestModel ->
subscription.unSubscribeTopicResult(
requestModel.user
......@@ -102,7 +100,6 @@ class SubscriptionUseCase @Inject constructor(
.map {user ->
var sub = user.subscriptions
?.firstOrNull { it.topic == topic.topicName && it.number == topic.topicId }
Timber.d("fetched topic: $sub")
if (sub == null) {
sub = SubscriptionEntity()
sub.setOwner(user)
......@@ -110,9 +107,7 @@ class SubscriptionUseCase @Inject constructor(
sub.setNumber(topic.topicId)
sub.setState(false)
}
Timber.w("sub is : $sub")
subscription.saveSubscribeState(sub as SubscriptionEntity)
.doOnNext { Timber.d("zzz blocking $it") }
.blockingFirst()
}
.map(::fromEntity)
......
......@@ -107,14 +107,13 @@ class DealScreenController :
.observeOn(AndroidSchedulers.mainThread())
private var servedDeal : DealModel? = null
override fun onAttach(view: View) {
super.onAttach(view)
detachDisposable.addAll(
toFlatView.clicks()
.map { servedDeal?.estate?.id?: -1000}
.map { dealModel?.estate?.id?: -1000}
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
Timber.d("got card clicked $it")
......@@ -217,6 +216,7 @@ class DealScreenController :
silentCheck = true
sw.isChecked = viewState.subState
}
toolBar.headerToolbar.invalidate()
}
private fun render(viewState: DealScreenViewState.SubscriptionError) {
......@@ -241,6 +241,8 @@ class DealScreenController :
silentCheck = true
sw.isChecked != sw.isChecked
}
viewState.restore.deal?.let { setUpDeal(it) }
}
private fun render(viewState: DealScreenViewState.ToursLoaded) {
......@@ -263,51 +265,56 @@ class DealScreenController :
private var dealModel:DealModel? = null
private fun render(viewState: DealScreenViewState.LoadDeal) {
servedDeal = viewState.estate
private fun setUpDeal(deal:DealModel){
dealModel = deal
dealModel?.let {
startTourView.setGone(servedDeal?.estate?.multitourId == null)
startTourView.setGone(it.estate.multitourId == null)
dealTitle.text = resources?.getString(
viewState.estate.estate.type.typeDoubleString(),
viewState.estate.estate.number
it.estate.type.typeDoubleString(),
it.estate.number
)
info1.title.text = resources?.getString(R.string.building)
info1.text.text = viewState.estate.estate.commonInfo?.building.toString()
info1.text.text = it.estate.commonInfo?.building.toString()
info2.title.text = resources?.getString(R.string.section_begin)
info2.text.text = viewState.estate.estate.commonInfo?.section_begin.toString()
info2.text.text = it.estate.commonInfo?.section_begin.toString()
info3.title.text = resources?.getString(R.string.floor)
info3.text.text = viewState.estate.estate.commonInfo?.floor.toString()
info3.text.text = it.estate.commonInfo?.floor.toString()
info4.title.text = resources?.getString(R.string.area)
info4.text.text =
resources?.getString(R.string.area_value,viewState.estate.estate.commonInfo?.area)
resources?.getString(R.string.area_value,it.estate.commonInfo?.area)
dealSum.text = viewState.estate.opportunitySum.toRubly()
dealPayed.text = viewState.estate.paymentSum.toRubly()
dealSumToPay.text = viewState.estate.amount_pay_sum.toRubly()
dealSum.text = it.opportunitySum.toRubly()
dealPayed.text = it.paymentSum.toRubly()
dealSumToPay.text = it.amount_pay_sum.toRubly()
viewState.estate.estate.multitourPreview?.let {
it.estate.multitourPreview?.let {url ->
Glide.with(tourScreen)
.load(it)
.load(url)
.diskCacheStrategy(DiskCacheStrategy.ALL)
.into(tourScreen)
}
dealModel = viewState.estate
toolBar.setToolbar(HeaderToolbarModel(
true
,resources?.getString(R.string.deal_back_chevron_title)
,null
,true)
)
}
}
progressLayout.removeAllViews()
private fun render(viewState: DealScreenViewState.LoadDeal) {
setUpDeal(viewState.estate)
progressLayout.removeAllViews()
viewState.statusList.forEach {
val statusLayout = LayoutInflater.from(activity)
......@@ -316,9 +323,7 @@ class DealScreenController :
,false)
as LinearLayout
Timber.d("layouted: $statusLayout")
val statusCeil = statusLayout.findViewById<StatusProgressCeil>(R.id.status)
Timber.d("layouted ceail : $statusCeil")
val position = it.orderId
val statusCount = viewState.statusList.size
......@@ -333,11 +338,11 @@ class DealScreenController :
statusCeil.invalidate()
val statusTitle = statusLayout.findViewById<MaterialTextView>(R.id.title)
Timber.d("layouted statusTitle : $statusTitle")
statusTitle.text = it.shortTitle
progressLayout.addView(statusLayout)
}
progressLayout.invalidate()
}
private fun getComponent() = DaggerDealScreenComponent.factory()
......
......@@ -38,19 +38,22 @@ class DealScreenPresenter @Inject constructor(
restoreStateObservable.accept(DealScreenViewState.RestoreView(restoreModel))
}
override fun bindIntents() {
val fetchDeal = interactor.getDeal(dealId)
private val fetchDeal = interactor.getDeal(dealId)
.doOnNext { restoreModel.deal = it }
.map<DealScreenViewState>{ deal ->
DealScreenViewState.LoadDeal(deal ,interactor.getStatusListSync())
}.share()
}
override fun bindIntents() {
val setRead = interactor.setDealRead(dealId)
.andThen(Observable.just(DealScreenViewState.Idle()))
val onSubChecked = intent(DealScreen::onSubscription)
.filter { restoreModel.sub != null }
.flatMap { newState ->
......@@ -72,17 +75,19 @@ class DealScreenPresenter @Inject constructor(
interactor.getSubscriptions(
(fetchedDealViewState as DealScreenViewState.LoadDeal).estate.estate_id
)
.doAfterNext { restoreModel.sub = it }
.doOnNext { restoreModel.sub = it }
.map<DealScreenViewState> { DealScreenViewState.SubscriptionStatus(it.state) }
.startWith(Observable.just<DealScreenViewState>(DealScreenViewState.Idle()))
}
}
.onErrorReturn (::parseError)
val onStartTours = intent(DealScreen::tourCardClicked)
.map { restoreModel.deal }
.map{ it.estate }
.flatMap {estate -> toursInteractor.getEstateTourList(estate)
.map { DealScreenViewState.ToursLoaded(it) }
.map<DealScreenViewState> { DealScreenViewState.ToursLoaded(it) }
.onErrorReturn (::parseError)
}
......
......@@ -118,6 +118,7 @@ class DealsScreenController :
is DealsScreenViewState.DealsLoaded -> render(viewState)
is DealsScreenViewState.SomeError -> render(viewState)
is DealsScreenViewState.ToursLoaded -> render(viewState)
is DealsScreenViewState.RestoreView -> render(viewState)
}
}
......@@ -125,6 +126,11 @@ class DealsScreenController :
}
private fun render(viewState: DealsScreenViewState.RestoreView){
(dealsRecyclerView.adapter as DealsListAdapter).addItems(viewState.restore.list)
}
private fun render(viewState: DealsScreenViewState.DealsLoaded){
(dealsRecyclerView.adapter as DealsListAdapter).addItems(viewState.items)
}
......
......@@ -25,6 +25,14 @@ class DealsScreenPresenter @Inject constructor(
override fun defaultErrorViewStateHandler() =
{ e: ExceptionString -> DealsScreenViewState.SomeError(e) }
private val restoreModel = RestoreModel(mutableListOf())
override fun detachView() {
super.detachView()
restoreStateObservable.accept(DealsScreenViewState.RestoreView(restoreModel))
}
override fun bindIntents() {
val getStatusList = interactor.getStatusList()
......@@ -40,6 +48,23 @@ class DealsScreenPresenter @Inject constructor(
}
}
}
.doOnNext { restoreModel.list = it.toMutableList() }
.map<DealsScreenViewState>(DealsScreenViewState::DealsLoaded)
.onErrorReturn(::parseError)
val getDeals = interactor.getDealsApi()
.flatMap { deals ->
getStatusList
.map{
List(deals.size) { index ->
DealPreviewModel(
Pair(deals[index], it)
)
}
}
}
.doOnNext { restoreModel.list = it.toMutableList() }
.map<DealsScreenViewState>(DealsScreenViewState::DealsLoaded)
.onErrorReturn(::parseError)
......@@ -54,6 +79,7 @@ class DealsScreenPresenter @Inject constructor(
arrayListOf(
restoreStateObservable,
fetchDeals,
getDeals,
onStartTours
)
)
......@@ -65,3 +91,9 @@ class DealsScreenPresenter @Inject constructor(
subscribeViewState(state.cast(DealsScreenViewState::class.java), DealsScreen::render)
}
}
data class RestoreModel(
var list:MutableList<DealPreviewModel>
)
\ No newline at end of file
......@@ -15,5 +15,6 @@ sealed class DealsScreenViewState : BigantoBaseViewState() {
class DealsLoaded(val items:List<DealPreviewModel>) : DealsScreenViewState()
class SomeError(val exception: ExceptionString) : DealsScreenViewState()
class ToursLoaded(val tours:List<TourModel>) : DealsScreenViewState()
class RestoreView(val restore:RestoreModel) : DealsScreenViewState()
}
......@@ -215,7 +215,8 @@ class EstateScreenController :
if (it.scrollY > flatTitle.measuredHeight) {
val status = estateModel?.to(
StatusToolbarModel(
StatusState.AVAILABLE
if (estateModel?.availableStatus == true) StatusState.AVAILABLE
else StatusState.SOLD_OUT
, null
, resources?.getString(
estateModel?.type?.typeShortString() ?: -1
......@@ -229,7 +230,8 @@ class EstateScreenController :
} else toolBar.setToolbar(
null,
StatusToolbarModel(
StatusState.AVAILABLE, null, null
if (estateModel?.availableStatus == true) StatusState.AVAILABLE
else StatusState.SOLD_OUT, null, null
)
)
}
......@@ -237,7 +239,8 @@ class EstateScreenController :
}
private fun bindRecycler() {
flatInfoRecyclerView.isNestedScrollingEnabled = true
flatScroll.isNestedScrollingEnabled = false
flatInfoRecyclerView.isNestedScrollingEnabled = false
flatInfoRecyclerView.layoutManager =
LinearLayoutManager(activity, RecyclerView.VERTICAL, false)
flatInfoRecyclerView.adapter = FlatInfoAdapter()
......@@ -378,8 +381,15 @@ class EstateScreenController :
}
private fun setEstateInfo(estateModel: EstateModel?){
this.estateModel = estateModel
toolBar.setToolbar(
null, StatusToolbarModel(StatusState.AVAILABLE,null, null)
null,
StatusToolbarModel(
if (estateModel?.availableStatus == true) StatusState.AVAILABLE
else StatusState.SOLD_OUT
, null
, null
)
)
estateModel?.let {estate ->
flatTitle.text = resources?.getString(estate.type.typeShortString()
......@@ -402,6 +412,7 @@ class EstateScreenController :
.into(tourScreen)
}
when(estateModel?.type){
FlatType.FLAT -> {
flatTypeView.setGone(false)
......
......@@ -37,6 +37,7 @@ class EstateScreenPresenter @Inject constructor(
private fun getPlan(plan: PlanPresetModel): Observable<EstateScreenViewState> =
interactor.getPlan(plan)
.map<EstateScreenViewState> { EstateScreenViewState.LoadPlan(it) }
.onErrorReturn (::parseError)
private var restoreModel = RestoreModel(null,null)
......@@ -50,11 +51,13 @@ class EstateScreenPresenter @Inject constructor(
val prefetchCards = interactor.getEstate(estateId)
.doOnNext { restoreModel.estate = it.copy() }
.map { EstateScreenViewState.LoadEstate(it) }
.map<EstateScreenViewState> { EstateScreenViewState.LoadEstate(it) }
.onErrorReturn (::parseError)
val fetchPlans = interactor.getPlanTypes(estateId)
.doOnNext {restoreModel.planList = it.toList() }
.map { EstateScreenViewState.LoadPlanTypes(it) }
.map<EstateScreenViewState> { EstateScreenViewState.LoadPlanTypes(it) }
.onErrorReturn (::parseError)
val fetchPlan = intent(EstateScreen::planTypesTabSelected)
.map { restoreModel.planList?.get(it) }
......@@ -77,6 +80,7 @@ class EstateScreenPresenter @Inject constructor(
)
)
)
.onErrorReturn (::parseError)
}
val switchSizes = intent(EstateScreen::switchSizes)
......
......@@ -66,6 +66,7 @@ class FavoritesScreenController :
}
private fun setToolbar(){
R.string.area_living
favoritesRecyclerView.isNestedScrollingEnabled = false
toolBar.setToolbar(
......@@ -76,7 +77,7 @@ class FavoritesScreenController :
}
private fun bindRecycler() {
favoritesRecyclerView.isNestedScrollingEnabled = true
favoritesRecyclerView.isNestedScrollingEnabled = false
favoritesRecyclerView.layoutManager =
LinearLayoutManager(activity, RecyclerView.VERTICAL, false)
favoritesRecyclerView.adapter = FavoritesListAdapter()
......@@ -104,6 +105,7 @@ class FavoritesScreenController :
is FavoritesScreenViewState.FavoriteEstatesLoaded -> render(viewState)
is FavoritesScreenViewState.SomeError -> render(viewState)
is FavoritesScreenViewState.ToursLoaded -> render(viewState)
is FavoritesScreenViewState.RestoreView -> render(viewState)
}
}
......@@ -111,6 +113,11 @@ class FavoritesScreenController :
}
private fun render(viewState: FavoritesScreenViewState.RestoreView){
(favoritesRecyclerView.adapter as FavoritesListAdapter).addItems(viewState.restore.list)
}
private fun render(viewState: FavoritesScreenViewState.ToursLoaded) {
router.pushController(RouterTransaction.with(
ChooseTourDialogController(ArrayList(viewState.tours))
......
......@@ -3,6 +3,7 @@ package com.biganto.visual.roompark.presentation.screen.favorites
import com.biganto.visual.roompark.conductor.BigantoBasePresenter
import com.biganto.visual.roompark.domain.interactor.FavoritesInteractor
import com.biganto.visual.roompark.domain.interactor.ToursInteractor
import com.biganto.visual.roompark.domain.model.EstateModel
import com.biganto.visual.roompark.util.monades.ExceptionString
import io.reactivex.Observable
import io.reactivex.android.schedulers.AndroidSchedulers
......@@ -24,15 +25,30 @@ class FavoritesScreenPresenter @Inject constructor(
override fun defaultErrorViewStateHandler() =
{ e: ExceptionString -> FavoritesScreenViewState.SomeError(e) }
private val restoreModel = RestoreModel(mutableListOf())
override fun detachView() {
super.detachView()
restoreStateObservable.accept(FavoritesScreenViewState.RestoreView(restoreModel))
}
override fun bindIntents() {
val prefetchCards = interactor.getFavoritesForCurrentUser()
.map { FavoritesScreenViewState.FavoriteEstatesLoaded(it) }
val prefetchCards = interactor.cachedFavorites()
.doOnNext { restoreModel.list = it.toMutableList() }
.map<FavoritesScreenViewState> { FavoritesScreenViewState.FavoriteEstatesLoaded(it) }
.onErrorReturn (::parseError)
val invalidateData = interactor.getFavoritesForCurrentUser()
.doOnNext { restoreModel.list = it.toMutableList() }
.map<FavoritesScreenViewState> { FavoritesScreenViewState.FavoriteEstatesLoaded(it) }
.onErrorReturn (::parseError)
val onStartTours = intent(FavoritesScreen::tourCardClicked)
.flatMap {estate -> toursInteractor.getEstateTourList(estate)
.map { FavoritesScreenViewState.ToursLoaded(it) }
.map<FavoritesScreenViewState> { FavoritesScreenViewState.ToursLoaded(it) }
.onErrorReturn (::parseError)
}
......@@ -40,6 +56,7 @@ class FavoritesScreenPresenter @Inject constructor(
arrayListOf(
restoreStateObservable,
prefetchCards,
invalidateData,
onStartTours
)
)
......@@ -55,3 +72,7 @@ class FavoritesScreenPresenter @Inject constructor(
}
}
data class RestoreModel(
var list:MutableList<EstateModel>
)
\ No newline at end of file
......@@ -15,4 +15,5 @@ sealed class FavoritesScreenViewState : BigantoBaseViewState() {
class FavoriteEstatesLoaded(val items: List<EstateModel>) : FavoritesScreenViewState()
class SomeError(val exception: ExceptionString) : FavoritesScreenViewState()
class ToursLoaded(val tours:List<TourModel>) : FavoritesScreenViewState()
class RestoreView(val restore:RestoreModel) : FavoritesScreenViewState()
}
\ No newline at end of file
......@@ -14,6 +14,7 @@ import com.biganto.visual.roompark.presentation.screen.settings.util.CommonRecyc
import com.biganto.visual.roompark.presentation.screen.settings.util.CommonViewHolder
import com.biganto.visual.roompark.util.extensions.setGone
import com.biganto.visual.roompark.util.extensions.startUrl
import com.google.android.material.textview.MaterialTextView
import com.jakewharton.rxbinding3.view.clicks
import io.reactivex.Observable
import io.reactivex.subjects.PublishSubject
......@@ -39,6 +40,9 @@ class FavoritesListAdapter : CommonRecyclerAdapter<FavoriteViewHolder,EstateMode
class FavoriteViewHolder(itemView: View) : CommonViewHolder<EstateModel>(itemView) {
@BindView(R.id.avaliable_text) lateinit var availableText: MaterialTextView
@BindView(R.id.avaliable_status) lateinit var availableStatus:View
@BindView(R.id.object_card_title) lateinit var estateTitle: TextView
@BindView(R.id.common_info_block) lateinit var commonInfo:View
@BindView(R.id.start_tour_button) lateinit var startTour:View
......@@ -62,7 +66,8 @@ class FavoriteViewHolder(itemView: View) : CommonViewHolder<EstateModel>(itemVie
ButterKnife.bind(this, itemView)
}
val onStartTourObs: Observable<EstateModel> get() = startTour.clicks().map { bindedModel }
val onStartTourObs: Observable<EstateModel> get() =
startTour.clicks().filter { bindedModel.availableStatus }.map { bindedModel }
override fun onViewBound(model: EstateModel) {
estateTitle.text =
......@@ -74,6 +79,13 @@ class FavoriteViewHolder(itemView: View) : CommonViewHolder<EstateModel>(itemVie
startTour.setGone(model.multitourId == null)
availableStatus.isEnabled = model.availableStatus
availableText.text = itemView.resources.getString(
if (model.availableStatus) R.string.estate_avalibale
else R.string.estate_sold_out
)
siteLink.setGone(model.url == null)
siteLinkDivider.setGone(model.url == null)
model.url?.let {url ->
......@@ -97,11 +109,11 @@ class FavoriteViewHolder(itemView: View) : CommonViewHolder<EstateModel>(itemVie
if (info.area == null) info1.visibility = View.GONE
else { info4.title().text = "Общая, м²"; info4.text().text = info.area.toString()}
if (info.price_meter == null) info1.visibility = View.GONE
else { info5.title().text = "Цена за м²"; info5.text().text = info.price_meter.toRubles()}
if (info.price_meter == null && !bindedModel.availableStatus) info1.visibility = View.GONE
else { info5.title().text = "Цена за м²"; info5.text().text = info.price_meter?.toRubles()}
if (info.price == null) info1.visibility = View.GONE
else { info6.title().text = "Стоимость"; info6.text().text = info.price.toRubles()}
if (info.price == null && !bindedModel.availableStatus) info1.visibility = View.GONE
else { info6.title().text = "Стоимость"; info6.text().text = info.price?.toRubles()}
if (true) info7.visibility = View.GONE
else { info7.title().text = "вщщ"; info7.text().text = info.building.toString()}
......
......@@ -4,7 +4,6 @@ import com.biganto.visual.roompark.base.BaseRoomParkActivity
import com.biganto.visual.roompark.conductor.BigantoBasePresenter
import com.biganto.visual.roompark.domain.interactor.SettingsInteractor
import com.biganto.visual.roompark.domain.model.CachedDataModel
import com.biganto.visual.roompark.domain.model.SubscriptionModel
import com.biganto.visual.roompark.domain.model.TitledSubscriptionModel
import com.biganto.visual.roompark.util.monades.ExceptionString
import com.jakewharton.rxrelay2.PublishRelay
......@@ -67,25 +66,9 @@ class SettingsScreenPresenter @Inject constructor(
val onSubChecked = intent(SettingsScreen::onSubscription)
.flatMap { sub ->
interactor.switchSubscription(sub, !sub.state)
.map<SettingsScreenViewState> {
restoreModel.subs=it.toMutableList()
SettingsScreenViewState.SubscriptionStatus(
sub.id, !sub.state
)
}
.doOnNext {
val ind = restoreModel
.subs
.indexOfFirst { it.subModel.id == sub.id }
restoreModel.subs[ind] =
TitledSubscriptionModel(
title = restoreModel.subs[ind].title,
subModel = SubscriptionModel(
topic = sub.topic,
id = restoreModel.subs[ind].subModel.id,
state = !sub.state
)
)
.map<SettingsScreenViewState> {list ->
restoreModel.subs = list.sortedBy { it.subModel.id }.toMutableList()
SettingsScreenViewState.LoadSubscriptions(restoreModel.subs)
}
.doOnError { Timber.e(it) }
.onErrorReturn { SettingsScreenViewState.SubscriptionError(sub.id, sub.state) }
......@@ -102,8 +85,10 @@ class SettingsScreenPresenter @Inject constructor(
.doOnNext {cacheSizeRefresher.accept(1) }
val fetchSubscriptions = interactor.getSubscriptions()
.doOnNext { restoreModel.subs = it.toMutableList() }
.map { SettingsScreenViewState.LoadSubscriptions(it) }
.map {list ->
restoreModel.subs = list.sortedBy { it.subModel.id }.toMutableList()
SettingsScreenViewState.LoadSubscriptions(restoreModel.subs)
}
val onSignOut = intent(SettingsScreen::signOut)
.flatMap {
......
......@@ -18,17 +18,16 @@
android:padding="16dp">
<FrameLayout
android:id="@+id/feed_read"
android:id="@+id/avaliable_status"
android:layout_width="12dp"
android:layout_height="12dp"
android:background="@drawable/new_feed_icon"
android:backgroundTint="@color/colorAccent"
android:background="@drawable/available_status"
android:visibility="visible"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/feed_date_text_view3"
<com.google.android.material.textview.MaterialTextView
android:id="@+id/avaliable_text"
style="@style/LiteText.Accent"
android:layout_width="0dp"
android:layout_height="wrap_content"
......@@ -38,7 +37,7 @@
android:visibility="visible"
app:layout_constraintBottom_toBottomOf="@+id/feed_read"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/feed_read"
app:layout_constraintStart_toEndOf="@+id/avaliable_status"
app:layout_constraintTop_toTopOf="@+id/feed_read" />
<com.google.android.material.textview.MaterialTextView
......@@ -50,7 +49,7 @@
android:text="\n"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/feed_date_text_view3" />
app:layout_constraintTop_toBottomOf="@+id/avaliable_text" />
<include
android:id="@+id/header_divider"
......
......@@ -16,7 +16,6 @@
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:background="@drawable/available_status"
android:fitsSystemWindows="true"
android:visibility="visible"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
......@@ -27,9 +26,8 @@
style="@style/Accent_Minor_TextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="4dp"
android:layout_marginStart="8dp"
android:fitsSystemWindows="true"
android:text="СВОБОДНА"
app:layout_constraintBottom_toBottomOf="@+id/status_icon"
app:layout_constraintStart_toEndOf="@+id/status_icon"
app:layout_constraintTop_toTopOf="@+id/status_icon" />
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment