forked from organicmaps/organicmaps
api/cache/sync finished
This commit is contained in:
parent
2400c21819
commit
104f02b987
29 changed files with 327 additions and 152 deletions
|
@ -382,7 +382,7 @@ dependencies {
|
|||
debugImplementation 'androidx.compose.ui:ui-test-manifest'
|
||||
|
||||
// hilt
|
||||
def hilt = "2.47"
|
||||
def hilt = '2.51.1'
|
||||
implementation "com.google.dagger:hilt-android:$hilt"
|
||||
kapt "com.google.dagger:hilt-compiler:$hilt"
|
||||
kapt "androidx.hilt:hilt-compiler:1.2.0"
|
||||
|
@ -398,6 +398,8 @@ dependencies {
|
|||
implementation 'com.hbb20:ccp:2.7.3'
|
||||
// webview
|
||||
implementation "androidx.webkit:webkit:1.11.0"
|
||||
// compress
|
||||
implementation 'id.zelory:compressor:3.0.1'
|
||||
|
||||
//Background processing
|
||||
def coroutines = '1.8.1'
|
||||
|
|
|
@ -24,6 +24,7 @@
|
|||
<uses-permission android:name="android.permission.ACCESS_LOCATION_EXTRA_COMMANDS" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
|
||||
<!--
|
||||
https://developer.android.com/reference/androidx/core/app/JobIntentService:
|
||||
When running on Android O, the JobScheduler will take care of wake locks
|
||||
|
@ -71,8 +72,17 @@
|
|||
android:theme="@style/MwmTheme"
|
||||
android:usesCleartextTraffic="true"
|
||||
tools:targetApi="33">
|
||||
<receiver
|
||||
android:name="app.tourism.data.remote.WifiReceiver"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.net.wifi.STATE_CHANGE" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<activity
|
||||
android:name="app.tourism.AuthActivity"
|
||||
android:screenOrientation="portrait"
|
||||
android:exported="false"
|
||||
android:windowSoftInputMode="adjustResize|adjustPan"
|
||||
android:theme="@style/MwmTheme" />
|
||||
|
@ -352,6 +362,7 @@
|
|||
|
||||
<activity
|
||||
android:name="app.tourism.MainActivity"
|
||||
android:screenOrientation="portrait"
|
||||
android:exported="false"
|
||||
android:theme="@style/MwmTheme" />
|
||||
<activity
|
||||
|
|
|
@ -16,6 +16,7 @@ import android.os.Handler;
|
|||
import android.os.Looper;
|
||||
import android.text.TextUtils;
|
||||
import android.text.method.LinkMovementMethod;
|
||||
import android.util.Log;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
|
@ -123,6 +124,7 @@ import static android.content.pm.PackageManager.PERMISSION_GRANTED;
|
|||
import static app.organicmaps.location.LocationState.FOLLOW;
|
||||
import static app.organicmaps.location.LocationState.FOLLOW_AND_ROTATE;
|
||||
import static app.organicmaps.location.LocationState.LOCATION_TAG;
|
||||
import static app.tourism.utils.MapUtilsKt.isInsideTajikistan;
|
||||
|
||||
public class MwmActivity extends BaseMwmFragmentActivity
|
||||
implements PlacePageActivationListener,
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
package app.tourism
|
||||
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.net.wifi.WifiManager
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.SystemBarStyle
|
||||
|
@ -14,6 +16,7 @@ import app.organicmaps.DownloadResourcesLegacyActivity
|
|||
import app.organicmaps.R
|
||||
import app.organicmaps.downloader.CountryItem
|
||||
import app.tourism.data.prefs.UserPreferences
|
||||
import app.tourism.data.remote.WifiReceiver
|
||||
import app.tourism.domain.models.resource.Resource
|
||||
import app.tourism.ui.screens.main.MainSection
|
||||
import app.tourism.ui.screens.main.ThemeViewModel
|
||||
|
@ -25,8 +28,11 @@ import kotlinx.coroutines.flow.collectLatest
|
|||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
|
||||
@AndroidEntryPoint
|
||||
class MainActivity : ComponentActivity() {
|
||||
private val wifiReceiver = WifiReceiver()
|
||||
|
||||
@Inject
|
||||
lateinit var userPreferences: UserPreferences
|
||||
|
||||
|
@ -36,6 +42,10 @@ class MainActivity : ComponentActivity() {
|
|||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
val intentFilter = IntentFilter()
|
||||
intentFilter.addAction(WifiManager.NETWORK_STATE_CHANGED_ACTION)
|
||||
registerReceiver(wifiReceiver, intentFilter)
|
||||
|
||||
navigateToMapToDownloadIfNotPresent()
|
||||
navigateToAuthIfNotAuthed()
|
||||
|
||||
|
@ -94,4 +104,9 @@ class MainActivity : ComponentActivity() {
|
|||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
startActivity(this, intent, null)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
unregisterReceiver(wifiReceiver)
|
||||
super.onDestroy()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ import app.tourism.data.db.dao.HashesDao
|
|||
import app.tourism.data.db.dao.PlacesDao
|
||||
import app.tourism.data.db.dao.ReviewsDao
|
||||
import app.tourism.data.db.entities.CurrencyRatesEntity
|
||||
import app.tourism.data.db.entities.FavoriteToSyncEntity
|
||||
import app.tourism.data.db.entities.FavoriteSyncEntity
|
||||
import app.tourism.data.db.entities.HashEntity
|
||||
import app.tourism.data.db.entities.PlaceEntity
|
||||
import app.tourism.data.db.entities.ReviewEntity
|
||||
|
@ -19,7 +19,7 @@ import app.tourism.data.db.entities.ReviewPlannedToPostEntity
|
|||
PlaceEntity::class,
|
||||
ReviewEntity::class,
|
||||
ReviewPlannedToPostEntity::class,
|
||||
FavoriteToSyncEntity::class,
|
||||
FavoriteSyncEntity::class,
|
||||
HashEntity::class,
|
||||
CurrencyRatesEntity::class
|
||||
],
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
package app.tourism.data.db.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Delete
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import app.tourism.data.db.entities.FavoriteToSyncEntity
|
||||
import app.tourism.data.db.entities.FavoriteSyncEntity
|
||||
import app.tourism.data.db.entities.PlaceEntity
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
|
@ -22,7 +21,7 @@ interface PlacesDao {
|
|||
suspend fun deleteAllPlacesByCategory(categoryId: Long)
|
||||
|
||||
@Query("SELECT * FROM places WHERE UPPER(name) LIKE UPPER(:q)")
|
||||
fun search(q: String= ""): Flow<List<PlaceEntity>>
|
||||
fun search(q: String = ""): Flow<List<PlaceEntity>>
|
||||
|
||||
@Query("SELECT * FROM places WHERE categoryId = :categoryId")
|
||||
fun getPlacesByCategoryId(categoryId: Long): Flow<List<PlaceEntity>>
|
||||
|
@ -43,8 +42,14 @@ interface PlacesDao {
|
|||
suspend fun setFavorite(placeId: Long, isFavorite: Boolean)
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun addFavoriteToSync(favoriteToSyncEntity: FavoriteToSyncEntity)
|
||||
suspend fun addFavoriteSync(favoriteSyncEntity: FavoriteSyncEntity)
|
||||
|
||||
@Query("DELETE FROM favorites_to_sync WHERE placeId = :placeId")
|
||||
suspend fun removeFavoriteToSync(placeId: Long)
|
||||
suspend fun removeFavoriteSync(placeId: Long)
|
||||
|
||||
@Query("DELETE FROM favorites_to_sync WHERE placeId in (:placeId)")
|
||||
suspend fun removeFavoriteSync(placeId: List<Long>)
|
||||
|
||||
@Query("SELECT * FROM favorites_to_sync")
|
||||
fun getFavoriteSyncData(): List<FavoriteSyncEntity>
|
||||
}
|
||||
|
|
|
@ -21,7 +21,7 @@ interface ReviewsDao {
|
|||
@Query("DELETE FROM reviews WHERE id = :id")
|
||||
suspend fun deleteReview(id: Long)
|
||||
|
||||
@Query("DELETE FROM reviews WHERE id = :idsList")
|
||||
@Query("DELETE FROM reviews WHERE id in (:idsList)")
|
||||
suspend fun deleteReviews(idsList: List<Long>)
|
||||
|
||||
@Query("DELETE FROM reviews")
|
||||
|
@ -42,6 +42,12 @@ interface ReviewsDao {
|
|||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insertReviewPlannedToPost(review: ReviewPlannedToPostEntity)
|
||||
|
||||
@Query("DELETE FROM reviews_planned_to_post WHERE placeId = :placeId")
|
||||
suspend fun deleteReviewPlannedToPost(placeId: Long)
|
||||
|
||||
@Query("SELECT * FROM reviews_planned_to_post")
|
||||
fun getReviewsPlannedToPost(): List<ReviewPlannedToPostEntity>
|
||||
|
||||
@Query("SELECT * FROM reviews_planned_to_post")
|
||||
fun getReviewsPlannedToPostFlow(): Flow<List<ReviewPlannedToPostEntity>>
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ import androidx.room.Entity
|
|||
import androidx.room.PrimaryKey
|
||||
|
||||
@Entity(tableName = "favorites_to_sync")
|
||||
data class FavoriteToSyncEntity(
|
||||
data class FavoriteSyncEntity(
|
||||
@PrimaryKey val placeId: Long,
|
||||
val isFavorite: Boolean,
|
||||
)
|
|
@ -16,7 +16,6 @@ data class ReviewPlannedToPostEntity(
|
|||
) {
|
||||
fun toReviewsPlannedToPostDto(): ReviewToPost {
|
||||
val imageFiles = images.map { File(it) }
|
||||
imageFiles.first().path
|
||||
|
||||
return ReviewToPost(
|
||||
placeId, comment, rating, imageFiles
|
||||
|
|
|
@ -2,8 +2,7 @@ package app.tourism.data.remote
|
|||
|
||||
import android.content.Context
|
||||
import android.net.ConnectivityManager
|
||||
import android.util.Log
|
||||
import androidx.core.content.ContextCompat.getSystemService
|
||||
import app.organicmaps.R
|
||||
import app.tourism.domain.models.SimpleResponse
|
||||
import app.tourism.domain.models.resource.Resource
|
||||
import com.google.gson.Gson
|
||||
|
@ -15,10 +14,10 @@ import retrofit2.HttpException
|
|||
import retrofit2.Response
|
||||
import java.io.IOException
|
||||
|
||||
|
||||
suspend inline fun <T, reified R> FlowCollector<Resource<R>>.handleGenericCall(
|
||||
suspend inline fun <T, reified Re> FlowCollector<Resource<Re>>.handleGenericCall(
|
||||
call: () -> Response<T>,
|
||||
mapper: (T) -> R,
|
||||
mapper: (T) -> Re,
|
||||
context: Context,
|
||||
emitLoadingStatusBeforeCall: Boolean = true
|
||||
) {
|
||||
if (emitLoadingStatusBeforeCall) emit(Resource.Loading())
|
||||
|
@ -29,25 +28,20 @@ suspend inline fun <T, reified R> FlowCollector<Resource<R>>.handleGenericCall(
|
|||
else emit(response.parseError())
|
||||
} catch (e: HttpException) {
|
||||
e.printStackTrace()
|
||||
emit(
|
||||
Resource.Error(
|
||||
message = "Упс! Что-то пошло не так."
|
||||
)
|
||||
)
|
||||
emit(Resource.Error(context.getString(R.string.smth_went_wrong)))
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
emit(
|
||||
Resource.Error(
|
||||
message = "Не удается соединиться с сервером, проверьте интернет подключение"
|
||||
)
|
||||
)
|
||||
emit(Resource.Error(context.getString(R.string.no_network)))
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
emit(Resource.Error(message = "Упс! Что-то пошло не так."))
|
||||
emit(Resource.Error(context.getString(R.string.smth_went_wrong)))
|
||||
}
|
||||
}
|
||||
|
||||
suspend inline fun <reified T> handleResponse(call: () -> Response<T>): Resource<T> {
|
||||
suspend inline fun <reified T> handleResponse(
|
||||
call: () -> Response<T>,
|
||||
context: Context,
|
||||
): Resource<T> {
|
||||
try {
|
||||
val response = call()
|
||||
if (response.isSuccessful) {
|
||||
|
@ -56,15 +50,13 @@ suspend inline fun <reified T> handleResponse(call: () -> Response<T>): Resource
|
|||
} else return response.parseError()
|
||||
} catch (e: HttpException) {
|
||||
e.printStackTrace()
|
||||
return Resource.Error(message = "Упс! Что-то пошло не так.")
|
||||
return Resource.Error(context.getString(R.string.smth_went_wrong))
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
return Resource.Error(
|
||||
message = "Не удается соединиться с сервером, проверьте интернет подключение"
|
||||
)
|
||||
return Resource.Error(context.getString(R.string.no_network))
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
return Resource.Error(message = "Упс! Что-то пошло не так.")
|
||||
return Resource.Error(context.getString(R.string.smth_went_wrong))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ import app.tourism.data.dto.FavoritesDto
|
|||
import app.tourism.data.dto.FavoritesIdsDto
|
||||
import app.tourism.data.dto.HashDto
|
||||
import app.tourism.data.dto.auth.AuthResponseDto
|
||||
import app.tourism.data.dto.place.ReviewDto
|
||||
import app.tourism.data.dto.place.ReviewIdsDto
|
||||
import app.tourism.data.dto.place.ReviewsDto
|
||||
import app.tourism.data.dto.profile.LanguageDto
|
||||
|
@ -108,7 +109,7 @@ interface TourismApi {
|
|||
@Part("mark_id") placeId: RequestBody? = null,
|
||||
@Part("points") points: RequestBody? = null,
|
||||
@Part images: List<MultipartBody.Part>? = null
|
||||
): Response<SimpleResponse>
|
||||
): Response<ReviewDto>
|
||||
|
||||
@HTTP(method = "DELETE", path = "feedbacks", hasBody = true)
|
||||
suspend fun deleteReviews(
|
||||
|
|
|
@ -3,35 +3,31 @@ package app.tourism.data.remote
|
|||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.ConnectivityManager.NetworkCallback
|
||||
import android.net.Network
|
||||
import android.net.NetworkRequest
|
||||
import android.net.NetworkInfo
|
||||
import android.net.wifi.WifiManager
|
||||
import app.tourism.data.repositories.PlacesRepository
|
||||
import app.tourism.data.repositories.ReviewsRepository
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class WifiReceiver : BroadcastReceiver() {
|
||||
@Inject
|
||||
lateinit var reviewsRepository: ReviewsRepository
|
||||
|
||||
@Inject
|
||||
lateinit var placesRepository: PlacesRepository
|
||||
@Inject lateinit var reviewsRepository: ReviewsRepository
|
||||
@Inject lateinit var placesRepository: PlacesRepository
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||
val builder = NetworkRequest.Builder()
|
||||
val info: NetworkInfo? = intent.getParcelableExtra(WifiManager.EXTRA_NETWORK_INFO)
|
||||
|
||||
cm.registerNetworkCallback(
|
||||
builder.build(),
|
||||
object : NetworkCallback() {
|
||||
override fun onAvailable(network: Network) {
|
||||
super.onAvailable(network)
|
||||
|
||||
reviewsRepository.syncReviews()
|
||||
placesRepository.syncFavorites()
|
||||
}
|
||||
if (info != null && info.isConnected) {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
delay(2000L) // to avoid errors
|
||||
CoroutineScope(Dispatchers.IO).launch { reviewsRepository.syncReviews() }
|
||||
CoroutineScope(Dispatchers.IO).launch { placesRepository.syncFavorites() }
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,19 +1,22 @@
|
|||
package app.tourism.data.repositories
|
||||
|
||||
import android.content.Context
|
||||
import app.tourism.data.remote.TourismApi
|
||||
import app.tourism.data.remote.handleGenericCall
|
||||
import app.tourism.domain.models.SimpleResponse
|
||||
import app.tourism.domain.models.auth.AuthResponse
|
||||
import app.tourism.domain.models.auth.RegistrationData
|
||||
import app.tourism.domain.models.resource.Resource
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
|
||||
class AuthRepository(private val api: TourismApi) {
|
||||
class AuthRepository(private val api: TourismApi, private val context: Context) {
|
||||
fun signIn(email: String, password: String): Flow<Resource<AuthResponse>> = flow {
|
||||
handleGenericCall(
|
||||
call = { api.signIn(email, password) },
|
||||
mapper = { it.toAuthResponse() }
|
||||
mapper = { it.toAuthResponse() },
|
||||
context
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -28,14 +31,16 @@ class AuthRepository(private val api: TourismApi) {
|
|||
registrationData.country
|
||||
)
|
||||
},
|
||||
mapper = { it.toAuthResponse() }
|
||||
mapper = { it.toAuthResponse() },
|
||||
context
|
||||
)
|
||||
}
|
||||
|
||||
fun signOut(): Flow<Resource<SimpleResponse>> = flow {
|
||||
handleGenericCall(
|
||||
call = { api.signOut() },
|
||||
mapper = { it }
|
||||
mapper = { it },
|
||||
context
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
package app.tourism.data.repositories
|
||||
|
||||
import android.content.Context
|
||||
import app.tourism.data.db.Database
|
||||
import app.tourism.data.dto.currency.CurrenciesList
|
||||
import app.tourism.data.remote.CurrencyApi
|
||||
|
@ -10,7 +11,11 @@ import kotlinx.coroutines.flow.Flow
|
|||
import kotlinx.coroutines.flow.flow
|
||||
import kotlin.Double.Companion.NaN
|
||||
|
||||
class CurrencyRepository(private val api: CurrencyApi, private val db: Database) {
|
||||
class CurrencyRepository(
|
||||
private val api: CurrencyApi,
|
||||
private val db: Database,
|
||||
val context: Context
|
||||
) {
|
||||
val currenciesDao = db.currencyRatesDao
|
||||
|
||||
suspend fun getCurrency(): Flow<Resource<CurrencyRates>> = flow {
|
||||
|
@ -25,6 +30,7 @@ class CurrencyRepository(private val api: CurrencyApi, private val db: Database)
|
|||
db.currencyRatesDao.updateCurrencyRates(currencyRates.toCurrencyRatesEntity())
|
||||
currencyRates
|
||||
},
|
||||
context
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -3,12 +3,11 @@ package app.tourism.data.repositories
|
|||
import android.content.Context
|
||||
import app.organicmaps.R
|
||||
import app.tourism.data.db.Database
|
||||
import app.tourism.data.db.entities.FavoriteToSyncEntity
|
||||
import app.tourism.data.db.entities.FavoriteSyncEntity
|
||||
import app.tourism.data.db.entities.HashEntity
|
||||
import app.tourism.data.db.entities.PlaceEntity
|
||||
import app.tourism.data.db.entities.ReviewEntity
|
||||
import app.tourism.data.dto.FavoritesIdsDto
|
||||
import app.tourism.data.dto.HashDto
|
||||
import app.tourism.data.dto.place.PlaceDto
|
||||
import app.tourism.data.remote.TourismApi
|
||||
import app.tourism.data.remote.handleGenericCall
|
||||
|
@ -37,7 +36,7 @@ class PlacesRepository(
|
|||
fun downloadAllData(): Flow<Resource<SimpleResponse>> = flow {
|
||||
val hashes = hashesDao.getHashes()
|
||||
|
||||
val favoritesResponse = handleResponse { api.getFavorites() }
|
||||
val favoritesResponse = handleResponse(call = { api.getFavorites() }, context)
|
||||
|
||||
if (hashes.isEmpty()) {
|
||||
handleGenericCall(
|
||||
|
@ -83,6 +82,11 @@ class PlacesRepository(
|
|||
reviewsDao.deleteAllReviews()
|
||||
reviewsDao.insertReviews(reviewsEntities)
|
||||
|
||||
// update favorites
|
||||
favorites?.forEach {
|
||||
placesDao.setFavorite(it.id, it.isFavorite)
|
||||
}
|
||||
|
||||
// update hashes
|
||||
hashesDao.insertHashes(
|
||||
listOf(
|
||||
|
@ -94,7 +98,8 @@ class PlacesRepository(
|
|||
|
||||
// return response
|
||||
SimpleResponse(message = context.getString(R.string.great_success))
|
||||
}
|
||||
},
|
||||
context
|
||||
)
|
||||
} else {
|
||||
emit(Resource.Success(SimpleResponse(message = context.getString(R.string.great_success))))
|
||||
|
@ -120,7 +125,7 @@ class PlacesRepository(
|
|||
|
||||
val favorites = placesDao.getFavoritePlaces("")
|
||||
val resource =
|
||||
handleResponse { api.getPlacesByCategory(id, hash?.value ?: "") }
|
||||
handleResponse(call = { api.getPlacesByCategory(id, hash?.value ?: "") }, context)
|
||||
|
||||
if (hash != null && resource is Resource.Success)
|
||||
resource.data?.let { categoryDto ->
|
||||
|
@ -178,20 +183,45 @@ class PlacesRepository(
|
|||
|
||||
val favoritesIdsDto = FavoritesIdsDto(marks = listOf(placeId))
|
||||
|
||||
val favoriteToSyncEntity = FavoriteToSyncEntity(placeId, isFavorite)
|
||||
placesDao.addFavoriteToSync(favoriteToSyncEntity)
|
||||
val favoriteSyncEntity = FavoriteSyncEntity(placeId, isFavorite)
|
||||
placesDao.addFavoriteSync(favoriteSyncEntity)
|
||||
val response: Resource<SimpleResponse> = if (isFavorite)
|
||||
handleResponse { api.addFavorites(favoritesIdsDto) }
|
||||
handleResponse(call = { api.addFavorites(favoritesIdsDto) }, context)
|
||||
else
|
||||
handleResponse { api.removeFromFavorites(favoritesIdsDto) }
|
||||
handleResponse(call = { api.removeFromFavorites(favoritesIdsDto) }, context)
|
||||
|
||||
if (response is Resource.Success)
|
||||
placesDao.removeFavoriteToSync(favoriteToSyncEntity.placeId)
|
||||
placesDao.removeFavoriteSync(favoriteSyncEntity.placeId)
|
||||
else if (response is Resource.Error)
|
||||
placesDao.addFavoriteToSync(favoriteToSyncEntity)
|
||||
placesDao.addFavoriteSync(favoriteSyncEntity)
|
||||
}
|
||||
|
||||
fun syncFavorites() {
|
||||
suspend fun syncFavorites() {
|
||||
val favoritesToSyncEntities = placesDao.getFavoriteSyncData()
|
||||
|
||||
val favoritesToAdd = favoritesToSyncEntities.filter { it.isFavorite }.map { it.placeId }
|
||||
val favoritesToRemove = favoritesToSyncEntities.filter { !it.isFavorite }.map { it.placeId }
|
||||
|
||||
if (favoritesToAdd.isNotEmpty()) {
|
||||
val responseToAddFavs =
|
||||
handleResponse(
|
||||
call = { api.addFavorites(FavoritesIdsDto(favoritesToAdd)) },
|
||||
context
|
||||
)
|
||||
if (responseToAddFavs is Resource.Success) {
|
||||
placesDao.removeFavoriteSync(favoritesToAdd)
|
||||
}
|
||||
}
|
||||
|
||||
if (favoritesToRemove.isNotEmpty()) {
|
||||
val responseToRemoveFavs =
|
||||
handleResponse(
|
||||
call = { api.removeFromFavorites(FavoritesIdsDto(favoritesToRemove)) },
|
||||
context
|
||||
)
|
||||
if (responseToRemoveFavs is Resource.Success) {
|
||||
placesDao.removeFavoriteSync(favoritesToRemove)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,7 +27,8 @@ class ProfileRepository(
|
|||
call = { api.getUser() },
|
||||
mapper = {
|
||||
it.data.toPersonalData()
|
||||
}
|
||||
},
|
||||
context
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -59,7 +60,8 @@ class ProfileRepository(
|
|||
avatar = pfpMultipart
|
||||
)
|
||||
},
|
||||
mapper = { it.data.toPersonalData() }
|
||||
mapper = { it.data.toPersonalData() },
|
||||
context
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package app.tourism.data.repositories
|
||||
|
||||
import android.content.Context
|
||||
import app.organicmaps.R
|
||||
import app.tourism.data.db.Database
|
||||
import app.tourism.data.dto.place.ReviewIdsDto
|
||||
import app.tourism.data.remote.TourismApi
|
||||
|
@ -11,6 +12,8 @@ import app.tourism.domain.models.SimpleResponse
|
|||
import app.tourism.domain.models.details.Review
|
||||
import app.tourism.domain.models.details.ReviewToPost
|
||||
import app.tourism.domain.models.resource.Resource
|
||||
import app.tourism.utils.compress
|
||||
import app.tourism.utils.saveToInternalStorage
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
@ -38,8 +41,14 @@ class ReviewsRepository(
|
|||
}
|
||||
}
|
||||
|
||||
fun isThereReviewPlannedToPublish(): Flow<Boolean> = channelFlow {
|
||||
reviewsDao.getReviewsPlannedToPostFlow().collectLatest { reviewsEntities ->
|
||||
send(reviewsEntities.isNotEmpty())
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getReviewsFromApi(id: Long) {
|
||||
val getReviewsResponse = handleResponse { api.getReviewsByPlaceId(id) }
|
||||
val getReviewsResponse = handleResponse(call = { api.getReviewsByPlaceId(id) }, context)
|
||||
if (getReviewsResponse is Resource.Success) {
|
||||
reviewsDao.deleteAllPlaceReviews(id)
|
||||
getReviewsResponse.data?.data?.map { it.toReview().toReviewEntity() }
|
||||
|
@ -48,96 +57,117 @@ class ReviewsRepository(
|
|||
}
|
||||
|
||||
fun postReview(review: ReviewToPost): Flow<Resource<SimpleResponse>> = flow {
|
||||
emit(Resource.Loading())
|
||||
val imageFiles = mutableListOf<File>()
|
||||
review.images.forEach { imageFiles.add(compress(it, context)) }
|
||||
|
||||
if (isOnline(context)) {
|
||||
emit(Resource.Loading())
|
||||
val postReviewResponse = handleResponse {
|
||||
api.postReview(
|
||||
placeId = review.placeId.toString().toFormDataRequestBody(),
|
||||
comment = review.comment.toFormDataRequestBody(),
|
||||
points = review.rating.toString().toFormDataRequestBody(),
|
||||
images = getMultipartFromImageFiles(review.images)
|
||||
)
|
||||
}
|
||||
emit(postReviewResponse)
|
||||
val postReviewResponse = handleResponse(
|
||||
call = {
|
||||
api.postReview(
|
||||
placeId = review.placeId.toString().toFormDataRequestBody(),
|
||||
comment = review.comment.toFormDataRequestBody(),
|
||||
points = review.rating.toString().toFormDataRequestBody(),
|
||||
images = getMultipartFromImageFiles(imageFiles)
|
||||
)
|
||||
},
|
||||
context
|
||||
)
|
||||
|
||||
if (postReviewResponse is Resource.Success) {
|
||||
updateReviewsForDb(review.placeId)
|
||||
emit(Resource.Success(SimpleResponse(context.getString(R.string.review_was_published))))
|
||||
} else if (postReviewResponse is Resource.Error) {
|
||||
emit(Resource.Error(postReviewResponse.message ?: ""))
|
||||
}
|
||||
} else {
|
||||
reviewsDao.insertReviewPlannedToPost(review.toReviewPlannedToPostEntity())
|
||||
try {
|
||||
saveToInternalStorage(imageFiles, context)
|
||||
reviewsDao.insertReviewPlannedToPost(review.toReviewPlannedToPostEntity(imageFiles))
|
||||
emit(Resource.Error(context.getString(R.string.review_will_be_published)))
|
||||
} catch (e: OutOfMemoryError) {
|
||||
e.printStackTrace()
|
||||
emit(Resource.Error(context.getString(R.string.smth_went_wrong)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteReview(id: Long): Flow<Resource<SimpleResponse>> =
|
||||
flow {
|
||||
reviewsDao.markReviewForDeletion(id)
|
||||
val deleteReviewResponse =
|
||||
handleResponse {
|
||||
api.deleteReviews(ReviewIdsDto(listOf(id)))
|
||||
if (isOnline(context)) {
|
||||
val deleteReviewResponse =
|
||||
handleResponse(
|
||||
call = { api.deleteReviews(ReviewIdsDto(listOf(id))) },
|
||||
context,
|
||||
)
|
||||
|
||||
if (deleteReviewResponse is Resource.Success) {
|
||||
reviewsDao.deleteReview(id)
|
||||
}
|
||||
|
||||
if (deleteReviewResponse is Resource.Success) {
|
||||
reviewsDao.deleteReview(id)
|
||||
emit(deleteReviewResponse)
|
||||
} else {
|
||||
reviewsDao.markReviewForDeletion(id)
|
||||
}
|
||||
emit(deleteReviewResponse)
|
||||
|
||||
// val token = UserPreferences(context).getToken()
|
||||
//
|
||||
// val client = OkHttpClient()
|
||||
// val mediaType = "application/json".toMediaType()
|
||||
// val body = "{\n \"feedbacks\": [$id]\n}".toRequestBody(mediaType)
|
||||
// val request = Request.Builder()
|
||||
// .url("http://192.168.1.80:8888/api/feedbacks")
|
||||
// .method("DELETE", body)
|
||||
// .addHeader("Accept", "application/json")
|
||||
// .addHeader("Content-Type", "application/json")
|
||||
// .addHeader("Authorization", "Bearer $token")
|
||||
// .build()
|
||||
// val response = client.newCall(request).execute()
|
||||
|
||||
}
|
||||
|
||||
fun syncReviews() {
|
||||
val coroutineScope = CoroutineScope(Dispatchers.IO)
|
||||
coroutineScope.launch {
|
||||
deleteReviewsThatWereNotDeletedOnTheServer()
|
||||
publishReviewsThatWereNotPublished()
|
||||
}
|
||||
suspend fun syncReviews() {
|
||||
deleteReviewsThatWereNotDeletedOnTheServer()
|
||||
publishReviewsThatWereNotPublished()
|
||||
}
|
||||
|
||||
private suspend fun deleteReviewsThatWereNotDeletedOnTheServer() {
|
||||
val reviews = reviewsDao.getReviewsPlannedForDeletion()
|
||||
if (reviews.isEmpty()) {
|
||||
if (reviews.isNotEmpty()) {
|
||||
val reviewsIds = reviews.map { it.id }
|
||||
val response = handleResponse { api.deleteReviews(ReviewIdsDto(reviewsIds)) }
|
||||
val response =
|
||||
handleResponse(call = { api.deleteReviews(ReviewIdsDto(reviewsIds)) }, context)
|
||||
if (response is Resource.Success) {
|
||||
reviewsDao.deleteReviews(reviewsIds)
|
||||
}
|
||||
}
|
||||
// todo
|
||||
}
|
||||
|
||||
private suspend fun publishReviewsThatWereNotPublished() {
|
||||
val reviewsPlannedToPostEntities = reviewsDao.getReviewsPlannedToPost()
|
||||
if (reviewsPlannedToPostEntities.isEmpty()) {
|
||||
if (reviewsPlannedToPostEntities.isNotEmpty()) {
|
||||
val reviews = reviewsPlannedToPostEntities.map { it.toReviewsPlannedToPostDto() }
|
||||
reviews.forEach {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
api.postReview(
|
||||
placeId = it.placeId.toString().toFormDataRequestBody(),
|
||||
comment = it.comment.toFormDataRequestBody(),
|
||||
points = it.rating.toString().toFormDataRequestBody(),
|
||||
images = getMultipartFromImageFiles(it.images)
|
||||
val response = handleResponse(
|
||||
call = {
|
||||
api.postReview(
|
||||
placeId = it.placeId.toString().toFormDataRequestBody(),
|
||||
comment = it.comment.toFormDataRequestBody(),
|
||||
points = it.rating.toString().toFormDataRequestBody(),
|
||||
images = getMultipartFromImageFiles(it.images)
|
||||
)
|
||||
},
|
||||
context,
|
||||
)
|
||||
if (response is Resource.Success) {
|
||||
try {
|
||||
updateReviewsForDb(it.placeId)
|
||||
reviewsDao.deleteReviewPlannedToPost(it.placeId)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
} else if (response is Resource.Error) {
|
||||
try {
|
||||
reviewsDao.deleteReviewPlannedToPost(it.placeId)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun updateReviewsForDb(id: Long) {
|
||||
val getReviewsResponse = handleResponse {
|
||||
api.getReviewsByPlaceId(id)
|
||||
}
|
||||
val getReviewsResponse = handleResponse(
|
||||
call = { api.getReviewsByPlaceId(id) },
|
||||
context
|
||||
)
|
||||
if (getReviewsResponse is Resource.Success) {
|
||||
reviewsDao.deleteAllPlaceReviews(id)
|
||||
val reviews =
|
||||
|
|
|
@ -22,8 +22,11 @@ import javax.inject.Singleton
|
|||
object RepositoriesModule {
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideAuthRepository(api: TourismApi): AuthRepository {
|
||||
return AuthRepository(api)
|
||||
fun provideAuthRepository(
|
||||
api: TourismApi,
|
||||
@ApplicationContext context: Context,
|
||||
): AuthRepository {
|
||||
return AuthRepository(api, context)
|
||||
}
|
||||
|
||||
@Provides
|
||||
|
@ -61,8 +64,9 @@ object RepositoriesModule {
|
|||
@Singleton
|
||||
fun provideCurrencyRepository(
|
||||
api: CurrencyApi,
|
||||
db: Database
|
||||
db: Database,
|
||||
@ApplicationContext context: Context,
|
||||
): CurrencyRepository {
|
||||
return CurrencyRepository(api, db)
|
||||
return CurrencyRepository(api, db, context )
|
||||
}
|
||||
}
|
|
@ -9,8 +9,8 @@ data class ReviewToPost(
|
|||
val rating: Int,
|
||||
val images: List<File>,
|
||||
) {
|
||||
fun toReviewPlannedToPostEntity(): ReviewPlannedToPostEntity {
|
||||
val imagesPaths = images.map { it.path }
|
||||
fun toReviewPlannedToPostEntity(compressedImages: List<File>): ReviewPlannedToPostEntity {
|
||||
val imagesPaths = compressedImages.map { it.path }
|
||||
|
||||
return ReviewPlannedToPostEntity(
|
||||
placeId = placeId,
|
||||
|
|
|
@ -45,7 +45,9 @@ class HomeViewModel @Inject constructor(
|
|||
val sights = _sights.asStateFlow()
|
||||
private fun getTopSights() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
placesRepository.getTopPlaces(id = PlaceCategory.Sights.id)
|
||||
val categoryId = PlaceCategory.Sights.id
|
||||
placesRepository.getPlacesByCategoryFromApiIfThereIsChange(categoryId)
|
||||
placesRepository.getTopPlaces(categoryId)
|
||||
.collectLatest { resource ->
|
||||
if (resource is Resource.Success) {
|
||||
resource.data?.let { _sights.value = it }
|
||||
|
@ -59,7 +61,9 @@ class HomeViewModel @Inject constructor(
|
|||
val restaurants = _restaurants.asStateFlow()
|
||||
private fun getTopRestaurants() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
placesRepository.getTopPlaces(id = PlaceCategory.Restaurants.id)
|
||||
val categoryId = PlaceCategory.Restaurants.id
|
||||
placesRepository.getPlacesByCategoryFromApiIfThereIsChange(categoryId)
|
||||
placesRepository.getTopPlaces(categoryId)
|
||||
.collectLatest { resource ->
|
||||
if (resource is Resource.Success) {
|
||||
resource.data?.let { _restaurants.value = it }
|
||||
|
|
|
@ -6,6 +6,7 @@ import androidx.lifecycle.viewModelScope
|
|||
import app.organicmaps.R
|
||||
import app.tourism.data.repositories.ReviewsRepository
|
||||
import app.tourism.domain.models.SimpleResponse
|
||||
import app.tourism.domain.models.details.Review
|
||||
import app.tourism.domain.models.details.ReviewToPost
|
||||
import app.tourism.domain.models.resource.Resource
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
|
@ -36,7 +37,6 @@ class PostReviewViewModel @Inject constructor(
|
|||
_rating.value = value
|
||||
}
|
||||
|
||||
|
||||
private val _comment = MutableStateFlow("")
|
||||
val comment = _comment.asStateFlow()
|
||||
|
||||
|
@ -87,6 +87,9 @@ class PostReviewViewModel @Inject constructor(
|
|||
uiChannel.send(
|
||||
UiEvent.ShowToast(it.message ?: context.getString(R.string.smth_went_wrong))
|
||||
)
|
||||
if (it.message == context.getString(R.string.review_will_be_published)) {
|
||||
uiChannel.send(UiEvent.CloseReviewBottomSheet)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -62,6 +62,8 @@ fun ReviewsScreen(
|
|||
val userReview = reviewsVM.userReview.collectAsState().value
|
||||
val reviews = reviewsVM.reviews.collectAsState().value
|
||||
|
||||
val isThereReviewPlannedToPublish = reviewsVM.isThereReviewPlannedToPublish.collectAsState().value
|
||||
|
||||
ObserveAsEvents(flow = reviewsVM.uiEventsChannelFlow) { event ->
|
||||
if (event is UiEvent.ShowToast) context.showToast(event.message)
|
||||
}
|
||||
|
@ -90,7 +92,7 @@ fun ReviewsScreen(
|
|||
Text(text = "%.1f".format(rating) + "/5", style = TextStyles.h1)
|
||||
}
|
||||
|
||||
if (userReview == null) {
|
||||
if (userReview == null && !isThereReviewPlannedToPublish) {
|
||||
TextButton(
|
||||
onClick = {
|
||||
showReviewBottomSheet = true
|
||||
|
|
|
@ -63,6 +63,18 @@ class ReviewsViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private val _isThereReviewPlannedToPublish = MutableStateFlow(false)
|
||||
val isThereReviewPlannedToPublish = _isThereReviewPlannedToPublish.asStateFlow()
|
||||
|
||||
init {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
reviewsRepository.isThereReviewPlannedToPublish().collectLatest {
|
||||
_isThereReviewPlannedToPublish.value = it
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class DeleteReviewStatus { DELETED, IN_PROCESS }
|
|
@ -118,26 +118,28 @@ fun PostReview(
|
|||
},
|
||||
)
|
||||
}
|
||||
ImagePicker(
|
||||
showPreview = false,
|
||||
onSuccess = { uri ->
|
||||
scope.launch(Dispatchers.IO) {
|
||||
postReviewVM.addFile(
|
||||
File(FileUtils(context).getPath(uri))
|
||||
)
|
||||
if (files.size <= 10)
|
||||
ImagePicker(
|
||||
showPreview = false,
|
||||
onSuccess = { uri ->
|
||||
scope.launch(Dispatchers.IO) {
|
||||
postReviewVM.addFile(
|
||||
File(FileUtils(context).getPath(uri))
|
||||
)
|
||||
}
|
||||
focusManager.clearFocus()
|
||||
}
|
||||
focusManager.clearFocus()
|
||||
) {
|
||||
AddPhoto()
|
||||
}
|
||||
) {
|
||||
AddPhoto()
|
||||
}
|
||||
}
|
||||
VerticalSpace(height = 32.dp)
|
||||
|
||||
PrimaryButton(
|
||||
label = stringResource(id = R.string.send),
|
||||
onClick = { postReviewVM.postReview(placeId) },
|
||||
isLoading = postReviewResponse is Resource.Loading
|
||||
isLoading = postReviewResponse is Resource.Loading,
|
||||
enabled = postReviewResponse !is Resource.Loading
|
||||
)
|
||||
VerticalSpace(height = 64.dp)
|
||||
}
|
||||
|
|
23
android/app/src/main/java/app/tourism/utils/FileUtils.kt
Normal file
23
android/app/src/main/java/app/tourism/utils/FileUtils.kt
Normal file
|
@ -0,0 +1,23 @@
|
|||
package app.tourism.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import id.zelory.compressor.Compressor
|
||||
import id.zelory.compressor.constraint.format
|
||||
import id.zelory.compressor.constraint.resolution
|
||||
import id.zelory.compressor.constraint.size
|
||||
import java.io.File
|
||||
|
||||
suspend fun saveToInternalStorage(files: List<File>, context: Context) {
|
||||
val filesDir = context.filesDir
|
||||
files.forEach { file ->
|
||||
val destinationFile = File(filesDir, file.name)
|
||||
file.inputStream().copyTo(destinationFile.outputStream())
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun compress(file: File, context: Context): File =
|
||||
Compressor.compress(context, file) {
|
||||
format(Bitmap.CompressFormat.JPEG)
|
||||
size(900_000)
|
||||
}
|
|
@ -17,4 +17,21 @@ fun navigateToMapForRoute(context: Context, placeLocation: PlaceLocation) {
|
|||
val intent = Intent(context, MwmActivity::class.java)
|
||||
intent.putExtra("end_point", placeLocation)
|
||||
ContextCompat.startActivity(context, intent, null)
|
||||
}
|
||||
}
|
||||
|
||||
fun isInsideTajikistan(latitude: Double, longitude: Double): Boolean {
|
||||
val minLatitude = 36.4
|
||||
val maxLatitude = 41.3
|
||||
val minLongitude = 67.1
|
||||
val maxLongitude = 75.5
|
||||
|
||||
if (latitude < minLatitude || latitude > maxLatitude) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (longitude < minLongitude || longitude > maxLongitude) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
|
|
@ -2226,4 +2226,7 @@
|
|||
<string name="deletionPlanned">В процессе удаления</string>
|
||||
<string name="plz_wait_dowloading">Пожалуйста подождите данные скачиваются</string>
|
||||
<string name="empty_list">Пусто</string>
|
||||
<string name="review_will_be_published">Отзыв будет публикован когда будете онлайн</string>
|
||||
<string name="review_was_published">Отзыв был успешно опубликован</string>
|
||||
<string name="failed_to_publish_review">Не удалось публиковать отзыв</string>
|
||||
</resources>
|
||||
|
|
|
@ -2268,4 +2268,7 @@
|
|||
<string name="deletionPlanned">Deleting…</string>
|
||||
<string name="plz_wait_dowloading">Please, wait, data being downloaded</string>
|
||||
<string name="empty_list">Пусто</string>
|
||||
<string name="review_will_be_published">Review will be published when you are online</string>
|
||||
<string name="review_was_published">Review was successfully published</string>
|
||||
<string name="failed_to_publish_review">Failed to publish review\n</string>
|
||||
</resources>
|
||||
|
|
|
@ -4,5 +4,5 @@ plugins {
|
|||
id 'com.android.library' version '8.4.1' apply false
|
||||
id 'org.jetbrains.kotlin.android' version '1.9.0' apply false
|
||||
id 'org.jetbrains.kotlin.plugin.serialization' version '1.9.24' apply false
|
||||
id 'com.google.dagger.hilt.android' version '2.47' apply false
|
||||
id 'com.google.dagger.hilt.android' version '2.51.1' apply false
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue