Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sessions offline first capabilities and app sync at startup time #160

Merged
merged 17 commits into from
Aug 23, 2023
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,15 @@
package com.android254.droidconKE2023.app

import android.app.Application
import android.app.NotificationChannel
import android.app.NotificationManager
import android.os.Build
import androidx.hilt.work.HiltWorkerFactory
import androidx.work.Configuration
import androidx.work.WorkManager
import com.android254.data.network.util.RemoteFeatureToggle
import com.android254.data.work.WorkConstants
import com.android254.data.work.WorkInitializer
import com.android254.droidconKE2023.BuildConfig
import com.android254.droidconKE2023.crashlytics.CrashlyticsTree
import dagger.hilt.android.HiltAndroidApp
Expand All @@ -37,6 +43,8 @@ class DroidconKE2023App : Application(), Configuration.Provider {
super.onCreate()
remoteFeatureToggle.sync()
initTimber()
setUpWorkerManagerNotificationChannel()
WorkInitializer.initialize(context = this)
}

override fun getWorkManagerConfiguration(): Configuration =
Expand All @@ -58,4 +66,16 @@ class DroidconKE2023App : Application(), Configuration.Provider {
Timber.plant(CrashlyticsTree())
}
}
private fun setUpWorkerManagerNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
WorkConstants.NOTIFICATION_CHANNEL,
WorkConstants.syncDataWorkerName,
NotificationManager.IMPORTANCE_HIGH
)
val notificationManager = getSystemService(NotificationManager::class.java)
notificationManager.createNotificationChannel(channel)
}
WorkManager.initialize(this, Configuration.Builder().setWorkerFactory(workerFactory).build())
}
}
3 changes: 2 additions & 1 deletion data/src/main/java/com/android254/data/dao/BookmarkDao.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,10 @@ package com.android254.data.dao
import androidx.room.Dao
import androidx.room.Query
import com.android254.data.db.model.BookmarkEntity
import kotlinx.coroutines.flow.Flow

@Dao
interface BookmarkDao : BaseDao<BookmarkEntity> {
@Query("SELECT * FROM bookmarks")
fun getBookmarkIds(): List<BookmarkEntity>
fun getBookmarkIds(): Flow<List<BookmarkEntity>>
}
6 changes: 3 additions & 3 deletions data/src/main/java/com/android254/data/dao/SessionDao.kt
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,10 @@ interface SessionDao : BaseDao<SessionEntity> {
suspend fun clearSessions()

@Query("SELECT * FROM sessions WHERE id = :id")
suspend fun getSessionById(id: String): SessionEntity?
fun getSessionById(id: String): Flow<SessionEntity?>

@RawQuery
suspend fun fetchSessionsWithFilters(query: SupportSQLiteQuery): List<SessionEntity>
@RawQuery(observedEntities = arrayOf(SessionEntity::class))
fun fetchSessionsWithFilters(query: SupportSQLiteQuery): Flow<List<SessionEntity>>

@Query("UPDATE sessions SET isBookmarked = :isBookmarked WHERE remote_id = :id")
suspend fun updateBookmarkedStatus(id: String, isBookmarked: Boolean)
Expand Down
210 changes: 132 additions & 78 deletions data/src/main/java/com/android254/data/repos/SessionsManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,22 +15,20 @@
*/
package com.android254.data.repos

import android.os.Build
import androidx.annotation.RequiresApi
import androidx.sqlite.db.SimpleSQLiteQuery
import com.android254.data.dao.BookmarkDao
import com.android254.data.dao.SessionDao
import com.android254.data.db.model.BookmarkEntity
import com.android254.data.di.IoDispatcher
import com.android254.data.network.apis.SessionsApi
import com.android254.data.network.util.NetworkError
import com.android254.data.repos.mappers.toDomainModel
import com.android254.data.repos.mappers.toEntity
import com.android254.domain.models.ResourceResult
import com.android254.domain.models.Session
import com.android254.domain.repos.SessionsRepo
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext
import javax.inject.Inject

Expand All @@ -40,87 +38,143 @@ class SessionsManager @Inject constructor(
private val bookmarkDao: BookmarkDao,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher
) : SessionsRepo {
@RequiresApi(Build.VERSION_CODES.O)
override suspend fun fetchAndSaveSessions(
fetchFromRemote: Boolean,
query: String?
): ResourceResult<List<Session>> = withContext(ioDispatcher) {
val sessions = if (query == null) {
dao.fetchSessions().first()
} else {
dao.fetchSessionsWithFilters(SimpleSQLiteQuery(query))
}

val isDbEmpty = sessions.isEmpty()
val hasAQuery = query != null
val shouldLoadFromCache = (!isDbEmpty && !fetchFromRemote) || hasAQuery
if (shouldLoadFromCache) {
return@withContext try {
val response = api.fetchSessions()
val remoteSessions = response.data.flatMap { (_, value) -> value }
if (remoteSessions.isEmpty()) {
ResourceResult.Empty("No sessions just yet")
} else {
remoteSessions.let {
dao.clearSessions()
val bookmarkIds = bookmarkDao.getBookmarkIds().map { sessionEntity ->
sessionEntity.sessionId
}
val sessionEntities = it.map { session ->
val newSession = session.toEntity().copy(
isBookmarked = bookmarkIds.contains(session.id)
)
newSession
}
dao.insert(sessionEntities)
ResourceResult.Success(
data = sessionEntities.map { sessionEntity -> sessionEntity.toDomainModel() }
)
}
}
} catch (e: Exception) {
when (e) {
is NetworkError -> ResourceResult.Error("Network error")
else -> ResourceResult.Error("Error fetching sessions")
override fun fetchSessions(): Flow<List<Session>> {
val bookmarksFlow = bookmarkDao.getBookmarkIds()
val sessionsFlow = dao.fetchSessions()
return combine(sessionsFlow, bookmarksFlow) { sessions, bookmarks ->
sessions.map { it.toDomainModel() }
.map { session ->
session.copy(isBookmarked = bookmarks.map { it.sessionId }.contains(session.id))
}
}
} else {
return@withContext ResourceResult.Success(
data = sessions.map {
it.toDomainModel()
}.flowOn(ioDispatcher)
}
override fun fetchBookmarkedSessions(): Flow<List<Session>> {
val bookmarksFlow = bookmarkDao.getBookmarkIds()
val sessionsFlow = dao.fetchSessions()
return combine(sessionsFlow, bookmarksFlow) { sessions, bookmarks ->
sessions.map { it.toDomainModel() }
.map { session ->
session.copy(isBookmarked = bookmarks.map { it.sessionId }.contains(session.id))
}
)
.filter { session -> session.isBookmarked }
}.flowOn(ioDispatcher)
}

override fun fetchFilteredSessions(query: String): Flow<List<Session>> {
val filteredSessions = dao.fetchSessionsWithFilters(SimpleSQLiteQuery(query))
val bookmarksFlow = bookmarkDao.getBookmarkIds()
return combine(filteredSessions, bookmarksFlow) { sessions, bookmarks ->
sessions.map { session ->
session.copy(
isBookmarked = bookmarks.map { it.sessionId }.contains(session.id.toString())
)
}.map { it.toDomainModel() }
}.flowOn(ioDispatcher)
}

override fun fetchSessionById(sessionId: String): Flow<Session?> {
val bookmarksFlow = bookmarkDao.getBookmarkIds()
val sessionFlow = dao.getSessionById(sessionId).map {
it?.toDomainModel()
}
return combine(sessionFlow, bookmarksFlow) { session, bookmarks ->
session?.copy(isBookmarked = bookmarks.map { it.sessionId }.contains(session?.id.toString()))
}
}

override suspend fun fetchSessionById(id: String): ResourceResult<Session> =
override suspend fun bookmarkSession(id: String) {
withContext(ioDispatcher) {
val session = dao.getSessionById(id)
?: return@withContext ResourceResult.Error(message = "requested event no longer available")
return@withContext ResourceResult.Success(data = session.toDomainModel())
bookmarkDao.insert(BookmarkEntity(id))
}
}

override suspend fun toggleBookmarkStatus(
id: String,
isCurrentlyStarred: Boolean
): ResourceResult<Boolean> = withContext(ioDispatcher) {
try {
dao.updateBookmarkedStatus(id, !isCurrentlyStarred)
if (isCurrentlyStarred) {
bookmarkDao.delete(BookmarkEntity(id))
} else {
bookmarkDao.insert(BookmarkEntity(id))
}
} catch (e: Exception) {
when (e) {
is NetworkError -> {
return@withContext ResourceResult.Error("Network error")
}
else -> {
return@withContext ResourceResult.Error("Error fetching sessions")
}
}
override suspend fun unBookmarkSession(id: String) {
withContext(ioDispatcher) {
bookmarkDao.delete(BookmarkEntity(id))
}
return@withContext ResourceResult.Success(data = dao.getBookmarkStatus(id))
}
// @RequiresApi(Build.VERSION_CODES.O)
Copy link
Contributor

@wangerekaharun wangerekaharun Aug 22, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@chege4179 one last one, can we not remove existing tests, want to merge this now

// override suspend fun fetchAndSaveSessions(
// fetchFromRemote: Boolean,
// query: String?
// ): ResourceResult<List<Session>> = withContext(ioDispatcher) {
// val sessions = if (query == null) {
// dao.fetchSessions().first()
// } else {
// dao.fetchSessionsWithFilters(SimpleSQLiteQuery(query))
// }
//
// val isDbEmpty = sessions.isEmpty()
// val hasAQuery = query != null
// val shouldLoadFromCache = (!isDbEmpty && !fetchFromRemote) || hasAQuery
// if (shouldLoadFromCache) {
// return@withContext try {
// val response = api.fetchSessions()
// val remoteSessions = response.data.flatMap { (_, value) -> value }
// if (remoteSessions.isEmpty()) {
// ResourceResult.Empty("No sessions just yet")
// } else {
// remoteSessions.let {
// dao.clearSessions()
// val bookmarkIds = bookmarkDao.getBookmarkIds().map { sessionEntity ->
// sessionEntity.sessionId
// }
// val sessionEntities = it.map { session ->
// val newSession = session.toEntity().copy(
// isBookmarked = bookmarkIds.contains(session.id)
// )
// newSession
// }
// dao.insert(sessionEntities)
// ResourceResult.Success(
// data = sessionEntities.map { sessionEntity -> sessionEntity.toDomainModel() }
// )
// }
// }
// } catch (e: Exception) {
// when (e) {
// is NetworkError -> ResourceResult.Error("Network error")
// else -> ResourceResult.Error("Error fetching sessions")
// }
// }
// } else {
// return@withContext ResourceResult.Success(
// data = sessions.map {
// it.toDomainModel()
// }
// )
// }
// }
//
// // override suspend fun fetchSessionById(id: String): ResourceResult<Session> =
// // withContext(ioDispatcher) {
// // val session = dao.getSessionById(id)
// // ?: return@withContext ResourceResult.Error(message = "requested event no longer available")
// // return@withContext ResourceResult.Success(data = session.toDomainModel())
// // }
//
// override suspend fun toggleBookmarkStatus(
// id: String,
// isCurrentlyStarred: Boolean
// ): ResourceResult<Boolean> = withContext(ioDispatcher) {
// try {
// dao.updateBookmarkedStatus(id, !isCurrentlyStarred)
// if (isCurrentlyStarred) {
// bookmarkDao.delete(BookmarkEntity(id))
// } else {
// bookmarkDao.insert(BookmarkEntity(id))
// }
// } catch (e: Exception) {
// when (e) {
// is NetworkError -> {
// return@withContext ResourceResult.Error("Network error")
// }
// else -> {
// return@withContext ResourceResult.Error("Error fetching sessions")
// }
// }
// }
// return@withContext ResourceResult.Success(data = dao.getBookmarkStatus(id))
// }
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@ interface LocalSessionsDataSource {

suspend fun deleteCachedSessions()

suspend fun getCachedSessionById(id: String): SessionEntity?
fun getCachedSessionById(id: String): Flow<SessionEntity?>

suspend fun fetchSessionWithFilters(query: SupportSQLiteQuery): List<Session>
fun fetchSessionWithFilters(query: SupportSQLiteQuery): Flow<List<Session>>

suspend fun updateBookmarkedStatus(id: String, isBookmarked: Boolean)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,17 +52,12 @@ class LocalSessionsDataSourceImpl @Inject constructor(
}
}

override suspend fun getCachedSessionById(id: String): SessionEntity? {
return withContext(ioDispatcher) {
sessionDao.getSessionById(id = id)
}
}
override fun getCachedSessionById(id: String): Flow<SessionEntity?> =
sessionDao.getSessionById(id = id).flowOn(ioDispatcher)

override suspend fun fetchSessionWithFilters(query: SupportSQLiteQuery): List<Session> {
return withContext(ioDispatcher) {
sessionDao.fetchSessionsWithFilters(query = query).map { it.toDomainModel() }
}
}
override fun fetchSessionWithFilters(query: SupportSQLiteQuery): Flow<List<Session>> =
sessionDao.fetchSessionsWithFilters(query = query).map { it.map { it.toDomainModel() } }
.flowOn(ioDispatcher)

override suspend fun updateBookmarkedStatus(id: String, isBookmarked: Boolean) {
withContext(ioDispatcher) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import androidx.work.NetworkType
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkInfo
import androidx.work.WorkManager
import com.android254.data.work.WorkConstants.syncDataWorkerName
import com.android254.domain.work.SyncDataWorkManager
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.flow.Flow
Expand Down Expand Up @@ -53,10 +54,6 @@ class SyncDataWorkManagerImpl @Inject constructor(
workManager.beginUniqueWork(syncDataWorkerName, ExistingWorkPolicy.KEEP, syncDataRequest)
.enqueue()
}

companion object {
const val syncDataWorkerName = "sync_data"
}
}

val List<WorkInfo>.anyRunning get() = any { it.state == WorkInfo.State.RUNNING }
Loading