Merge pull request #3974 from omnivore-app/fix/android-content-display

Use internal storage for content files
This commit is contained in:
Jackson Harper
2024-05-20 14:06:25 +08:00
committed by GitHub
13 changed files with 137 additions and 58 deletions

View File

@ -1,5 +1,6 @@
package app.omnivore.omnivore.core.data
import android.content.Context
import android.util.Log
import app.omnivore.omnivore.core.data.model.ServerSyncStatus
import app.omnivore.omnivore.core.database.entities.Highlight
@ -12,8 +13,8 @@ import app.omnivore.omnivore.core.network.savedItem
import app.omnivore.omnivore.core.network.savedItemUpdates
import app.omnivore.omnivore.core.network.search
suspend fun DataService.librarySearch(cursor: String?, query: String): SearchResult {
val searchResult = networker.search(cursor = cursor, limit = 10, query = query)
suspend fun DataService.librarySearch(context: Context, cursor: String?, query: String): SearchResult {
val searchResult = networker.search(context, cursor = cursor, limit = 10, query = query)
val savedItems = searchResult.items.map {
SavedItemWithLabelsAndHighlights(
@ -39,7 +40,7 @@ suspend fun DataService.librarySearch(cursor: String?, query: String): SearchRes
)
}
suspend fun DataService.sync(since: String, cursor: String?, limit: Int = 20): SavedItemSyncResult {
suspend fun DataService.sync(context: Context, since: String, cursor: String?, limit: Int = 20): SavedItemSyncResult {
val syncResult = networker.savedItemUpdates(cursor = cursor, limit = limit, since = since)
?: return SavedItemSyncResult.errorResult
@ -48,7 +49,16 @@ suspend fun DataService.sync(since: String, cursor: String?, limit: Int = 20): S
}
val savedItems = syncResult.items.map {
saveLibraryItemContentToFile(it.id, it.content)
if (!saveLibraryItemContentToFile(context, it.id, it.content)) {
return SavedItemSyncResult(
hasError = true,
errorString = "Error saving page content",
hasMoreItems = false,
count = 0,
cursor = null,
savedItemSlugs = listOf()
)
}
val savedItem = SavedItem(
savedItemId = it.id,
title = it.title,
@ -109,25 +119,27 @@ suspend fun DataService.sync(since: String, cursor: String?, limit: Int = 20): S
Log.d("sync", "found ${syncResult.items.size} items with sync api. Since: $since")
return SavedItemSyncResult(hasError = false,
return SavedItemSyncResult(
hasError = false,
errorString = null,
hasMoreItems = syncResult.hasMoreItems,
cursor = syncResult.cursor,
count = syncResult.items.size,
savedItemSlugs = syncResult.items.map { it.slug })
savedItemSlugs = syncResult.items.map { it.slug }
)
}
suspend fun DataService.isSavedItemContentStoredInDB(slug: String): Boolean {
suspend fun DataService.isSavedItemContentStoredInDB(context: Context, slug: String): Boolean {
val existingItem = db.savedItemDao().getSavedItemWithLabelsAndHighlights(slug)
existingItem?.savedItem?.savedItemId?.let { savedItemId ->
val htmlContent = loadLibraryItemContent(savedItemId)
val htmlContent = loadLibraryItemContent(context, savedItemId)
return (htmlContent ?: "").length > 10
}
return false
}
suspend fun DataService.fetchSavedItemContent(slug: String) {
val syncResult = networker.savedItem(slug)
suspend fun DataService.fetchSavedItemContent(context: Context, slug: String) {
val syncResult = networker.savedItem(context, slug)
val savedItem = syncResult.item
savedItem?.let {
val item = SavedItemWithLabelsAndHighlights(
@ -140,6 +152,7 @@ suspend fun DataService.fetchSavedItemContent(slug: String) {
data class SavedItemSyncResult(
val hasError: Boolean,
val errorString: String?,
val hasMoreItems: Boolean,
val count: Int,
val savedItemSlugs: List<String>,
@ -151,6 +164,7 @@ data class SavedItemSyncResult(
hasMoreItems = true,
cursor = null,
count = 0,
errorString = null,
savedItemSlugs = listOf()
)
}

View File

@ -1,5 +1,6 @@
package app.omnivore.omnivore.core.data.repository
import android.content.Context
import app.omnivore.omnivore.core.data.SavedItemSyncResult
import app.omnivore.omnivore.core.data.SearchResult
import app.omnivore.omnivore.core.data.model.LibraryQuery
@ -16,7 +17,7 @@ interface LibraryRepository {
suspend fun getLabels(): List<SavedItemLabel>
suspend fun fetchSavedItemContent(slug: String)
suspend fun fetchSavedItemContent(context: Context, slug: String)
suspend fun insertAllLabels(labels: List<SavedItemLabel>)
@ -30,9 +31,9 @@ interface LibraryRepository {
suspend fun createNewSavedItemLabel(labelName: String, hexColorValue: String)
suspend fun librarySearch(cursor: String?, query: String): SearchResult
suspend fun librarySearch(context: Context, cursor: String?, query: String): SearchResult
suspend fun isSavedItemContentStoredInDB(slug: String): Boolean
suspend fun isSavedItemContentStoredInDB(context: Context, slug: String): Boolean
suspend fun deleteSavedItem(itemID: String)
@ -44,5 +45,5 @@ interface LibraryRepository {
suspend fun syncHighlightChange(highlightChange: HighlightChange): Boolean
suspend fun sync(since: String, cursor: String?, limit: Int = 20): SavedItemSyncResult
suspend fun sync(context: Context, since: String, cursor: String?, limit: Int = 20): SavedItemSyncResult
}

View File

@ -1,5 +1,6 @@
package app.omnivore.omnivore.core.data.repository.impl
import android.content.Context
import android.util.Log
import app.omnivore.omnivore.core.data.DataService
import app.omnivore.omnivore.core.data.SavedItemSyncResult
@ -81,8 +82,8 @@ class LibraryRepositoryImpl @Inject constructor(
savedItemLabelDao.insertAll(labels)
}
override suspend fun fetchSavedItemContent(slug: String) {
val syncResult = networker.savedItem(slug)
override suspend fun fetchSavedItemContent(context: Context, slug: String) {
val syncResult = networker.savedItem(context, slug)
val savedItem = syncResult.item
savedItem?.let {
@ -192,9 +193,8 @@ class LibraryRepositoryImpl @Inject constructor(
}
}
override suspend fun librarySearch(cursor: String?, query: String): SearchResult {
val searchResult = networker.search(cursor = cursor, limit = 10, query = query)
override suspend fun librarySearch(context: Context, cursor: String?, query: String): SearchResult {
val searchResult = networker.search(context = context, cursor = cursor, limit = 10, query = query)
val savedItems = searchResult.items.map {
SavedItemWithLabelsAndHighlights(
savedItem = it.item,
@ -219,10 +219,10 @@ class LibraryRepositoryImpl @Inject constructor(
)
}
override suspend fun isSavedItemContentStoredInDB(slug: String): Boolean {
override suspend fun isSavedItemContentStoredInDB(context: Context, slug: String): Boolean {
val existingItem = savedItemDao.getSavedItemWithLabelsAndHighlights(slug)
existingItem?.savedItem?.savedItemId?.let { savedItemId ->
val htmlContent = loadLibraryItemContent(savedItemId)
val htmlContent = loadLibraryItemContent(context, savedItemId)
return (htmlContent ?: "").length > 10
}
return false
@ -412,7 +412,7 @@ class LibraryRepositoryImpl @Inject constructor(
}
}
override suspend fun sync(since: String, cursor: String?, limit: Int): SavedItemSyncResult {
override suspend fun sync(context: Context, since: String, cursor: String?, limit: Int): SavedItemSyncResult {
val syncResult = networker.savedItemUpdates(cursor = cursor, limit = limit, since = since)
?: return SavedItemSyncResult.errorResult
@ -421,7 +421,7 @@ class LibraryRepositoryImpl @Inject constructor(
}
val savedItems = syncResult.items.map {
saveLibraryItemContentToFile(it.id, it.content)
saveLibraryItemContentToFile(context, it.id, it.content)
val savedItem = SavedItem(
savedItemId = it.id,
title = it.title,
@ -482,10 +482,13 @@ class LibraryRepositoryImpl @Inject constructor(
Log.d("sync", "found ${syncResult.items.size} items with sync api. Since: $since")
return SavedItemSyncResult(hasError = false,
return SavedItemSyncResult(
hasError = false,
errorString = null,
hasMoreItems = syncResult.hasMoreItems,
cursor = syncResult.cursor,
count = syncResult.items.size,
savedItemSlugs = syncResult.items.map { it.slug })
savedItemSlugs = syncResult.items.map { it.slug }
)
}
}

View File

@ -1,5 +1,6 @@
package app.omnivore.omnivore.core.network
import android.content.Context
import android.util.Log
import app.omnivore.omnivore.core.database.entities.Highlight
import app.omnivore.omnivore.core.database.entities.SavedItem
@ -24,7 +25,7 @@ data class SavedItemQueryResponse(
}
}
suspend fun Networker.savedItem(slug: String): SavedItemQueryResponse {
suspend fun Networker.savedItem(context: Context, slug: String): SavedItemQueryResponse {
try {
val result = authenticatedApolloClient().query(
GetArticleQuery(slug = slug)
@ -80,7 +81,7 @@ suspend fun Networker.savedItem(slug: String): SavedItemQueryResponse {
localPDFPath = localFile.toPath().toString()
}
saveLibraryItemContentToFile(article.articleFields.id, article.articleFields.content)
saveLibraryItemContentToFile(context, article.articleFields.id, article.articleFields.content)
val savedItem = SavedItem(
savedItemId = article.articleFields.id,

View File

@ -1,16 +1,22 @@
package app.omnivore.omnivore.core.network
import android.os.Environment
import android.widget.Toast
import androidx.core.app.ActivityCompat
import app.omnivore.omnivore.core.data.model.ServerSyncStatus
import app.omnivore.omnivore.core.database.entities.Highlight
import app.omnivore.omnivore.core.database.entities.SavedItem
import app.omnivore.omnivore.core.database.entities.SavedItemLabel
import app.omnivore.omnivore.graphql.generated.SearchQuery
import com.apollographql.apollo3.api.Optional
import androidx.core.content.ContextCompat
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import android.Manifest
import android.content.Context
import android.content.Context.MODE_PRIVATE
import android.util.Log
import androidx.compose.ui.platform.LocalContext
data class LibrarySearchQueryResponse(
val cursor: String?, val items: List<LibrarySearchItem>
@ -21,6 +27,7 @@ data class LibrarySearchItem(
)
suspend fun Networker.search(
context: Context,
cursor: String? = null, limit: Int = 15, query: String
): LibrarySearchQueryResponse {
try {
@ -36,7 +43,7 @@ suspend fun Networker.search(
val itemList = result.data?.search?.onSearchSuccess?.edges ?: listOf()
val searchItems = itemList.map {
saveLibraryItemContentToFile(it.node.id, it.node.content)
saveLibraryItemContentToFile(context, it.node.id, it.node.content)
LibrarySearchItem(item = SavedItem(
savedItemId = it.node.id,
title = it.node.title,
@ -95,12 +102,38 @@ suspend fun Networker.search(
}
}
fun saveLibraryItemContentToFile(libraryItemId: String, content: String?): Boolean {
//
private fun writeToInternalStorage(context: Context, content: String, fileName: String) {
try {
context.openFileOutput(fileName, MODE_PRIVATE).use { outputStream ->
outputStream.write(content.toByteArray())
outputStream.flush()
Log.d("FileWrite", "File written successfully to internal storage.")
}
} catch (e: Exception) {
Log.e("FileWrite", "Error writing file", e)
throw e
}
}
private fun readFromInternalStorage(context: Context, fileName: String): String? {
return try {
context.openFileInput(fileName).bufferedReader().useLines { lines ->
lines.fold("") { some, text ->
"$some\n$text"
}
}
} catch (e: Exception) {
Log.e("FileRead", "Error reading file", e)
null
}
}
fun saveLibraryItemContentToFile(context: Context, libraryItemId: String, content: String?): Boolean {
return try {
content?.let { content ->
val directory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS)
val file = File(directory, "${libraryItemId}.html")
FileOutputStream(file).use { it.write(content.toByteArray()) }
writeToInternalStorage(context, content = content, fileName = "${libraryItemId}.html", )
return false
}
false
@ -110,15 +143,9 @@ fun saveLibraryItemContentToFile(libraryItemId: String, content: String?): Boole
}
}
fun loadLibraryItemContent(libraryItemId: String): String? {
fun loadLibraryItemContent(context: Context, libraryItemId: String): String? {
return try {
val directory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS)
val file = File(directory, "${libraryItemId}.html")
if (file.exists()) {
return FileInputStream(file).bufferedReader().use { it.readText() }
} else {
null
}
return readFromInternalStorage(context = context, fileName = "${libraryItemId}.html")
} catch (e: Exception) {
e.printStackTrace()
null

View File

@ -4,6 +4,7 @@ import android.content.Context
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
@ -91,9 +92,10 @@ class FollowingViewModel @Inject constructor(
syncLabels()
viewModelScope.launch {
val context = applicationContext
handleFilterChanges()
for (slug in contentRequestChannel) {
libraryRepository.fetchSavedItemContent(slug)
libraryRepository.fetchSavedItemContent(context, slug)
}
}
@ -148,7 +150,9 @@ class FollowingViewModel @Inject constructor(
fun loadUsingSearchAPI() {
viewModelScope.launch {
val context = applicationContext
val result = libraryRepository.librarySearch(
context = context,
cursor = librarySearchCursor,
query = searchQueryString()
)
@ -156,7 +160,7 @@ class FollowingViewModel @Inject constructor(
librarySearchCursor = it
}
result.savedItems.map {
val isSavedInDB = libraryRepository.isSavedItemContentStoredInDB(it.savedItem.slug)
val isSavedInDB = libraryRepository.isSavedItemContentStoredInDB(context = applicationContext, it.savedItem.slug)
if (!isSavedInDB) {
delay(2000)
@ -257,7 +261,7 @@ class FollowingViewModel @Inject constructor(
startTime: String,
) {
libraryRepository.syncOfflineItemsWithServerIfNeeded()
val result = libraryRepository.sync(since = since, cursor = cursor, limit = 20)
val result = libraryRepository.sync(context = this.applicationContext, since = since, cursor = cursor, limit = 20)
val totalCount = count + result.count
if (!result.hasError && result.hasMoreItems && result.cursor != null) {

View File

@ -1,7 +1,9 @@
package app.omnivore.omnivore.feature.library
import android.content.Intent
import android.content.pm.PackageManager
import android.util.Log
import android.widget.Toast
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.FloatTweenSpec
import androidx.compose.animation.core.animateFloatAsState
@ -56,6 +58,8 @@ import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavHostController
@ -76,6 +80,7 @@ import app.omnivore.omnivore.navigation.TopLevelDestination
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.launch
import java.util.jar.Manifest
@Composable
internal fun LibraryView(

View File

@ -1,9 +1,15 @@
package app.omnivore.omnivore.feature.library
import android.content.Context
import android.content.pm.PackageManager
import android.content.pm.PackageManager.*
import android.widget.Toast
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalContext
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
@ -33,6 +39,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import java.time.Instant
import java.util.jar.Manifest
import javax.inject.Inject
@OptIn(ExperimentalCoroutinesApi::class)
@ -113,7 +120,8 @@ class LibraryViewModel @Inject constructor(
viewModelScope.launch {
handleFilterChanges()
for (slug in contentRequestChannel) {
libraryRepository.fetchSavedItemContent(slug)
val context = applicationContext
libraryRepository.fetchSavedItemContent(context, slug)
}
}
@ -167,8 +175,10 @@ class LibraryViewModel @Inject constructor(
}
fun loadUsingSearchAPI() {
val context = applicationContext
viewModelScope.launch {
val result = libraryRepository.librarySearch(
context = context,
cursor = librarySearchCursor,
query = searchQueryString()
)
@ -176,7 +186,7 @@ class LibraryViewModel @Inject constructor(
librarySearchCursor = it
}
result.savedItems.map {
val isSavedInDB = libraryRepository.isSavedItemContentStoredInDB(it.savedItem.slug)
val isSavedInDB = libraryRepository.isSavedItemContentStoredInDB(context, it.savedItem.slug)
if (!isSavedInDB) {
delay(2000)
@ -277,9 +287,15 @@ class LibraryViewModel @Inject constructor(
) {
libraryRepository.syncOfflineItemsWithServerIfNeeded()
val result = libraryRepository.sync(since = since, cursor = cursor, limit = 20)
val result = libraryRepository.sync(context = this.applicationContext, since = since, cursor = cursor, limit = 10)
val totalCount = count + result.count
if (result.hasError) {
result.errorString?.let { errorString ->
System.out.println("SYNC ERROR: ${errorString}")
}
}
if (!result.hasError && result.hasMoreItems && result.cursor != null) {
performItemSync(
cursor = result.cursor,

View File

@ -1,5 +1,6 @@
package app.omnivore.omnivore.feature.library
import android.content.Context
import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
@ -17,6 +18,7 @@ import app.omnivore.omnivore.core.datastore.DatastoreRepository
import app.omnivore.omnivore.core.network.Networker
import app.omnivore.omnivore.core.network.typeaheadSearch
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.delay
@ -28,7 +30,8 @@ import javax.inject.Inject
class SearchViewModel @Inject constructor(
private val networker: Networker,
private val dataService: DataService,
private val datastoreRepo: DatastoreRepository
private val datastoreRepo: DatastoreRepository,
@ApplicationContext private val applicationContext: Context
) : ViewModel(), SavedItemViewModel {
private val contentRequestChannel = Channel<String>(capacity = Channel.UNLIMITED)
@ -76,8 +79,10 @@ class SearchViewModel @Inject constructor(
private fun loadUsingSearchAPI() {
viewModelScope.launch {
val context = applicationContext
withContext(Dispatchers.IO) {
val result = dataService.librarySearch(
context = applicationContext,
cursor = librarySearchCursor,
query = searchQueryString()
)
@ -86,7 +91,7 @@ class SearchViewModel @Inject constructor(
}
result.savedItems.map {
val isSavedInDB = dataService.isSavedItemContentStoredInDB(it.savedItem.slug)
val isSavedInDB = dataService.isSavedItemContentStoredInDB(applicationContext, it.savedItem.slug)
if (!isSavedInDB) {
delay(2000)

View File

@ -132,7 +132,6 @@ internal fun AboutScreen(
icon = ImageVector.vectorResource(R.drawable.ic_x),
url = "https://x.com/omnivoreapp",
)
LinkIcon(
label = "GitHub",
icon = ImageVector.vectorResource(R.drawable.ic_github),

View File

@ -27,6 +27,7 @@ import com.pspdfkit.annotations.HighlightAnnotation
import com.pspdfkit.document.download.DownloadJob
import com.pspdfkit.document.download.DownloadRequest
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@ -46,7 +47,8 @@ data class PDFReaderParams(
@HiltViewModel
class PDFReaderViewModel @Inject constructor(
private val dataService: DataService,
private val networker: Networker
private val networker: Networker,
@ApplicationContext private val applicationContext: Context
): ViewModel() {
var annotationUnderNoteEdit: Annotation? = null
val pdfReaderParamsLiveData = MutableLiveData<PDFReaderParams?>(null)
@ -91,7 +93,7 @@ class PDFReaderViewModel @Inject constructor(
private suspend fun loadItemFromNetwork(slug: String, context: Context) {
withContext(Dispatchers.IO) {
val articleQueryResult = networker.savedItem(slug)
val articleQueryResult = networker.savedItem(context = applicationContext, slug)
val article = articleQueryResult.item ?: return@withContext
val request = DownloadRequest.Builder(context)
.uri(article.pageURLString)

View File

@ -48,6 +48,7 @@ import app.omnivore.omnivore.graphql.generated.type.CreateLabelInput
import com.apollographql.apollo3.api.Optional.Companion.presentIfNotNull
import com.google.gson.Gson
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
@ -92,7 +93,8 @@ class WebReaderViewModel @Inject constructor(
private val dataService: DataService,
private val networker: Networker,
private val eventTracker: EventTracker,
private val savedItemDao: SavedItemDao // TODO - Use repo
private val savedItemDao: SavedItemDao,
@ApplicationContext private val applicationContext: Context
) : ViewModel() {
var lastJavascriptActionLoopUUID: UUID = UUID.randomUUID()
var javascriptDispatchQueue: MutableList<String> = mutableListOf()
@ -267,7 +269,7 @@ class WebReaderViewModel @Inject constructor(
val persistedItem = dataService.db.savedItemDao().getSavedItemWithLabelsAndHighlights(slug)
val savedItemId = persistedItem?.savedItem?.savedItemId
if (savedItemId != null) {
val htmlContent = loadLibraryItemContent(savedItemId)
val htmlContent = loadLibraryItemContent(applicationContext, savedItemId)
if (htmlContent != null) {
val articleContent = ArticleContent(
title = persistedItem.savedItem.title,
@ -300,10 +302,10 @@ class WebReaderViewModel @Inject constructor(
}
private suspend fun loadItemFromServer(slug: String): WebReaderParams? {
val articleQueryResult = networker.savedItem(slug)
val articleQueryResult = networker.savedItem(context = applicationContext, slug)
val article = articleQueryResult.item ?: return null
val htmlContent = loadLibraryItemContent(article.savedItemId)
val htmlContent = loadLibraryItemContent(applicationContext, article.savedItemId)
val articleContent = ArticleContent(
title = article.title,

View File

@ -159,7 +159,7 @@ const moduleExports = {
{
source: '/install/chrome',
destination:
'https://chrome.google.com/webstore/detail/omnivore/blkggjdmcfjdbmmmlfcpplkchpeaiiab/',
'https://chromewebstore.google.com/detail/omnivore/blkggjdmcfjdbmmmlfcpplkchpeaiiab',
permanent: true,
},
{