Commit 140839e0 authored by Vladislav Bogdashkin's avatar Vladislav Bogdashkin 🎣

Merge branch 'feature/notification_service' into develop

parents d044c9c2 6d47619a
......@@ -60,6 +60,10 @@ android {
targetCompatibility 1.8
sourceCompatibility 1.8
}
lintOptions {
disable 'BinaryOperationInTimber'
}
}
kapt {
......
......@@ -2,8 +2,13 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.biganto.visual.roompark">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<application
android:name=".base.RoomParkApplication"
......@@ -58,6 +63,11 @@
android:resource="@color/colorAccent" />
</service>
<service
android:name=".data.service.download.DownloadManagerService"
android:enabled="true"
android:exported="false" />
</application>
</manifest>
\ No newline at end of file
......@@ -125,8 +125,7 @@ class DownloadTourDialogController : Controller {
snackbar.showSnackBar(error.localizedMessage)
}
))
cancelDownloadText.setOnClickListener {
downloadToken.isCancelled = true;handleBack() }
cancelDownloadText.setOnClickListener {handleBack() }
// downloadTour(it.tour.tour_id, downloadToken)
// view.findViewById<View>(R.id.close_current_button).setOnClickListener { handleBack() }
......@@ -147,7 +146,8 @@ class DownloadTourDialogController : Controller {
fun getLayoutId() = R.layout.download_tour_layout
override fun handleBack(): Boolean {
return router.popCurrentController()
downloadToken.isCancelled = true
return router.popController(this)
}
......
......@@ -80,7 +80,7 @@ class ChooseTourDialogController : Controller {
.subscribe(::onTourClicked)
)
//view.findViewById<View>(R.id.close_current_button).setOnClickListener { handleBack() }
view.findViewById<View>(R.id.close_current_button).setOnClickListener { handleBack() }
view.setOnClickListener { handleBack() }
return view
......
......@@ -60,7 +60,7 @@ class BigantoMviConductorLifecycleListener<V : MvpView, P : MviPresenter<V, *>>
}
val viewMpv = callback?.mvpView ?: throw NullPointerException(
"MvpView returned from getMvpView() is null. Returned by " + controller.activity!!)
"MvpView returned from getMvpView() is null. Returned by ${controller.activity}")
if (viewStateWillBeRestored) {
callback!!.setRestoringViewState(true)
......
......@@ -61,6 +61,7 @@ class EstateRepository @Inject constructor(
}
}
.doOnNext(db::blockingUpsert)
.doOnNext{ db.refreshUser(user) }
}
private val getFavoritesDb: Observable<List<EstateEntity>> =
......
......@@ -60,12 +60,18 @@ class BigantoRetrofitRepository @Inject constructor(@Named("bigantoApi") retrof
.doOnError(::e)
override fun getOfferTours(multiTourId:Int): Observable<List<TourPreviewRaw>> =
override fun getOfferTours(multiTourIds:List<Int>): Observable<List<TourPreviewRaw>> =
api
.getOfferTours(offerId = multiTourId)
.getOfferTours(offerId = multiTourIds)
.compose(RetrofitResponseValidation())
.map { it.values.flatten() }
.doOnError { e(it) }
.subscribeOn(Schedulers.io())
override fun getOfferTours(multiTourId:Int): Observable<List<TourPreviewRaw>> =
api
.getOfferTours(offerId = arrayListOf(multiTourId))
.compose(RetrofitResponseValidation())
.map { it[multiTourId.toString()]?.toList()?: error("No tours avaliable")}
.doOnError { e(it) }
.subscribeOn(Schedulers.io())
......
......@@ -19,10 +19,11 @@ interface IBigantoApi {
fun downloadFile(uri: String, headers: Map<String, String>?): Flowable<ResponseBody>
// fun getToursFiles(tour_ids: List<Int>, resolution: String): Flowable<Map<String, List<TourFileRaw>>>?
fun getTourMetaAsString(tour_id: String): Observable<String>?
fun getTourMetaAsString(tour_id: String): Observable<String>
fun getTourFiles(tour_id: String, resolution: String): Observable<List<TourFilesDataRaw>>
fun getAppVersion(): Observable<AppVersionRaw>
fun getToursPreviewById(tourIds: List<String>): Observable<List<TourPreviewRaw>>
fun getOfferTours(multiTourId:Int): Observable<List<TourPreviewRaw>>
fun getOfferTours(multiTourIds: List<Int>): Observable<List<TourPreviewRaw>>
}
\ No newline at end of file
......@@ -145,7 +145,7 @@ interface IBigantoMobileApi {
@Query(CLIENT_VERSION_PARAM) clientVersion: String = DEFAULT_CLIENT_VERSION,
@Query(API_VERSION_PARAM) apiVersion: String = DEFAULT_API_VERSION,
@Query(LANG_PARAM) languageCode: String = Locale.getDefault().language,
@Query(OFFER_GET_TOURS_ID) offerId: Int
@Query(OFFER_GET_TOURS_ID) offerId: List<Int>
): Observable<Response<Map<String,List<TourPreviewRaw>>>>
@Streaming
......
......@@ -72,4 +72,5 @@ interface IDb {
fun dropFileTable(): Completable
fun dropTourFileJuncTable(): Completable
fun dropTourTable(): Completable
fun refreshUser(userEntity: UserEntity): Observable<UserEntity>
}
\ No newline at end of file
......@@ -4,7 +4,6 @@ import android.app.Application
import com.biganto.visual.roompark.Models
import com.biganto.visual.roompark.data.repository.db.IDb
import com.biganto.visual.roompark.data.repository.db.requrey.model.*
import com.biganto.visual.roompark.di.dagger.DATABASE_VERSION
import com.biganto.visual.roomparkvr.data.repository.db.requery.model.DownloadState
import com.biganto.visual.roomparkvr.data.repository.db.requery.model.TourPreview
import com.biganto.visual.roomparkvr.data.repository.db.requery.model.TourPreviewEntity
......@@ -30,6 +29,8 @@ import javax.inject.Inject
*/
private const val DATABASE_VERSION = 14
@Module
class DbModule{
......@@ -52,6 +53,9 @@ class RequeryRepository @Inject constructor(
)
: IDb {
override fun refreshUser(userEntity: UserEntity): Observable<UserEntity> =
store.refresh(userEntity).toObservable()
override fun dropTourTable() = store.delete(TourPreviewEntity::class).get().toCompletable()
override fun dropTourFileJuncTable() = store.delete(TourFileJunctionEntity::class).get().toCompletable()
......
......@@ -78,6 +78,6 @@ interface Estate : Persistable {
@get:Nullable
@get:Column(name = "UserContainer")
@get:ForeignKey(references = User::class )
@get:OneToOne(mappedBy = "uuid",cascade = [CascadeAction.NONE])
@get:ManyToOne(cascade = [CascadeAction.NONE])
var user:User?
}
\ No newline at end of file
......@@ -25,6 +25,10 @@ interface User : Persistable {
@get:OneToMany(cascade = [CascadeAction.DELETE])
val deals:List<Deal>?
@get:Nullable
@get:OneToMany(cascade = [CascadeAction.DELETE])
val estates:List<Estate>?
@get:Nullable
@get:OneToMany(cascade = [CascadeAction.DELETE])
val subscriptions:List<Subscription>?
......
......@@ -68,6 +68,12 @@ class FileModule @Inject constructor(val context: Application) {
// fun deleteFile(uri:String)= getFile(uri).delete()
fun deleteAssetFile(uri:String) =
getAssetFile(uri).delete()
fun getAssetFile(uri:String) =
File(assetsDirectory(context).plus(uri))
fun deleteAllCacheObservable() =
Observable.create<Pair<Int, Int>> {emitter ->
val foldersToDelete = listOf(
......
package com.biganto.visual.roompark.data.service.download
import com.biganto.visual.roompark.data.repository.db.requrey.RevisionString
import com.biganto.visual.roompark.data.repository.db.requrey.model.FileEntity
import com.biganto.visual.roompark.data.repository.db.requrey.model.TourFileJunctionEntity
import com.biganto.visual.roomparkvr.data.repository.db.requery.model.DownloadState
/**
* Created by Vladislav Bogdashkin on 14.04.2020.
*/
data class TourFileData(
val fileUrl: RevisionString,
val tourId: String,
var tempDownloadedSize: Long = 0L,
var tempOverallFileSize: Long = 0L,
var fileDownloadedSize: Long = 0L,
var tempTourTotalDiff: Long = 0L,
var isDownloaded: Boolean = false,
val fatalState: DownloadState? = null
) {
constructor(entity: FileEntity, junction: TourFileJunctionEntity) : this(
fileUrl = junction.file
, tourId = junction.tour
, tempDownloadedSize = 0L
, tempOverallFileSize = entity.totalSize
, fileDownloadedSize = entity.downloadedSize
, tempTourTotalDiff = 0L
, isDownloaded = entity.isDownloaded
)
}
package com.biganto.visual.roompark.data.service.download
import android.app.Service
import android.content.Context
import android.content.Intent
import android.media.MediaScannerConnection
import android.os.IBinder
import androidx.core.math.MathUtils.clamp
import com.biganto.visual.roompark.R
import com.biganto.visual.roompark.base.RoomParkApplication
import com.biganto.visual.roompark.data.repository.api.biganto.IBigantoApi
import com.biganto.visual.roompark.data.repository.db.IDb
import com.biganto.visual.roompark.data.repository.db.requrey.RevisionString
import com.biganto.visual.roompark.data.repository.db.requrey.model.FileEntity
import com.biganto.visual.roompark.data.repository.db.requrey.model.TourFileJunctionEntity
import com.biganto.visual.roompark.data.repository.db.requrey.model.fromRaw
import com.biganto.visual.roompark.data.repository.file.FileModule
import com.biganto.visual.roompark.data.service.lifecycle.ForegroundLifecycleObserver
import com.biganto.visual.roompark.data.service.notification.INotificationCenter
import com.biganto.visual.roompark.domain.use_case.TOUR_IDS_TO_DOWNLOAD_KEY
import com.biganto.visual.roomparkvr.data.repository.db.requery.model.DownloadState
import com.biganto.visual.roomparkvr.data.repository.db.requery.model.TourPreviewEntity
import com.jakewharton.rxrelay2.PublishRelay
import io.reactivex.BackpressureStrategy
import io.reactivex.Flowable
import io.reactivex.Observable
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.schedulers.Schedulers
import okhttp3.ResponseBody
import okio.Okio
import timber.log.Timber
import java.io.File
import java.util.*
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean
import javax.inject.Inject
import javax.inject.Singleton
/**
* Created by Vladislav Bogdashkin on 13.04.2020.
*/
private const val DB_ACCESS_CHUNK_SIZE = 256
private const val READ_SYNC_MILLS = 120L
private const val DEQUE_REQUEST_TIMEOUT_MILLS=100L
private const val META_PREDICTION="/tourMeta_"
private const val META_FILE_TYPE=".json"
const val DOWNLOAD_MANAGER_COMMAND_KEY = "TOURS_DOWNLOAD_MANAGER_COMMAND"
const val DOWNLOAD_MANAGER_ADD_IDS_TO_LOAD_COMMAND = "ADD_TOUR_IDS_TO_QUEUE"
@Singleton
class DownloadManagerService @Inject constructor(
): Service() {
//region define dependencies
@Inject
lateinit var db: IDb
@Inject
lateinit var api: IBigantoApi
@Inject
lateinit var fileModule: FileModule
@Inject
lateinit var context: Context
@Inject
lateinit var notificationsCenter: INotificationCenter
@Inject
lateinit var appLifeCycle: ForegroundLifecycleObserver
//endregion
//region downloading flow
private val disposable = CompositeDisposable()
private val downloadQueue = ArrayDeque<String>()
private val deletingQueue = ArrayDeque<String>()
private var activeDownloading: AtomicBoolean = AtomicBoolean(false)
private fun downloadTourFiles(data: TourPreviewEntity) {
if (!downloadQueue.contains(data.id))
downloadQueue.add(data.id)
}
private fun deleteTourFiles(data: TourPreviewEntity) {
if (!deletingQueue.contains(data.id))
deletingQueue.add(data.id)
}
//endregion
init {
db = RoomParkApplication.component.providedb()
api = RoomParkApplication.component.provideBigantoApi()
context = RoomParkApplication.component.provideAppContext()
fileModule = RoomParkApplication.component.provideFileSystem()
appLifeCycle = RoomParkApplication.component.provideLifeCycle()
notificationsCenter = RoomParkApplication.component.provideNotifivations()
}
override fun onCreate() {
super.onCreate()
Timber.d("START SERVICE AND SEND NOTIFICATION")
startForeground(notificationsCenter.foregroundDownloadServiceChannelId
, notificationsCenter.foregroundDownloadServiceNotification)
}
override fun onBind(intent: Intent?): IBinder? {
return null
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
// Timber.d(" GOT INTENT $intent with action ${intent?.action}")
// if (intent?.action == NOTIFICATION_INTENT_STOP_SERVICE_ACTION)
// {
// stopForeground(false)
// stopSelf()
// }
val command = intent?.getStringExtra(DOWNLOAD_MANAGER_COMMAND_KEY)
if (command == DOWNLOAD_MANAGER_ADD_IDS_TO_LOAD_COMMAND){
intent.getStringArrayListExtra(TOUR_IDS_TO_DOWNLOAD_KEY)
?.forEach { toursToDownloadObserver.accept(it) }
}
Timber.d(" ON START COMMAND,${disposable.size()}")
if (disposable.size() == 0)
attachDownloader()
return START_NOT_STICKY
}
override fun onDestroy() {
disposable.clear()
downloadQueue.forEach { id -> setTourStatus(id, DownloadState.NotDownloaded) }
downloadQueue.clear()
deletingQueue.forEach { id ->
val tour = db.getTourPreview(id).observable().blockingSubscribe { tour ->
setTourStatus(id,
if (tour.downloadedSize == tour.overallSize)
DownloadState.Downloaded
else DownloadState.Suspended
)
}
}
deletingQueue.clear()
super.onDestroy()
}
private fun setTourStatus(id:String,state:DownloadState) =
db.getTourPreview(id)
.observable()
.map { it.apply { isDownloaded = state } }
.flatMap { db.upsert(it).toObservable() }
.blockingSubscribe()
private fun writeFile(response: ResponseBody, model: TourFileData): Observable<TourFileData> {
return Observable.create<TourFileData> { sub ->
try {
if (model.fatalState == DownloadState.Crushed) {
sub.onNext(model)
sub.onComplete()
}
val fileStorage = fileModule.getFile(model.fileUrl.uri())
val sink = Okio.buffer(Okio.appendingSink(fileStorage))
val buffer = sink.buffer()
var read = 0L
val step = 4096
val source = response.source()
var timer = System.currentTimeMillis()
var stop: Boolean = false
sink.use {
while (!stop && { read = source.read(buffer, step.toLong());read }() != -1L) {
model.tempDownloadedSize += read
if ((System.currentTimeMillis() - timer) > READ_SYNC_MILLS || source.exhausted()) {
timer = System.currentTimeMillis()
model.fileDownloadedSize += model.tempDownloadedSize
if (model.tempOverallFileSize == 0L)
model.tempTourTotalDiff += model.tempDownloadedSize
model.isDownloaded = (source.exhausted()
&& (model.fileDownloadedSize == model.tempOverallFileSize
|| model.tempOverallFileSize == 0L))
model.tempTourTotalDiff = 0
model.tempDownloadedSize = 0
}
}
}
model.isDownloaded = (source.exhausted()
&& (model.fileDownloadedSize == model.tempOverallFileSize
|| model.tempOverallFileSize == 0L))
sub.onNext(model.copy())
model.tempTourTotalDiff = 0
model.tempDownloadedSize = 0
sub.onComplete()
sink.close()
// refreshGallery(file) //-> обновляем представление файлов в файловой системе android (чтобы их можно было просматривать через explorer и видеть размер, корректно открывать и т.п.)
} catch (e: Exception) {
Timber.e(e)
if (!sub.isDisposed)
sub.onError(e)
setTourStatus(model.tourId,DownloadState.Crushed)
}
}
}
private fun mergeFiles(files: List<FileEntity>) {
Timber.d("Merge files")
files.forEach { file ->
val entity: FileEntity? = db.getFileEntity(file.uri).firstOrNull()
entity?.let {
file.setDownloaded(it.isDownloaded)
file.setDownloadedSize(it.downloadedSize)
file.setTotalSize(it.totalSize)
}
}
Timber.d("save files: ${files.size}")
files.chunked(DB_ACCESS_CHUNK_SIZE).forEach {chunk ->
db.upsertFileEntity(chunk).blockingSubscribe { Timber.d("file saved ${chunk.size}") }
}
}
private fun setDownloadInfo(
id: String,
downloadedSize: Long? = null
, downloadedDiffSize: Long? = null
, totalSize: Long? = null
, resolution: Int? = null
, filesCount: Int? = null
, tempLoadedFiles: Int? = null
, totalSizedDiffSize: Long? = null
) =
db.getTourPreview(id).observable().map {entity ->
entity.also {
downloadedSize?.let { entity.downloadedSize = it }
downloadedDiffSize?.let { entity.downloadedSize += it }
totalSize?.let { entity.overallSize = it }
resolution?.let { entity.targetResolution = it }
filesCount?.let { entity.overallFiles = it }
tempLoadedFiles?.let { entity.downloadedFiles += it }
totalSizedDiffSize?.let { entity.overallSize += it }
if (entity.downloadedFiles == entity.overallFiles)
entity.isDownloaded = DownloadState.Downloaded
}
}
.flatMap { db.upsert(it).toObservable() }
private fun flowableFilesDownloading(tour: TourPreviewEntity) =
api.getTourFiles(tour.id, tour.targetResolution.toString())
.map { it.first() }
.flatMap { raw ->
var downloadedSize = 0L
var totalSize = 0L
val fileEntities = raw.files.map(::fromRaw)
mergeFiles(fileEntities)
val jlist = db.getTourFilesJunction(tour.id).toList()
val junctionList = fileEntities
.map {file ->
val entity = jlist.firstOrNull{it.tour == tour.id && it.file == file.uri}
?: TourFileJunctionEntity().apply {
setTour(tour.id)
setFile(file.uri)
}
downloadedSize += file.downloadedSize
totalSize += file.totalSize
entity
}
setDownloadInfo(
raw.id.toString()
, tempLoadedFiles = 0
, downloadedSize = downloadedSize
, totalSize = totalSize
, resolution = raw.resolution
, filesCount = raw.files.count()
).map { junctionList }
}
.flatMap{ db.upsertTourFileJunction(it) }
.flatMapIterable { it }
.flatMap { junction ->
db.getFileEntity(junction.file)
.observable()
.map { entity -> TourFileData(entity,junction) }
}
.toFlowable(BackpressureStrategy.BUFFER)
// .map(::validateFile)
.parallel(clamp(Runtime.getRuntime().availableProcessors() - 2, 2, 4))
.runOn(Schedulers.io())
.flatMap { model ->
if (model.isDownloaded) return@flatMap Flowable.just(model)
var header: HashMap<String, String>? = null
if (model.fileDownloadedSize > 0)
header = hashMapOf(Pair("Range", "bytes=${model.fileDownloadedSize}-"))
api.downloadFile(model.fileUrl.revisionUri(), header)
.doOnError {
Timber.e(it)
setTourStatus(model.tourId, DownloadState.Crushed)
}
.flatMap<TourFileData> {
writeFile(it, model)
.toFlowable(BackpressureStrategy.BUFFER)
.doOnCancel { Timber.d("CANCELLED") }
}
}
.flatMap { downloadInfo ->
db.upsertFileEntity(
FileEntity().apply {
setUri(downloadInfo.fileUrl)
setDownloadedSize(downloadInfo.fileDownloadedSize)
setTotalSize(downloadInfo.tempOverallFileSize)
setDownloaded(downloadInfo.isDownloaded)
}
)
.toFlowable(BackpressureStrategy.BUFFER)
.map { downloadInfo }
}
.sequential()
.toObservable()
.observeOn(Schedulers.computation())
// .doOnNext{Timber.d("7 ${it}")}
.flatMap { model ->
setDownloadInfo(
model.tourId
, totalSizedDiffSize = model.tempTourTotalDiff
, downloadedDiffSize = model.tempDownloadedSize
, tempLoadedFiles = if (model.isDownloaded) 1 else null
)
.map {
model.tempDownloadedSize = 0
model.tourId
}
}
// .delay(12L, TimeUnit.MILLISECONDS)
private val checkService =
Observable.interval(0L, DEQUE_REQUEST_TIMEOUT_MILLS, TimeUnit.MILLISECONDS)
.filter { hasTasks }
.filter { !activeDownloading.get() }
.switchMap<String> {
when {
deletingQueue.isNotEmpty() -> {
notifyDeleting()
deleteTourFromQueue()
}
downloadQueue.isNotEmpty() -> {
notifyDownloading()
downloadTourFromQueue()
}
else -> {
Timber.e("Empty queues!");return@switchMap Observable.empty()
}
}
}
private fun downloadTourFromQueue(): Observable<String> =
Observable.fromCallable { activeDownloading.set(true);downloadQueue.poll() }
.flatMap { db.getTourPreview(it).observable() }
.filter {
val forward = it.isDownloaded != DownloadState.Downloaded
activeDownloading.set(forward)
if (!forward)
notifyDownloadProgress()
forward
}
.flatMap { tour ->
flowableFilesDownloading(tour)
.doFinally {
activeDownloading.set(false)
notifyDownloadProgress()
}
}
.map { it }
.subscribeOn(Schedulers.single())
private fun deleteTourFromQueue() =
Observable.fromCallable { activeDownloading.set(true);deletingQueue.poll() }
.flatMap { db.getTourPreview(it).observable() }
.filter {
val forward = it.isDownloaded == DownloadState.Deleting
activeDownloading.set(forward)
forward
}
.map { it.id }
.doOnNext {
deleteTourSync(it)
}
.onErrorResumeNext(Observable.empty())
.doFinally {
activeDownloading.set(false)
notifyDeleteProgress()
}
.subscribeOn(Schedulers.single())
private fun deleteTourSync(tourId:String) {
try {
db.getTourFilesJunctionUniqueFiles(tourId)
.filterNotNull()
.let { list ->
list.asSequence()
.map{ FileEntity().apply { setUri(it.file) } }
.chunked(DB_ACCESS_CHUNK_SIZE)
.forEach { db.deleteFiles( it) }
list.forEach { j -> if (!fileModule.deleteAssetFile(j.file.uri()))
Timber.w("Not success to delete file! \n" +
"short uri: ${j.file.uri()} \n" +
"full uri: ${fileModule.getAssetFile(j.file.uri())}")
}
}
val resultRowsDelete = db.deleteTourFilesJunction(tourId).value()
Timber.d("Deleted form TourFile Junction rows $resultRowsDelete")
db.deleteTourPreview(tourId)
} catch (err: java.lang.Exception) {
Timber.e(err)
error("can't delete tour")
}
}
private val hasTasks: Boolean
get() = downloadQueue.isNotEmpty() || deletingQueue.isNotEmpty()
//region Notifications
private fun notifyDownloadProgress() {
if (downloadQueue.isEmpty()) {
notificationsCenter
.completeProgressMessage(
resources.getString(R.string.on_all_tours_downloaded_notification_message)
)
if (!hasTasks) {
stopForeground(false)
stopSelf()
}
} else notifyDownloading()
}
private fun notifyDeleteProgress() {
if (deletingQueue.isEmpty()) {
notificationsCenter
.completeProgressMessage(
resources.getString(R.string.on_all_tours_deleted_notification_message)
)
if (!hasTasks) {
stopForeground(false)
stopSelf()
}
} else notifyDeleting()
}
private fun notifyDeleting() = notificationsCenter
.indeterminateProgressMessage(
progress = 1
, progressMax = (deletingQueue.size + 1)
, message =
String.format(
context.resources.getString(R.string.noty_tours_delete_left)
, deletingQueue.size)
)
private fun notifyDownloading() = notificationsCenter
.indeterminateProgressMessage(
progress = 1
, progressMax = (downloadQueue.size + 1)
, message = String.format(
context.resources.getString(R.string.noty_tours_download_left)
, downloadQueue.size)
)
//endregion Notifications
private val toursToDownloadObserver = PublishRelay.create<String>()
public fun addTourToQueue(id:String) = toursToDownloadObserver.accept(id)
private fun attachDownloader() {
disposable.add(
checkService
.subscribe {
if (!appLifeCycle.isAppForeground
&& !hasTasks
&& !activeDownloading.get()) {
stopForeground(true)
stopSelf()
}
}
)
// disposable.add(touresCache.observableToursForDeleting().subscribe(::deleteTourFiles))
disposable.add(
toursToDownloadObserver
.flatMap { db.getTourPreview(it).observable() }
.doOnNext {
it.isDownloaded = DownloadState.Downloading
it.downloadedFiles = 0
it.downloadedSize = 0L
it.tempSize = it.overallSize // <- overall changes due downloading!!
it.overallSize = 0L
}
.observeOn(Schedulers.computation())
.flatMap(::getMeta)
.subscribeOn(Schedulers.io())
.doOnError(Timber::e)
.subscribe { downloadQueue.add(it) }
)
}
private fun getMeta(tour: TourPreviewEntity): Observable<String> =
api.getTourMetaAsString(tour.id)
.doOnNext { meta ->
tour.let {
val metaUri = RevisionString("$META_PREDICTION${tour.id}$META_FILE_TYPE")
it.setMetaFileEntityId(metaUri)
fileModule.saveFileToDisk(fileModule.getFile(metaUri.uri()), meta)
}
}
.map { tour.id }
.onErrorReturn {
setTourStatus(tour.id,DownloadState.Crushed)
tour.id
}
//#endregion oldMethod
private fun refreshGallery(file: File) {
MediaScannerConnection.scanFile(context, arrayOf(file.path), null
)
{ path, uri ->
{}//Timber.d("Scanned $path")
}
}
}
package com.biganto.visual.roompark.data.service.lifecycle
/**
* Created by Vladislav Bogdashkin on 14.04.2020.
*/
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleObserver
import androidx.lifecycle.OnLifecycleEvent
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class AppLifecycleListener @Inject constructor(): ForegroundLifecycleObserver {
private var isForeground=false
override val isAppForeground : Boolean
get() = isForeground
@OnLifecycleEvent(Lifecycle.Event.ON_START)
fun onMoveToForeground() {
Timber.d("Returning to foreground…")
isForeground=true
}
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
fun onMoveToBackground() {
Timber.d("Moving to background…")
isForeground=false
}
}
interface ForegroundLifecycleObserver : LifecycleObserver{
val isAppForeground : Boolean
}
\ No newline at end of file
package com.biganto.visual.roompark.data.service.notification
import android.annotation.TargetApi
import android.app.*
import android.content.Context
import android.content.Intent
import android.graphics.BitmapFactory
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
import com.biganto.visual.roompark.R
import com.biganto.visual.roompark.base.RoomParkMainActivity
import com.biganto.visual.roomparkvr.data.repository.db.requery.model.DownloadState
import javax.inject.Inject
import javax.inject.Singleton
/**
* Created by Vladislav Bogdashkin on 13.04.2020.
*/
const val ANDROID_CHANNEL_ID = "com.biganto.visual.androidplayer.data.services.downloader.DownloadManagerService.CHANNEL_ID"
const val TOURS_CHANNEL_ID = "com.biganto.visual.androidplayer.data.services.downloader.DownloadManagerService.TOURS_CHANNEL_ID"
const val DOWNLOAD_SERVICE_ID = 7897
const val TOUR_INFO_SERVICE_ID = 7899
const val NOTIFICATION_INTENT="NOTIFICATION_INTENT_KEY"
const val NOTIFICATION_START_SCREEN="NOTIFICATION_START_SCREEN_KEY"
const val NOTIFICATION_INTENT_SCREEN_TYPE="NOTIFICATION_SHOW_SCREEN"
const val NOTIFICATION_INTENT_STOP_SERVICE_ACTION="STOP_DOWNLOADS_SERVICE"
const val PENDING_REQUEST_CODE=0
interface INotificationCenter{
val foregroundDownloadServiceChannelId : Int
val foregroundDownloadServiceNotification: Notification
fun indeterminateProgressMessage(progress:Int=0
,progressMax:Int=0
,message: String)
fun completeProgressMessage(message: String)
fun donwloadServiceProgressNotfication(progress:Int=0
,progressMax:Int=0
,indeterminate:Boolean=true
,message: String)
}
@Singleton
class NotificationCenter @Inject constructor(val context: Application) : INotificationCenter{
private val updateProgressNotificationDelay_Milliseconds= 333
private var lastTimeProgressNotificationUpdated = 0L
private val actualNotifyManager:NotificationManager
get()= context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
init {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
createNotificationChannel()
createToursNotificationChannel()
}
}
private val builder = NotificationCompat.Builder(context, ANDROID_CHANNEL_ID)
private val toursNotyBuilder = NotificationCompat.Builder(context, TOURS_CHANNEL_ID)
private val notificationSystemColor = ContextCompat.getColor(context, R.color.colorAccent)
private val icon = BitmapFactory.decodeResource(context.resources, R.mipmap.ic_launcher_round);
//Заготовка под интерфейсы для равзедения каналов нотификаций по разным инстансам
override val foregroundDownloadServiceChannelId = DOWNLOAD_SERVICE_ID
override val foregroundDownloadServiceNotification: Notification =
builder
.setContentTitle(context.getString(R.string.notification_content_title))//getString(R.string.app_name))
.setContentText(context.getString(R.string.notification_content_text))
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setSmallIcon(R.mipmap.ic_launcher)
.setColor(notificationSystemColor)
.build()
//Заготовка под переход в определенный экран приложения по нажатию на нотификацию
private val toDownloadsIntent =
Intent(context, RoomParkMainActivity::class.java)
.putExtra(NOTIFICATION_INTENT,NOTIFICATION_START_SCREEN)
// .putExtra(NOTIFICATION_INTENT_SCREEN_TYPE,R.id.tab_downloads)
.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
// private val stopServiceIntent=
// Intent(context, DownloadManagerService::class.java)
// .setAction(NOTIFICATION_INTENT_STOP_SERVICE_ACTION)
override fun indeterminateProgressMessage(progress:Int
, progressMax:Int
, message: String) {
if (progress!=0)
if (System.currentTimeMillis()-lastTimeProgressNotificationUpdated
<updateProgressNotificationDelay_Milliseconds)
return
lastTimeProgressNotificationUpdated=System.currentTimeMillis()
donwloadServiceProgressNotfication(progress, progressMax, true, message)
}
override fun completeProgressMessage(message: String){
donwloadServiceProgressNotfication(indeterminate = false,message = message)
}
override fun donwloadServiceProgressNotfication(progress:Int
, progressMax:Int
, indeterminate:Boolean
, message: String){
val pendingIntent = PendingIntent.getActivity(context
, PENDING_REQUEST_CODE
, toDownloadsIntent
, PendingIntent.FLAG_ONE_SHOT
)
val notification =
(if (indeterminate) builder else toursNotyBuilder)
// builder
.setOnlyAlertOnce(true)
.setContentTitle(context.getString(R.string.notification_content_title))
.setContentText(message)
.setProgress(progress, progressMax,indeterminate)
.setSmallIcon(R.mipmap.ic_launcher)
.setBadgeIconType(NotificationCompat.BADGE_ICON_LARGE)
.setColor(notificationSystemColor)
.setLargeIcon(icon)
.setContentIntent(pendingIntent)
.setAutoCancel(false)
.build()
actualNotifyManager.notify(
(if (indeterminate) DOWNLOAD_SERVICE_ID else TOUR_INFO_SERVICE_ID)
, notification)
}
@TargetApi(Build.VERSION_CODES.O)
@RequiresApi(Build.VERSION_CODES.O)
private fun createNotificationChannel(): String {
val channelId = ANDROID_CHANNEL_ID
val channelName = "Biganto Visual Download Service"
val chan = NotificationChannel(channelId,
channelName, NotificationManager.IMPORTANCE_DEFAULT)
chan.lockscreenVisibility = Notification.VISIBILITY_PUBLIC
chan.enableVibration(false)
chan.lightColor = R.color.colorPrimary;
val service = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
service.createNotificationChannel(chan)
return channelId
}
@TargetApi(Build.VERSION_CODES.O)
@RequiresApi(Build.VERSION_CODES.O)
private fun createToursNotificationChannel(): String {
val channelId = TOURS_CHANNEL_ID
val channelName = "Biganto Visual Tour info update"
val chan = NotificationChannel(channelId,
channelName, NotificationManager.IMPORTANCE_DEFAULT)
chan.lockscreenVisibility = Notification.VISIBILITY_PUBLIC
chan.enableVibration(true)
chan.lightColor = R.color.colorPrimary;
val service = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
service.createNotificationChannel(chan)
return channelId
}
private fun DownloadState.notyName() =
when(this){
DownloadState.Downloaded ->context.getString(R.string.state_downloaded_notify_message)
DownloadState.Crushed -> context.getString(R.string.state_crushed_notify_message)
DownloadState.Suspended -> context.getString(R.string.state_suspended_notify_message)
DownloadState.Downloading -> context.getString(R.string.state_downloading_notify_message)
else -> context.getString(R.string.state_else_notify_message)
}
}
......@@ -10,6 +10,8 @@ import com.biganto.visual.roompark.data.repository.api.room_park.IRoomParkApi
import com.biganto.visual.roompark.data.repository.db.IDb
import com.biganto.visual.roompark.data.repository.db.requrey.DbModule
import com.biganto.visual.roompark.data.repository.file.FileModule
import com.biganto.visual.roompark.data.service.lifecycle.ForegroundLifecycleObserver
import com.biganto.visual.roompark.data.service.notification.INotificationCenter
import com.biganto.visual.roompark.domain.contract.*
import dagger.BindsInstance
import dagger.Component
......@@ -64,6 +66,11 @@ interface AppComponent : AndroidInjector<RoomParkApplication>{
fun provideTour():TourContract
fun provideLifeCycle(): ForegroundLifecycleObserver
fun provideNotifivations(): INotificationCenter
fun provideAppContext():Application
fun provideFileSystem(): FileModule
......
......@@ -9,7 +9,6 @@ import dagger.Module
* Created by Vladislav Bogdashkin on 13.06.2018.
*/
const val DATABASE_VERSION = 13
@Module
abstract class AppModule{
......
......@@ -10,6 +10,10 @@ import com.biganto.visual.roompark.data.repository.api.room_park.RetrofitReposit
import com.biganto.visual.roompark.data.repository.db.IDb
import com.biganto.visual.roompark.data.repository.db.requrey.DbModule
import com.biganto.visual.roompark.data.repository.db.requrey.RequeryRepository
import com.biganto.visual.roompark.data.service.lifecycle.AppLifecycleListener
import com.biganto.visual.roompark.data.service.lifecycle.ForegroundLifecycleObserver
import com.biganto.visual.roompark.data.service.notification.INotificationCenter
import com.biganto.visual.roompark.data.service.notification.NotificationCenter
import com.biganto.visual.roompark.domain.contract.*
import dagger.Binds
import dagger.Component
......@@ -71,6 +75,15 @@ abstract class DataModule {
@Binds
abstract fun provideRoomParkApi(roomParkApi:RetrofitRepository): IRoomParkApi
@Singleton
@Binds
abstract fun provideNotyCenter(center: NotificationCenter): INotificationCenter
@Singleton
@Binds
abstract fun provideLifecycleObserver(obs:AppLifecycleListener): ForegroundLifecycleObserver
@Singleton
@Binds
abstract fun provideDb(db: RequeryRepository) : IDb
......
package com.biganto.visual.roompark.domain.interactor
import android.content.Context
import android.content.Intent
import android.os.Build
import com.biganto.visual.roompark.R
import com.biganto.visual.roompark.base.BaseRoomParkActivity
import com.biganto.visual.roompark.data.service.download.DOWNLOAD_MANAGER_ADD_IDS_TO_LOAD_COMMAND
import com.biganto.visual.roompark.data.service.download.DOWNLOAD_MANAGER_COMMAND_KEY
import com.biganto.visual.roompark.data.service.download.DownloadManagerService
import com.biganto.visual.roompark.domain.model.CachedDataModel
import com.biganto.visual.roompark.domain.model.PushSwitchModel
import com.biganto.visual.roompark.domain.model.SettingsModel
import com.biganto.visual.roompark.domain.model.SubscriptionModel
import com.biganto.visual.roompark.domain.use_case.AuthUseCase
import com.biganto.visual.roompark.domain.use_case.SettingsUseCase
import com.biganto.visual.roompark.domain.use_case.SubscriptionUseCase
import com.biganto.visual.roompark.domain.use_case.*
import io.reactivex.Completable
import io.reactivex.Observable
import io.reactivex.schedulers.Schedulers
import timber.log.Timber
import java.util.concurrent.TimeUnit
import javax.inject.Inject
/**
......@@ -21,10 +26,48 @@ import javax.inject.Inject
class SettingsInteractor @Inject constructor(
private val auth: AuthUseCase,
private val settingsUseCase: SettingsUseCase,
private val activity: Context,
private val subUc: SubscriptionUseCase
private val activity: BaseRoomParkActivity,
private val subUc: SubscriptionUseCase,
private val toursUc: TourPreviewsUseCase
){
private fun startDownloadService() {
val i = Intent(activity, DownloadManagerService::class.java)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
activity.startForegroundService(i)
} else {
activity.startService(i)
}
}
private fun startDownloadService(ids:List<String>){
Timber.d(" gonna startService ++")
try {
val i = Intent(activity, DownloadManagerService::class.java)
i.putExtra(DOWNLOAD_MANAGER_COMMAND_KEY, DOWNLOAD_MANAGER_ADD_IDS_TO_LOAD_COMMAND)
i.putStringArrayListExtra(TOUR_IDS_TO_DOWNLOAD_KEY, ArrayList(ids))
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
activity.startForegroundService(i)
} else {
activity.startService(i)
}
}catch (e:Exception){
Timber.d(" gonna startService errror")
Timber.e(e)
}
}
fun startToursDownloading(): Completable =
toursUc.downloadAllDeelsAndEstates()
.doOnNext { Timber.d(" gonna startService") }
.doOnNext { this.startDownloadService() }
.delay(100,TimeUnit.MILLISECONDS)
.doOnNext { tours ->
startDownloadService(tours.map { tour -> tour.id })
}.ignoreElements()
fun getSubscriptions() =
subUc.getCurrentUserSubscriptions()
......
......@@ -10,11 +10,14 @@ import com.biganto.visual.roompark.data.repository.db.requrey.model.FileEntity
import com.biganto.visual.roompark.data.repository.db.requrey.model.TourFileJunctionEntity
import com.biganto.visual.roompark.data.repository.db.requrey.model.fromRaw
import com.biganto.visual.roompark.data.repository.file.FileModule
import com.biganto.visual.roompark.data.service.download.TourFileData
import com.biganto.visual.roompark.domain.contract.AuthContract
import com.biganto.visual.roomparkvr.data.repository.db.requery.model.DownloadState
import com.biganto.visual.roomparkvr.data.repository.db.requery.model.TourPreviewEntity
import io.reactivex.BackpressureStrategy
import io.reactivex.Flowable
import io.reactivex.Observable
import io.reactivex.rxkotlin.Observables
import io.reactivex.schedulers.Schedulers
import okhttp3.ResponseBody
import okio.Okio
......@@ -35,9 +38,12 @@ class DownloadUseCase @Inject constructor(
private val db: IDb,
private val api: IBigantoApi,
private val fileModule: FileModule,
private val auth: AuthContract,
private val context: Application
) {
private fun writeFile(
response: ResponseBody,
model: TourFileData,
......@@ -109,7 +115,7 @@ class DownloadUseCase @Inject constructor(
}
Timber.d("save files: ${files.size}")
db.upsertFileEntity(files)?.blockingSubscribe { Timber.d("file saved") }
db.upsertFileEntity(files).blockingSubscribe { Timber.d("file saved") }
}
@Volatile
......@@ -142,7 +148,8 @@ class DownloadUseCase @Inject constructor(
}
private fun observableTourDownloading(tour: TourPreviewEntity, token: CancellationToken) =
private fun observableTourDownloading(tour: TourPreviewEntity, token: CancellationToken)
: Observable<TourPreviewEntity> =
api.getTourFiles(tour.id, tour.targetResolution.toString())
.map { tourDbModel = tour;it.first() }
.map { raw ->
......@@ -172,30 +179,18 @@ class DownloadUseCase @Inject constructor(
junctionList
}
.doOnNext { junctionList ->
db.upsertTourFileJunction(junctionList)?.subscribe { Timber.d("junction upserted") }
}
.doOnNext { _ ->
.flatMap{ list ->
tourDbModel?.let {
db.upsertTourPreview(it)?.subscribe { Timber.d("tour upserted") }
Observables.zip(
db.upsertTourPreview(it), db.upsertTourFileJunction(list)
).map { list }
}
}
.flatMapIterable { it }
.flatMap { junction ->
db.getFileEntity(junction.file)
.observable()
.map { entity ->
TourFileData(
fileUrl = junction.file
, tourId = junction.tour
, tempDownloadedSize = 0L
, tempOverallFileSize = entity.totalSize
, fileDownloadedSize = entity.downloadedSize
, tempTourTotalDiff = 0L
, isDownloaded = entity.isDownloaded
)
}
.map { entity -> TourFileData(entity,junction) }
}
.toFlowable(BackpressureStrategy.BUFFER)
.parallel(4)
......@@ -205,41 +200,32 @@ class DownloadUseCase @Inject constructor(
if (model.isDownloaded)
return@flatMap Flowable.just(model)
var header: HashMap<String, String>? = null
if (model.fileDownloadedSize > 0){
header = hashMapOf(Pair("Range", "bytes=${model.fileDownloadedSize}-"))
Timber.w("trying to continue download file " +
"url by: ${model.fileUrl}" +
"size is: ${model.fileDownloadedSize}/${model.tempOverallFileSize}" +
"and header is: $header")
}
val header: HashMap<String, String>? =
if (model.fileDownloadedSize > 0)
hashMapOf(Pair("Range", "bytes=${model.fileDownloadedSize}-"))
else null
api.downloadFile(model.fileUrl.revisionUri(), header)
.doOnError {
Timber.e(it)
}
.flatMap<TourFileData> {
writeFile(it, model, token)
.toFlowable(BackpressureStrategy.BUFFER)
.doOnCancel { Timber.d("CANCELLED") }
.doOnCancel { Timber.w("TOUR DOWNLOADING CANCELLED") }
}
.flatMap { downloadInfo ->
db.upsertFileEntity(
FileEntity().also{
it.setUri(downloadInfo.fileUrl)
it.setDownloadedSize(downloadInfo.fileDownloadedSize)
it.setTotalSize(downloadInfo.tempOverallFileSize)
it.setDownloaded(downloadInfo.isDownloaded)})
FileEntity().apply {
setUri(downloadInfo.fileUrl)
setDownloadedSize(downloadInfo.fileDownloadedSize)
setTotalSize(downloadInfo.tempOverallFileSize)
setDownloaded(downloadInfo.isDownloaded)
}
)
.toFlowable(BackpressureStrategy.BUFFER)
.map { downloadInfo }
}
}
.sequential()
.toObservable()
// .buffer(15L,TimeUnit.MILLISECONDS)
// .flatMapIterable { it }
.map { model ->
setDownloadInfo(
model.tourId
......@@ -250,8 +236,7 @@ class DownloadUseCase @Inject constructor(
model.tempDownloadedSize = 0
model.tourId
}
.delay(14L, TimeUnit.MILLISECONDS)
// .sample(37L, TimeUnit.MILLISECONDS)
.delay(12L, TimeUnit.MILLISECONDS)
.flatMap { db.upsertTourPreview(tourDbModel!!) }
......@@ -273,27 +258,25 @@ class DownloadUseCase @Inject constructor(
private fun getMeta(tour: TourPreviewEntity) =
api.getTourMetaAsString(tour.id)
?.doOnNext { meta ->
tour.let {
val metaUri = RevisionString("$META_PREDICTION${tour.id}$META_FILE_TYPE")
it.setMetaFileEntityId(metaUri)
.map { meta ->
tour.apply {
val metaUri =
RevisionString("$META_PREDICTION${tour.id}$META_FILE_TYPE")
setMetaFileEntityId(metaUri)
fileModule.saveFileToDisk(
File(
FileModule.assetsDirectory(context).plus(metaUri.uri())
),meta
File(FileModule.assetsDirectory(context).plus(metaUri.uri()))
, meta
)
}
}
?.map { tour }
?.onErrorReturn {
.onErrorReturn {
tour.isDownloaded = DownloadState.Crushed
db.upsertTourPreview(tour)?.blockingSubscribe()
db.upsertTourPreview(tour).blockingSubscribe()
tour
}
//#endregion oldMethod
private fun refreshGallery(file: File) {
MediaScannerConnection.scanFile(
context, arrayOf(file.path), null
......@@ -303,15 +286,5 @@ class DownloadUseCase @Inject constructor(
}
}
data class TourFileData(
val fileUrl: RevisionString,
val tourId: String,
var tempDownloadedSize: Long = 0L,
var tempOverallFileSize: Long = 0L,
var fileDownloadedSize: Long = 0L,
var tempTourTotalDiff: Long = 0L,
var isDownloaded: Boolean = false
)
data class CancellationToken(var isCancelled: Boolean)
}
\ No newline at end of file
......@@ -21,6 +21,8 @@ import javax.inject.Inject
*/
const val EMPTY_PARENT = -778
const val TOUR_IDS_TO_DOWNLOAD_KEY = "DOWNLOAD_MANAGER_IDS_TO LOAD"
class TourPreviewsUseCase @Inject constructor(
private val api:IBigantoApi
......@@ -54,7 +56,7 @@ class TourPreviewsUseCase @Inject constructor(
db.getTourPreview(tourId)
.observable()
.doOnNext{ it.isDownloaded=state }
.flatMap { db.upsertTourPreview(it)?.map{true} }
.flatMap { db.upsertTourPreview(it).map{true} }
private fun syncTour(tourId:String, parentId:Int):Observable<TourModel> =
syncTour(tourId,parentId,calcTargetResolution())
......@@ -168,6 +170,51 @@ class TourPreviewsUseCase @Inject constructor(
return fromApi
}
fun downloadAllDeelsAndEstates(): Observable<Iterable<TourPreviewEntity>> =
auth.currentUser()
.map { user ->
val estatesList =
user.deals?.map { it.estate }?.plus(
user.estates?.asIterable()?: arrayListOf()
)
estatesList
?.asSequence()
?.filter { it.multitourId != null }
?.toList()
?.map {
Pair(
it.multitourId!!
, TourRemoteRequestModel(
estateId = it.id
, targetResolution = user.targetResolution
)
)
}
}
.doOnNext { Timber.d("merged list:${it?.size}") }
.map { list ->
list.map { pair ->
api.getOfferTours(pair.first)
.doOnNext { Timber.d(" gonna merge") }
.map {
mergeRaw(
it
, pair.second
)
}
.map { it }
.doOnNext { Timber.d("merged list:${it.size}") }
.blockingFirst()
}
.flatten()
}
// .map { it.flatten() }
.doOnNext { Timber.d("merged flatten list:${it.size}") }
.flatMap { db.upsertTourPreview(it) }
.subscribeOn(Schedulers.io())
}
data class TourRemoteRequestModel(val estateId: Int, val token: String? = null, val targetResolution: Int)
......
......@@ -9,6 +9,7 @@ import io.reactivex.Observable
*/
interface SettingsScreen : BigantoBaseContract<SettingsScreenViewState> {
fun downloadAllTours(): Observable<Int>
fun signOut(): Observable<Int>
fun clearCache(): Observable<Int>
fun refreshCacheInfo(): Observable<Int>
......
......@@ -47,10 +47,14 @@ class SettingsScreenController :
override fun clearCache(): Observable<Int> =
clearCacheButton.clicks()
.debounce ( 500, TimeUnit.MICROSECONDS )
.debounce ( 120, TimeUnit.MICROSECONDS )
.map { Timber.d("Clicked clear cache button"); 1 }
.observeOn(AndroidSchedulers.mainThread())
override fun downloadAllTours(): Observable<Int> =
toursDownloaderButton.clicks().map { 1 }.observeOn(AndroidSchedulers.mainThread())
private val refreshEmitter = BehaviorRelay.create<Int>()
override fun refreshCacheInfo(): Observable<Int> = refreshEmitter
......
package com.biganto.visual.roompark.presentation.screen.settings
import android.content.Context
import com.biganto.visual.roompark.base.BaseRoomParkActivity
import com.biganto.visual.roompark.base.RoomParkMainActivity
import com.biganto.visual.roompark.di.dagger.AppComponent
import com.biganto.visual.roompark.di.dagger.PerScreen
......@@ -33,7 +33,7 @@ abstract class SettingsScreenModule{
@PerScreen
@Binds
abstract fun provideContext(activity: RoomParkMainActivity): Context
abstract fun provideActivity(activity: RoomParkMainActivity): BaseRoomParkActivity
// @PerScreen
// @Binds
......
package com.biganto.visual.roompark.presentation.screen.settings
import android.content.Context
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
......@@ -21,7 +21,7 @@ import javax.inject.Inject
class SettingsScreenPresenter @Inject constructor(
private val interactor: SettingsInteractor,
private val activity: Context
private val activity: BaseRoomParkActivity
)
: BigantoBasePresenter<SettingsScreen, SettingsScreenViewState>() {
......@@ -38,6 +38,10 @@ class SettingsScreenPresenter @Inject constructor(
override fun bindIntents() {
val onDownloadTours = intent(SettingsScreen::downloadAllTours)
.flatMap { interactor.startToursDownloading()
.andThen(Observable.just(SettingsScreenViewState.Idle()))
}
val onSubChecked = intent(SettingsScreen::onSubscription)
.flatMap { sub ->
......@@ -101,7 +105,7 @@ class SettingsScreenPresenter @Inject constructor(
it.first / it.second.toFloat()
)
}
.delay(500,TimeUnit.MILLISECONDS)
.delay(600,TimeUnit.MILLISECONDS)
.startWith(SettingsScreenViewState.OnCacheDeleting(0f))
.doOnError { Timber.e(it) }
}
......@@ -115,7 +119,8 @@ class SettingsScreenPresenter @Inject constructor(
refreshInfo,
fetchSubscriptions,
fetchCache,
onSubChecked
onSubChecked,
onDownloadTours
)
)
.doOnError { Timber.e(it) }
......
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="178dp"
android:height="91dp"
android:viewportWidth="178"
android:viewportHeight="91">
<group>
<clip-path android:pathData="M73,0L105,0L105,3.156L73,3.156ZM82.617,9.531L95.535,9.531L95.535,12.734L82.617,12.734ZM92.375,12.734L95.535,12.734L95.535,31.781L92.375,31.781ZM82.617,12.734L85.953,12.734L85.953,31.781L82.617,31.781ZM82.617,31.781L95.535,31.781L95.535,34.879L82.617,34.879ZM73,3.156L76.156,3.156L76.156,34.879L73,34.879ZM73,34.879L85.922,34.879L85.922,38.145L73,38.145ZM101.844,3.156L105,3.156L105,41.25L101.844,41.25ZM82.613,38.145L85.922,38.145L85.922,41.25L82.613,41.25ZM82.613,41.25L105,41.25L105,44.379L82.613,44.379ZM82.613,44.379L85.77,44.379L85.77,57.145L82.613,57.145ZM82.613,44.379 M 0,0"/>
<path
android:pathData="M68,-5L110,-5L110,62.145L68,62.145ZM68,-5"
android:fillColor="#40A19B"
android:fillAlpha="1"
android:fillType="nonZero"
android:strokeColor="#00000000"/>
</group>
</vector>
......@@ -7,12 +7,20 @@
android:orientation="vertical">
<ImageView
android:id="@+id/backgroundDownloader"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/backgroundDownloader"
android:scaleType="centerCrop"
android:background="#A62B2727"
android:scaleType="centerCrop"
android:clickable="true"
android:focusableInTouchMode="true"
android:focusable="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
/>
<com.google.android.material.textview.MaterialTextView
android:layout_width="0dp"
android:layout_height="wrap_content"
......
......@@ -4,7 +4,9 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/playTourCardOpacityLight">
android:background="@color/playTourCardOpacityLight"
android:clickable="true"
android:focusableInTouchMode="true">
<com.google.android.material.card.MaterialCardView
android:layout_width="0dp"
......@@ -45,8 +47,8 @@
android:focusable="true"
android:focusableInTouchMode="true"
android:scaleType="fitXY"
app:layout_constraintTop_toTopOf="parent"
android:src="@drawable/ic_close_circled"
app:layout_constraintEnd_toEndOf="parent"
android:src="@drawable/ic_close_circled" />
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
......@@ -100,6 +100,25 @@
<string name="download_tour_cancel_text">Отмена</string>
<!-- region notifications-->
<string name="notification_content_title">Румянцево Парк</string>
<string name="notification_content_text">Загрузчик</string>
<string name="state_downloaded_notify_message">загружен</string>
<string name="state_crushed_notify_message">произошла ошибка</string>
<string name="state_suspended_notify_message">загрузка приостановлена</string>
<string name="state_downloading_notify_message">загружается</string>
<string name="state_else_notify_message" />
<!--endregion-->
<string name="on_all_tours_downloaded_notification_message">Загрузка туров завершена</string>
<string name="on_all_tours_deleted_notification_message">Удаление туров завершено</string>
<string name="noty_tours_delete_left">Осталось удалить: %d%n</string>
<string name="noty_tours_download_left">Осталось загрузить: %d%n</string>
<string name="game_view_content_description" />
......
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