Article.kt 8.74 KB
Newer Older
Christophe Henry's avatar
Christophe Henry committed
1 2 3 4
package fr.chenry.android.freshrss.store.database.models

import android.net.Uri
import androidx.databinding.BaseObservable
5
import androidx.databinding.Bindable
6
import androidx.lifecycle.LiveData
7
import androidx.room.*
8
import fr.chenry.android.freshrss.BR
9 10
import fr.chenry.android.freshrss.store.api.FAVORITE_ITEMS_ID
import fr.chenry.android.freshrss.store.api.READ_ITEMS_ID
Christophe Henry's avatar
Christophe Henry committed
11
import fr.chenry.android.freshrss.store.api.models.ContentItem
12
import fr.chenry.android.freshrss.store.api.models.Href
13
import fr.chenry.android.freshrss.store.database.DbSerializable
14
import fr.chenry.android.freshrss.utils.firstOrElse
Christophe Henry's avatar
Christophe Henry committed
15
import io.reactivex.Flowable
16
import org.joda.time.LocalDateTime
17
import java.util.concurrent.atomic.AtomicBoolean
Christophe Henry's avatar
Christophe Henry committed
18

19 20 21 22 23 24 25
/*
 * NOTE: the managment of read/unread and favorite/not favorite here is utterly
 * complex. This is because, when the number of unread article is long, it is
 * not possible to sync their status in a single SQL query.
 * See: https://git.feneas.org/christophehenry/freshrss-android/-/issues/100
 */

Christophe Henry's avatar
Christophe Henry committed
26 27
typealias Articles = List<Article>

28 29 30 31 32 33
@Entity(tableName = "unread_article_ids")
data class UnreadArticleId(@PrimaryKey val id: String)

@Entity(tableName = "favorite_article_ids")
data class FavoriteArticleId(@PrimaryKey val id: String)

Christophe Henry's avatar
Christophe Henry committed
34 35 36 37 38 39 40 41
@Entity(tableName = "articles")
data class Article(
    @PrimaryKey
    val id: String,
    val title: String,
    val href: String,
    val author: String,
    val content: String,
42
    val streamId: String,
43
    val readStatus: ReadStatus = ReadStatus.READ,
44
    val favorite: FavoriteStatus = FavoriteStatus.NOT_FAVORITE,
Christophe Henry's avatar
Christophe Henry committed
45
    val crawled: LocalDateTime = LocalDateTime(0),
46 47
    val published: LocalDateTime = LocalDateTime(0),
    val scrollPosition: Int = 0
48 49
): BaseObservable() {

50

Christophe Henry's avatar
Christophe Henry committed
51
    @Ignore
52
    val url: Uri = Uri.parse(href)
Christophe Henry's avatar
Christophe Henry committed
53

54
    @Ignore
55
    private val mReadStatusRequestOnGoing = AtomicBoolean(false)
56 57

    @get:Bindable
58 59
    var readStatusRequestOnGoing
        get() = mReadStatusRequestOnGoing.get()
60
        set(value) {
61 62 63 64 65 66 67 68 69 70 71 72 73
            mReadStatusRequestOnGoing.set(value)
            notifyPropertyChanged(BR.readStatusRequestOnGoing)
        }

    @Ignore
    private val mFavoriteRequestOnGoing = AtomicBoolean(false)

    @get:Bindable
    var favoriteRequestOnGoing
        get() = mFavoriteRequestOnGoing.get()
        set(value) {
            mFavoriteRequestOnGoing.set(value)
            notifyPropertyChanged(BR.favoriteRequestOnGoing)
74 75
        }

Christophe Henry's avatar
Christophe Henry committed
76
    companion object {
77 78 79 80
        /** Prefix to append to article ids when dealing with ids returned by
         * [fr.chenry.android.freshrss.store.Store.getUnreadItemIds] */
        const val ARTICLE_ID_PREFIX = "tag:google.com,2005:reader/item/"

Christophe Henry's avatar
Christophe Henry committed
81 82 83
        fun fromContentItem(item: ContentItem) = Article(
            item.id,
            item.title,
84
            item.alternate.firstOrElse(Href("")).href,
Christophe Henry's avatar
Christophe Henry committed
85 86
            item.author,
            item.summary.content,
87
            item.origin.streamId,
Christophe Henry's avatar
Christophe Henry committed
88 89
            crawled = item.crawled,
            published = item.published
Christophe Henry's avatar
Christophe Henry committed
90 91 92 93
        )
    }
}

94 95 96 97 98 99 100 101 102 103
data class ArticleReadStatusUpdate(
    val id: String,
    val readStatus: ReadStatus
)

data class ArticleFavoriteStatusUpdate(
    val id: String,
    val favorite: FavoriteStatus
)

104 105 106 107 108
data class ArticleScrollPositionUpdate(
    val id: String,
    val scrollPosition: Int
)

109 110
enum class ReadStatus(override val dbName: String): DbSerializable {
    READ(ReadStatus.READ_DB_NAME), UNREAD(ReadStatus.UNREAD_DB_NAME);
Christophe Henry's avatar
Christophe Henry committed
111

112
    fun toggle() = when(this) {
Christophe Henry's avatar
Christophe Henry committed
113 114 115
        READ -> UNREAD
        UNREAD -> READ
    }
116 117

    fun toPostFormData(articleId: String) = when(this) {
118 119 120
        READ -> mapOf("i" to articleId, "a" to READ_ITEMS_ID)
        UNREAD -> mapOf("i" to articleId, "r" to READ_ITEMS_ID)
    }
121 122 123 124 125 126 127 128 129 130 131

    companion object {
        const val READ_DB_NAME = "READ"
        const val UNREAD_DB_NAME = "UNREAD"

        fun dbValueOf(value: String?) = when(value) {
            READ_DB_NAME -> READ
            UNREAD_DB_NAME -> UNREAD
            else -> READ
        }
    }
132 133
}

134 135
enum class FavoriteStatus(override val dbName: String): DbSerializable {
    FAVORITE(FavoriteStatus.FAVORITE_DB_NAME), NOT_FAVORITE(FavoriteStatus.NOT_FAVORITE_DB_NAME);
136 137 138 139 140 141 142 143 144

    fun toggle() = when(this) {
        FAVORITE -> NOT_FAVORITE
        NOT_FAVORITE -> FAVORITE
    }

    fun toPostFormData(articleId: String) = when(this) {
        FAVORITE -> mapOf("i" to articleId, "a" to FAVORITE_ITEMS_ID)
        NOT_FAVORITE -> mapOf("i" to articleId, "r" to FAVORITE_ITEMS_ID)
145
    }
146 147 148 149 150 151 152 153 154 155 156

    companion object {
        const val FAVORITE_DB_NAME = "FAVORITE"
        const val NOT_FAVORITE_DB_NAME = "NOT_FAVORITE"

        fun dbValueOf(value: String?) = when(value) {
            FAVORITE_DB_NAME -> FAVORITE
            NOT_FAVORITE_DB_NAME -> NOT_FAVORITE
            else -> NOT_FAVORITE
        }
    }
Christophe Henry's avatar
Christophe Henry committed
157 158 159 160
}

@Dao
interface ArticlesDAO {
161 162


163 164
    @Insert(onConflict = OnConflictStrategy.IGNORE)
    suspend fun insertAll(articles: Articles)
Christophe Henry's avatar
Christophe Henry committed
165

166 167
    @Update(entity = Article::class)
    suspend fun updateReadStatuses(readStatuses: List<ArticleReadStatusUpdate>)
Christophe Henry's avatar
Christophe Henry committed
168

169 170
    @Update(entity = Article::class)
    suspend fun updateFavoriteStatuses(readStatuses: List<ArticleFavoriteStatusUpdate>)
171

172 173 174
    @Update(entity = Article::class)
    suspend fun updateScrollPosition(articleScrollPositionUpdate: ArticleScrollPositionUpdate)

175 176
    @Query("UPDATE articles SET readStatus = '${ReadStatus.READ_DB_NAME}'")
    suspend fun markAllArticlesRead(): Int
177

178
    @Query("UPDATE articles SET favorite = '${FavoriteStatus.FAVORITE_DB_NAME}' WHERE id IN (:ids)")
179
    suspend fun markArticlesFavoriteForIds(ids: List<String>): Int
180

181
    @Query("UPDATE articles SET favorite = '${FavoriteStatus.NOT_FAVORITE_DB_NAME}' WHERE id NOT IN (:ids)")
182
    suspend fun markArtclesNotFavoriteExcludingIds(ids: List<String>): Int
183

184 185 186 187 188 189 190
    @Query("UPDATE articles SET favorite = '${FavoriteStatus.NOT_FAVORITE_DB_NAME}'")
    suspend fun markArtclesNotFavorite(): Int

    suspend fun syncAllReadStatuses(ids: List<String>) {
        val unreadIds = ids.map(::UnreadArticleId)
        emptyUnreadTableAndReload(unreadIds)
        syncReadAndUnreadArticles()
191
    }
Christophe Henry's avatar
Christophe Henry committed
192

193
    suspend fun syncAllFavoriteStatus(ids: List<String>) {
194 195 196
        val unreadIds = ids.map(::FavoriteArticleId)
        emptyFavoriteTableAndReload(unreadIds)
        syncFavoriteAndNotFavoriteArticles()
197 198
    }

Christophe Henry's avatar
Christophe Henry committed
199
    @Query("SELECT * FROM articles WHERE streamId = :streamId")
200
    fun getByStreamId(streamId: String): Flowable<Articles>
Christophe Henry's avatar
Christophe Henry committed
201

202 203
    @Query("SELECT * FROM articles WHERE id = :id LIMIT 1")
    fun getById(id: String): Flowable<Article>
Christophe Henry's avatar
Christophe Henry committed
204

205 206
    @Query("SELECT * FROM articles WHERE streamId = :streamId AND readStatus = '${ReadStatus.UNREAD_DB_NAME}'")
    fun getByStreamIdAndUnread(streamId: String): Flowable<Articles>
207

208 209
    @Query("SELECT * FROM articles WHERE streamId = :streamId AND favorite = '${FavoriteStatus.FAVORITE_DB_NAME}'")
    fun getByStreamIdAndFavorite(streamId: String): Flowable<Articles>
210 211 212

    @Query("SELECT max(published) FROM articles")
    fun mostRecentPubDate(): LiveData<LocalDateTime?>
213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290

    //region Favorites and unread support
    @Query("DELETE FROM unread_article_ids")
    suspend fun nukeUnreadTable()

    @Insert
    suspend fun insertUneadArticleIds(ids: List<UnreadArticleId>)

    @Query(
        """
        UPDATE articles SET readStatus = '${ReadStatus.UNREAD_DB_NAME}'
        WHERE id IN (
            SELECT id FROM unread_article_ids
        )
    """
    )
    suspend fun syncUneadArticles(): Int

    @Query(
        """
        UPDATE articles SET readStatus = '${ReadStatus.READ_DB_NAME}'
        WHERE id NOT IN (
            SELECT id FROM unread_article_ids
        )
    """
    )
    suspend fun syncReadArticles(): Int

    @Transaction
    suspend fun emptyUnreadTableAndReload(ids: List<UnreadArticleId>) {
        nukeUnreadTable()
        insertUneadArticleIds(ids)
    }

    @Transaction
    suspend fun syncReadAndUnreadArticles() {
        syncUneadArticles()
        syncReadArticles()
    }

    @Query("DELETE FROM favorite_article_ids")
    suspend fun nukeFavoriteTable()

    @Insert
    suspend fun insertFavoriteArticleIds(ids: List<FavoriteArticleId>)

    @Query(
        """
        UPDATE articles SET favorite = '${FavoriteStatus.FAVORITE_DB_NAME}'
        WHERE id IN (
            SELECT id FROM favorite_article_ids
        )
    """
    )
    suspend fun syncFavoriteArticles(): Int

    @Query(
        """
        UPDATE articles SET favorite = '${FavoriteStatus.NOT_FAVORITE_DB_NAME}'
        WHERE id NOT IN (
            SELECT id FROM favorite_article_ids
        )
    """
    )
    suspend fun syncNotFavoriteArticles(): Int

    @Transaction
    suspend fun emptyFavoriteTableAndReload(ids: List<FavoriteArticleId>) {
        nukeFavoriteTable()
        insertFavoriteArticleIds(ids)
    }

    @Transaction
    suspend fun syncFavoriteAndNotFavoriteArticles() {
        syncFavoriteArticles()
        syncNotFavoriteArticles()
    }
    //endregion
Christophe Henry's avatar
Christophe Henry committed
291
}