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 { ...@@ -60,6 +60,10 @@ android {
targetCompatibility 1.8 targetCompatibility 1.8
sourceCompatibility 1.8 sourceCompatibility 1.8
} }
lintOptions {
disable 'BinaryOperationInTimber'
}
} }
kapt { kapt {
......
...@@ -2,8 +2,13 @@ ...@@ -2,8 +2,13 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.biganto.visual.roompark"> 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.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<application <application
android:name=".base.RoomParkApplication" android:name=".base.RoomParkApplication"
...@@ -58,6 +63,11 @@ ...@@ -58,6 +63,11 @@
android:resource="@color/colorAccent" /> android:resource="@color/colorAccent" />
</service> </service>
<service
android:name=".data.service.download.DownloadManagerService"
android:enabled="true"
android:exported="false" />
</application> </application>
</manifest> </manifest>
\ No newline at end of file
...@@ -125,8 +125,7 @@ class DownloadTourDialogController : Controller { ...@@ -125,8 +125,7 @@ class DownloadTourDialogController : Controller {
snackbar.showSnackBar(error.localizedMessage) snackbar.showSnackBar(error.localizedMessage)
} }
)) ))
cancelDownloadText.setOnClickListener { cancelDownloadText.setOnClickListener {handleBack() }
downloadToken.isCancelled = true;handleBack() }
// downloadTour(it.tour.tour_id, downloadToken) // downloadTour(it.tour.tour_id, downloadToken)
// view.findViewById<View>(R.id.close_current_button).setOnClickListener { handleBack() } // view.findViewById<View>(R.id.close_current_button).setOnClickListener { handleBack() }
...@@ -147,7 +146,8 @@ class DownloadTourDialogController : Controller { ...@@ -147,7 +146,8 @@ class DownloadTourDialogController : Controller {
fun getLayoutId() = R.layout.download_tour_layout fun getLayoutId() = R.layout.download_tour_layout
override fun handleBack(): Boolean { override fun handleBack(): Boolean {
return router.popCurrentController() downloadToken.isCancelled = true
return router.popController(this)
} }
......
...@@ -80,7 +80,7 @@ class ChooseTourDialogController : Controller { ...@@ -80,7 +80,7 @@ class ChooseTourDialogController : Controller {
.subscribe(::onTourClicked) .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() } view.setOnClickListener { handleBack() }
return view return view
......
...@@ -60,7 +60,7 @@ class BigantoMviConductorLifecycleListener<V : MvpView, P : MviPresenter<V, *>> ...@@ -60,7 +60,7 @@ class BigantoMviConductorLifecycleListener<V : MvpView, P : MviPresenter<V, *>>
} }
val viewMpv = callback?.mvpView ?: throw NullPointerException( 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) { if (viewStateWillBeRestored) {
callback!!.setRestoringViewState(true) callback!!.setRestoringViewState(true)
......
...@@ -61,6 +61,7 @@ class EstateRepository @Inject constructor( ...@@ -61,6 +61,7 @@ class EstateRepository @Inject constructor(
} }
} }
.doOnNext(db::blockingUpsert) .doOnNext(db::blockingUpsert)
.doOnNext{ db.refreshUser(user) }
} }
private val getFavoritesDb: Observable<List<EstateEntity>> = private val getFavoritesDb: Observable<List<EstateEntity>> =
......
...@@ -60,12 +60,18 @@ class BigantoRetrofitRepository @Inject constructor(@Named("bigantoApi") retrof ...@@ -60,12 +60,18 @@ class BigantoRetrofitRepository @Inject constructor(@Named("bigantoApi") retrof
.doOnError(::e) .doOnError(::e)
override fun getOfferTours(multiTourId:Int): Observable<List<TourPreviewRaw>> = override fun getOfferTours(multiTourIds:List<Int>): Observable<List<TourPreviewRaw>> =
api 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()) .compose(RetrofitResponseValidation())
.map { it[multiTourId.toString()]?.toList()?: error("No tours avaliable")} .map { it[multiTourId.toString()]?.toList()?: error("No tours avaliable")}
.doOnError { e(it) } .doOnError { e(it) }
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
......
...@@ -19,10 +19,11 @@ interface IBigantoApi { ...@@ -19,10 +19,11 @@ interface IBigantoApi {
fun downloadFile(uri: String, headers: Map<String, String>?): Flowable<ResponseBody> fun downloadFile(uri: String, headers: Map<String, String>?): Flowable<ResponseBody>
// fun getToursFiles(tour_ids: List<Int>, resolution: String): Flowable<Map<String, List<TourFileRaw>>>? // 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 getTourFiles(tour_id: String, resolution: String): Observable<List<TourFilesDataRaw>>
fun getAppVersion(): Observable<AppVersionRaw> fun getAppVersion(): Observable<AppVersionRaw>
fun getToursPreviewById(tourIds: List<String>): Observable<List<TourPreviewRaw>> fun getToursPreviewById(tourIds: List<String>): Observable<List<TourPreviewRaw>>
fun getOfferTours(multiTourId:Int): 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 { ...@@ -145,7 +145,7 @@ interface IBigantoMobileApi {
@Query(CLIENT_VERSION_PARAM) clientVersion: String = DEFAULT_CLIENT_VERSION, @Query(CLIENT_VERSION_PARAM) clientVersion: String = DEFAULT_CLIENT_VERSION,
@Query(API_VERSION_PARAM) apiVersion: String = DEFAULT_API_VERSION, @Query(API_VERSION_PARAM) apiVersion: String = DEFAULT_API_VERSION,
@Query(LANG_PARAM) languageCode: String = Locale.getDefault().language, @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>>>> ): Observable<Response<Map<String,List<TourPreviewRaw>>>>
@Streaming @Streaming
......
...@@ -72,4 +72,5 @@ interface IDb { ...@@ -72,4 +72,5 @@ interface IDb {
fun dropFileTable(): Completable fun dropFileTable(): Completable
fun dropTourFileJuncTable(): Completable fun dropTourFileJuncTable(): Completable
fun dropTourTable(): Completable fun dropTourTable(): Completable
fun refreshUser(userEntity: UserEntity): Observable<UserEntity>
} }
\ No newline at end of file
...@@ -4,7 +4,6 @@ import android.app.Application ...@@ -4,7 +4,6 @@ import android.app.Application
import com.biganto.visual.roompark.Models import com.biganto.visual.roompark.Models
import com.biganto.visual.roompark.data.repository.db.IDb import com.biganto.visual.roompark.data.repository.db.IDb
import com.biganto.visual.roompark.data.repository.db.requrey.model.* 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.DownloadState
import com.biganto.visual.roomparkvr.data.repository.db.requery.model.TourPreview import com.biganto.visual.roomparkvr.data.repository.db.requery.model.TourPreview
import com.biganto.visual.roomparkvr.data.repository.db.requery.model.TourPreviewEntity import com.biganto.visual.roomparkvr.data.repository.db.requery.model.TourPreviewEntity
...@@ -30,6 +29,8 @@ import javax.inject.Inject ...@@ -30,6 +29,8 @@ import javax.inject.Inject
*/ */
private const val DATABASE_VERSION = 14
@Module @Module
class DbModule{ class DbModule{
...@@ -52,6 +53,9 @@ class RequeryRepository @Inject constructor( ...@@ -52,6 +53,9 @@ class RequeryRepository @Inject constructor(
) )
: IDb { : IDb {
override fun refreshUser(userEntity: UserEntity): Observable<UserEntity> =
store.refresh(userEntity).toObservable()
override fun dropTourTable() = store.delete(TourPreviewEntity::class).get().toCompletable() override fun dropTourTable() = store.delete(TourPreviewEntity::class).get().toCompletable()
override fun dropTourFileJuncTable() = store.delete(TourFileJunctionEntity::class).get().toCompletable() override fun dropTourFileJuncTable() = store.delete(TourFileJunctionEntity::class).get().toCompletable()
......
...@@ -78,6 +78,6 @@ interface Estate : Persistable { ...@@ -78,6 +78,6 @@ interface Estate : Persistable {
@get:Nullable @get:Nullable
@get:Column(name = "UserContainer") @get:Column(name = "UserContainer")
@get:ForeignKey(references = User::class ) @get:ForeignKey(references = User::class )
@get:OneToOne(mappedBy = "uuid",cascade = [CascadeAction.NONE]) @get:ManyToOne(cascade = [CascadeAction.NONE])
var user:User? var user:User?
} }
\ No newline at end of file
...@@ -25,6 +25,10 @@ interface User : Persistable { ...@@ -25,6 +25,10 @@ interface User : Persistable {
@get:OneToMany(cascade = [CascadeAction.DELETE]) @get:OneToMany(cascade = [CascadeAction.DELETE])
val deals:List<Deal>? val deals:List<Deal>?
@get:Nullable
@get:OneToMany(cascade = [CascadeAction.DELETE])
val estates:List<Estate>?
@get:Nullable @get:Nullable
@get:OneToMany(cascade = [CascadeAction.DELETE]) @get:OneToMany(cascade = [CascadeAction.DELETE])
val subscriptions:List<Subscription>? val subscriptions:List<Subscription>?
......
...@@ -68,6 +68,12 @@ class FileModule @Inject constructor(val context: Application) { ...@@ -68,6 +68,12 @@ class FileModule @Inject constructor(val context: Application) {
// fun deleteFile(uri:String)= getFile(uri).delete() // 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() = fun deleteAllCacheObservable() =
Observable.create<Pair<Int, Int>> {emitter -> Observable.create<Pair<Int, Int>> {emitter ->
val foldersToDelete = listOf( 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 ...@@ -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.IDb
import com.biganto.visual.roompark.data.repository.db.requrey.DbModule 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.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 com.biganto.visual.roompark.domain.contract.*
import dagger.BindsInstance import dagger.BindsInstance
import dagger.Component import dagger.Component
...@@ -64,6 +66,11 @@ interface AppComponent : AndroidInjector<RoomParkApplication>{ ...@@ -64,6 +66,11 @@ interface AppComponent : AndroidInjector<RoomParkApplication>{
fun provideTour():TourContract fun provideTour():TourContract
fun provideLifeCycle(): ForegroundLifecycleObserver
fun provideNotifivations(): INotificationCenter
fun provideAppContext():Application fun provideAppContext():Application
fun provideFileSystem(): FileModule fun provideFileSystem(): FileModule
......
...@@ -9,7 +9,6 @@ import dagger.Module ...@@ -9,7 +9,6 @@ import dagger.Module
* Created by Vladislav Bogdashkin on 13.06.2018. * Created by Vladislav Bogdashkin on 13.06.2018.
*/ */
const val DATABASE_VERSION = 13
@Module @Module
abstract class AppModule{ abstract class AppModule{
......
...@@ -10,6 +10,10 @@ import com.biganto.visual.roompark.data.repository.api.room_park.RetrofitReposit ...@@ -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.IDb
import com.biganto.visual.roompark.data.repository.db.requrey.DbModule 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.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 com.biganto.visual.roompark.domain.contract.*
import dagger.Binds import dagger.Binds
import dagger.Component import dagger.Component
...@@ -71,6 +75,15 @@ abstract class DataModule { ...@@ -71,6 +75,15 @@ abstract class DataModule {
@Binds @Binds
abstract fun provideRoomParkApi(roomParkApi:RetrofitRepository): IRoomParkApi abstract fun provideRoomParkApi(roomParkApi:RetrofitRepository): IRoomParkApi
@Singleton
@Binds
abstract fun provideNotyCenter(center: NotificationCenter): INotificationCenter
@Singleton
@Binds
abstract fun provideLifecycleObserver(obs:AppLifecycleListener): ForegroundLifecycleObserver
@Singleton @Singleton
@Binds @Binds
abstract fun provideDb(db: RequeryRepository) : IDb abstract fun provideDb(db: RequeryRepository) : IDb
......
package com.biganto.visual.roompark.domain.interactor 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.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.CachedDataModel
import com.biganto.visual.roompark.domain.model.PushSwitchModel import com.biganto.visual.roompark.domain.model.PushSwitchModel
import com.biganto.visual.roompark.domain.model.SettingsModel import com.biganto.visual.roompark.domain.model.SettingsModel
import com.biganto.visual.roompark.domain.model.SubscriptionModel 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.*
import com.biganto.visual.roompark.domain.use_case.SettingsUseCase
import com.biganto.visual.roompark.domain.use_case.SubscriptionUseCase
import io.reactivex.Completable import io.reactivex.Completable
import io.reactivex.Observable import io.reactivex.Observable
import io.reactivex.schedulers.Schedulers import io.reactivex.schedulers.Schedulers
import timber.log.Timber
import java.util.concurrent.TimeUnit
import javax.inject.Inject import javax.inject.Inject
/** /**
...@@ -21,10 +26,48 @@ import javax.inject.Inject ...@@ -21,10 +26,48 @@ import javax.inject.Inject
class SettingsInteractor @Inject constructor( class SettingsInteractor @Inject constructor(
private val auth: AuthUseCase, private val auth: AuthUseCase,
private val settingsUseCase: SettingsUseCase, private val settingsUseCase: SettingsUseCase,
private val activity: Context, private val activity: BaseRoomParkActivity,
private val subUc: SubscriptionUseCase 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() = fun getSubscriptions() =
subUc.getCurrentUserSubscriptions() subUc.getCurrentUserSubscriptions()
......
...@@ -10,11 +10,14 @@ import com.biganto.visual.roompark.data.repository.db.requrey.model.FileEntity ...@@ -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.TourFileJunctionEntity
import com.biganto.visual.roompark.data.repository.db.requrey.model.fromRaw 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.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.DownloadState
import com.biganto.visual.roomparkvr.data.repository.db.requery.model.TourPreviewEntity import com.biganto.visual.roomparkvr.data.repository.db.requery.model.TourPreviewEntity
import io.reactivex.BackpressureStrategy import io.reactivex.BackpressureStrategy
import io.reactivex.Flowable import io.reactivex.Flowable
import io.reactivex.Observable import io.reactivex.Observable
import io.reactivex.rxkotlin.Observables
import io.reactivex.schedulers.Schedulers import io.reactivex.schedulers.Schedulers
import okhttp3.ResponseBody import okhttp3.ResponseBody
import okio.Okio import okio.Okio
...@@ -35,9 +38,12 @@ class DownloadUseCase @Inject constructor( ...@@ -35,9 +38,12 @@ class DownloadUseCase @Inject constructor(
private val db: IDb, private val db: IDb,
private val api: IBigantoApi, private val api: IBigantoApi,
private val fileModule: FileModule, private val fileModule: FileModule,
private val auth: AuthContract,
private val context: Application private val context: Application
) { ) {
private fun writeFile( private fun writeFile(
response: ResponseBody, response: ResponseBody,
model: TourFileData, model: TourFileData,
...@@ -109,7 +115,7 @@ class DownloadUseCase @Inject constructor( ...@@ -109,7 +115,7 @@ class DownloadUseCase @Inject constructor(
} }
Timber.d("save files: ${files.size}") Timber.d("save files: ${files.size}")
db.upsertFileEntity(files)?.blockingSubscribe { Timber.d("file saved") } db.upsertFileEntity(files).blockingSubscribe { Timber.d("file saved") }
} }
@Volatile @Volatile
...@@ -142,7 +148,8 @@ class DownloadUseCase @Inject constructor( ...@@ -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()) api.getTourFiles(tour.id, tour.targetResolution.toString())
.map { tourDbModel = tour;it.first() } .map { tourDbModel = tour;it.first() }
.map { raw -> .map { raw ->
...@@ -172,30 +179,18 @@ class DownloadUseCase @Inject constructor( ...@@ -172,30 +179,18 @@ class DownloadUseCase @Inject constructor(
junctionList junctionList
} }
.doOnNext { junctionList -> .flatMap{ list ->
db.upsertTourFileJunction(junctionList)?.subscribe { Timber.d("junction upserted") }
}
.doOnNext { _ ->
tourDbModel?.let { tourDbModel?.let {
db.upsertTourPreview(it)?.subscribe { Timber.d("tour upserted") } Observables.zip(
db.upsertTourPreview(it), db.upsertTourFileJunction(list)
).map { list }
} }
} }
.flatMapIterable { it } .flatMapIterable { it }
.flatMap { junction -> .flatMap { junction ->
db.getFileEntity(junction.file) db.getFileEntity(junction.file)
.observable() .observable()
.map { entity -> .map { entity -> TourFileData(entity,junction) }
TourFileData(
fileUrl = junction.file
, tourId = junction.tour
, tempDownloadedSize = 0L
, tempOverallFileSize = entity.totalSize
, fileDownloadedSize = entity.downloadedSize
, tempTourTotalDiff = 0L
, isDownloaded = entity.isDownloaded
)
}
} }
.toFlowable(BackpressureStrategy.BUFFER) .toFlowable(BackpressureStrategy.BUFFER)
.parallel(4) .parallel(4)
...@@ -205,41 +200,32 @@ class DownloadUseCase @Inject constructor( ...@@ -205,41 +200,32 @@ class DownloadUseCase @Inject constructor(
if (model.isDownloaded) if (model.isDownloaded)
return@flatMap Flowable.just(model) return@flatMap Flowable.just(model)
var header: HashMap<String, String>? = null val header: HashMap<String, String>? =
if (model.fileDownloadedSize > 0){ if (model.fileDownloadedSize > 0)
header = hashMapOf(Pair("Range", "bytes=${model.fileDownloadedSize}-")) hashMapOf(Pair("Range", "bytes=${model.fileDownloadedSize}-"))
Timber.w("trying to continue download file " + else null
"url by: ${model.fileUrl}" +
"size is: ${model.fileDownloadedSize}/${model.tempOverallFileSize}" +
"and header is: $header")
}
api.downloadFile(model.fileUrl.revisionUri(), header) api.downloadFile(model.fileUrl.revisionUri(), header)
.doOnError {
Timber.e(it)
}
.flatMap<TourFileData> { .flatMap<TourFileData> {
writeFile(it, model, token) writeFile(it, model, token)
.toFlowable(BackpressureStrategy.BUFFER) .toFlowable(BackpressureStrategy.BUFFER)
.doOnCancel { Timber.d("CANCELLED") } .doOnCancel { Timber.w("TOUR DOWNLOADING CANCELLED") }
} }
.flatMap { downloadInfo -> .flatMap { downloadInfo ->
db.upsertFileEntity( db.upsertFileEntity(
FileEntity().also{ FileEntity().apply {
it.setUri(downloadInfo.fileUrl) setUri(downloadInfo.fileUrl)
it.setDownloadedSize(downloadInfo.fileDownloadedSize) setDownloadedSize(downloadInfo.fileDownloadedSize)
it.setTotalSize(downloadInfo.tempOverallFileSize) setTotalSize(downloadInfo.tempOverallFileSize)
it.setDownloaded(downloadInfo.isDownloaded)}) setDownloaded(downloadInfo.isDownloaded)
}
)
.toFlowable(BackpressureStrategy.BUFFER) .toFlowable(BackpressureStrategy.BUFFER)
.map { downloadInfo } .map { downloadInfo }
} }
} }
.sequential() .sequential()
.toObservable() .toObservable()
// .buffer(15L,TimeUnit.MILLISECONDS)
// .flatMapIterable { it }
.map { model -> .map { model ->
setDownloadInfo( setDownloadInfo(
model.tourId model.tourId
...@@ -250,8 +236,7 @@ class DownloadUseCase @Inject constructor( ...@@ -250,8 +236,7 @@ class DownloadUseCase @Inject constructor(
model.tempDownloadedSize = 0 model.tempDownloadedSize = 0
model.tourId model.tourId
} }
.delay(14L, TimeUnit.MILLISECONDS) .delay(12L, TimeUnit.MILLISECONDS)
// .sample(37L, TimeUnit.MILLISECONDS)
.flatMap { db.upsertTourPreview(tourDbModel!!) } .flatMap { db.upsertTourPreview(tourDbModel!!) }
...@@ -273,27 +258,25 @@ class DownloadUseCase @Inject constructor( ...@@ -273,27 +258,25 @@ class DownloadUseCase @Inject constructor(
private fun getMeta(tour: TourPreviewEntity) = private fun getMeta(tour: TourPreviewEntity) =
api.getTourMetaAsString(tour.id) api.getTourMetaAsString(tour.id)
?.doOnNext { meta -> .map { meta ->
tour.let { tour.apply {
val metaUri = RevisionString("$META_PREDICTION${tour.id}$META_FILE_TYPE") val metaUri =
it.setMetaFileEntityId(metaUri) RevisionString("$META_PREDICTION${tour.id}$META_FILE_TYPE")
setMetaFileEntityId(metaUri)
fileModule.saveFileToDisk( fileModule.saveFileToDisk(
File( File(FileModule.assetsDirectory(context).plus(metaUri.uri()))
FileModule.assetsDirectory(context).plus(metaUri.uri()) , meta
),meta
) )
} }
} }
?.map { tour } .onErrorReturn {
?.onErrorReturn {
tour.isDownloaded = DownloadState.Crushed tour.isDownloaded = DownloadState.Crushed
db.upsertTourPreview(tour)?.blockingSubscribe() db.upsertTourPreview(tour).blockingSubscribe()
tour tour
} }
//#endregion oldMethod //#endregion oldMethod
private fun refreshGallery(file: File) { private fun refreshGallery(file: File) {
MediaScannerConnection.scanFile( MediaScannerConnection.scanFile(
context, arrayOf(file.path), null context, arrayOf(file.path), null
...@@ -303,15 +286,5 @@ class DownloadUseCase @Inject constructor( ...@@ -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) data class CancellationToken(var isCancelled: Boolean)
} }
\ No newline at end of file
...@@ -21,6 +21,8 @@ import javax.inject.Inject ...@@ -21,6 +21,8 @@ import javax.inject.Inject
*/ */
const val EMPTY_PARENT = -778 const val EMPTY_PARENT = -778
const val TOUR_IDS_TO_DOWNLOAD_KEY = "DOWNLOAD_MANAGER_IDS_TO LOAD"
class TourPreviewsUseCase @Inject constructor( class TourPreviewsUseCase @Inject constructor(
private val api:IBigantoApi private val api:IBigantoApi
...@@ -54,7 +56,7 @@ class TourPreviewsUseCase @Inject constructor( ...@@ -54,7 +56,7 @@ class TourPreviewsUseCase @Inject constructor(
db.getTourPreview(tourId) db.getTourPreview(tourId)
.observable() .observable()
.doOnNext{ it.isDownloaded=state } .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> = private fun syncTour(tourId:String, parentId:Int):Observable<TourModel> =
syncTour(tourId,parentId,calcTargetResolution()) syncTour(tourId,parentId,calcTargetResolution())
...@@ -168,6 +170,51 @@ class TourPreviewsUseCase @Inject constructor( ...@@ -168,6 +170,51 @@ class TourPreviewsUseCase @Inject constructor(
return fromApi 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) data class TourRemoteRequestModel(val estateId: Int, val token: String? = null, val targetResolution: Int)
......
...@@ -9,6 +9,7 @@ import io.reactivex.Observable ...@@ -9,6 +9,7 @@ import io.reactivex.Observable
*/ */
interface SettingsScreen : BigantoBaseContract<SettingsScreenViewState> { interface SettingsScreen : BigantoBaseContract<SettingsScreenViewState> {
fun downloadAllTours(): Observable<Int>
fun signOut(): Observable<Int> fun signOut(): Observable<Int>
fun clearCache(): Observable<Int> fun clearCache(): Observable<Int>
fun refreshCacheInfo(): Observable<Int> fun refreshCacheInfo(): Observable<Int>
......
...@@ -47,10 +47,14 @@ class SettingsScreenController : ...@@ -47,10 +47,14 @@ class SettingsScreenController :
override fun clearCache(): Observable<Int> = override fun clearCache(): Observable<Int> =
clearCacheButton.clicks() clearCacheButton.clicks()
.debounce ( 500, TimeUnit.MICROSECONDS ) .debounce ( 120, TimeUnit.MICROSECONDS )
.map { Timber.d("Clicked clear cache button"); 1 } .map { Timber.d("Clicked clear cache button"); 1 }
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
override fun downloadAllTours(): Observable<Int> =
toursDownloaderButton.clicks().map { 1 }.observeOn(AndroidSchedulers.mainThread())
private val refreshEmitter = BehaviorRelay.create<Int>() private val refreshEmitter = BehaviorRelay.create<Int>()
override fun refreshCacheInfo(): Observable<Int> = refreshEmitter override fun refreshCacheInfo(): Observable<Int> = refreshEmitter
......
package com.biganto.visual.roompark.presentation.screen.settings 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.base.RoomParkMainActivity
import com.biganto.visual.roompark.di.dagger.AppComponent import com.biganto.visual.roompark.di.dagger.AppComponent
import com.biganto.visual.roompark.di.dagger.PerScreen import com.biganto.visual.roompark.di.dagger.PerScreen
...@@ -33,7 +33,7 @@ abstract class SettingsScreenModule{ ...@@ -33,7 +33,7 @@ abstract class SettingsScreenModule{
@PerScreen @PerScreen
@Binds @Binds
abstract fun provideContext(activity: RoomParkMainActivity): Context abstract fun provideActivity(activity: RoomParkMainActivity): BaseRoomParkActivity
// @PerScreen // @PerScreen
// @Binds // @Binds
......
package com.biganto.visual.roompark.presentation.screen.settings 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.conductor.BigantoBasePresenter
import com.biganto.visual.roompark.domain.interactor.SettingsInteractor import com.biganto.visual.roompark.domain.interactor.SettingsInteractor
import com.biganto.visual.roompark.domain.model.CachedDataModel import com.biganto.visual.roompark.domain.model.CachedDataModel
...@@ -21,7 +21,7 @@ import javax.inject.Inject ...@@ -21,7 +21,7 @@ import javax.inject.Inject
class SettingsScreenPresenter @Inject constructor( class SettingsScreenPresenter @Inject constructor(
private val interactor: SettingsInteractor, private val interactor: SettingsInteractor,
private val activity: Context private val activity: BaseRoomParkActivity
) )
: BigantoBasePresenter<SettingsScreen, SettingsScreenViewState>() { : BigantoBasePresenter<SettingsScreen, SettingsScreenViewState>() {
...@@ -38,6 +38,10 @@ class SettingsScreenPresenter @Inject constructor( ...@@ -38,6 +38,10 @@ class SettingsScreenPresenter @Inject constructor(
override fun bindIntents() { override fun bindIntents() {
val onDownloadTours = intent(SettingsScreen::downloadAllTours)
.flatMap { interactor.startToursDownloading()
.andThen(Observable.just(SettingsScreenViewState.Idle()))
}
val onSubChecked = intent(SettingsScreen::onSubscription) val onSubChecked = intent(SettingsScreen::onSubscription)
.flatMap { sub -> .flatMap { sub ->
...@@ -101,7 +105,7 @@ class SettingsScreenPresenter @Inject constructor( ...@@ -101,7 +105,7 @@ class SettingsScreenPresenter @Inject constructor(
it.first / it.second.toFloat() it.first / it.second.toFloat()
) )
} }
.delay(500,TimeUnit.MILLISECONDS) .delay(600,TimeUnit.MILLISECONDS)
.startWith(SettingsScreenViewState.OnCacheDeleting(0f)) .startWith(SettingsScreenViewState.OnCacheDeleting(0f))
.doOnError { Timber.e(it) } .doOnError { Timber.e(it) }
} }
...@@ -115,7 +119,8 @@ class SettingsScreenPresenter @Inject constructor( ...@@ -115,7 +119,8 @@ class SettingsScreenPresenter @Inject constructor(
refreshInfo, refreshInfo,
fetchSubscriptions, fetchSubscriptions,
fetchCache, fetchCache,
onSubChecked onSubChecked,
onDownloadTours
) )
) )
.doOnError { Timber.e(it) } .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 @@ ...@@ -7,12 +7,20 @@
android:orientation="vertical"> android:orientation="vertical">
<ImageView <ImageView
android:layout_width="match_parent" android:id="@+id/backgroundDownloader"
android:layout_height="match_parent" android:layout_width="match_parent"
android:id="@+id/backgroundDownloader" android:layout_height="match_parent"
android:scaleType="centerCrop" android:background="#A62B2727"
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 <com.google.android.material.textview.MaterialTextView
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
......
...@@ -4,7 +4,9 @@ ...@@ -4,7 +4,9 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="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 <com.google.android.material.card.MaterialCardView
android:layout_width="0dp" android:layout_width="0dp"
...@@ -45,8 +47,8 @@ ...@@ -45,8 +47,8 @@
android:focusable="true" android:focusable="true"
android:focusableInTouchMode="true" android:focusableInTouchMode="true"
android:scaleType="fitXY" android:scaleType="fitXY"
app:layout_constraintTop_toTopOf="parent" android:src="@drawable/ic_close_circled"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
android:src="@drawable/ic_close_circled" /> app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
...@@ -100,6 +100,25 @@ ...@@ -100,6 +100,25 @@
<string name="download_tour_cancel_text">Отмена</string> <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" /> <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