Skip to content

Commit

Permalink
Switch to internal files directory for storing books (#129)
Browse files Browse the repository at this point in the history
This fixes the issue of books failing to open randomly over time due to the app losing access to EPUB files it created. This loss of access can occur for various reasons, such as uninstalling and reinstalling the app, MediaStore reindexing, or another app gaining access to the download directory, resulting in write/read access loss. These issues are difficult to reproduce. Two possible fixes I can think of are requesting access to all files, as file managers do, or switching to the app-specific internal files directory. The former option, of course, makes no sense, so we're going with the latter. If you need access to the EPUB file of a certain book, you can always swipe right to save or share it anywhere you like.
  • Loading branch information
starry-shivam authored Mar 21, 2024
1 parent d1c5381 commit 1f35d7d
Show file tree
Hide file tree
Showing 29 changed files with 146 additions and 346 deletions.
5 changes: 0 additions & 5 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,6 @@

<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

<application
android:name=".MyneApp"
Expand All @@ -16,7 +12,6 @@
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:requestLegacyExternalStorage="true"
android:supportsRtl="true"
android:theme="@style/Theme.Myne"
tools:targetApi="34">
Expand Down
22 changes: 0 additions & 22 deletions app/src/main/java/com/starry/myne/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,7 @@

package com.starry.myne

import android.Manifest.permission.READ_EXTERNAL_STORAGE
import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import android.util.Log
import androidx.activity.compose.setContent
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.animation.ExperimentalAnimationApi
Expand All @@ -34,7 +29,6 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.core.app.ActivityCompat
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.ViewModelProvider
import coil.annotation.ExperimentalCoilApi
Expand Down Expand Up @@ -92,21 +86,5 @@ class MainActivity : AppCompatActivity() {
}
}
}
checkStoragePermission()
}

fun checkStoragePermission(): Boolean {
return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
if (checkSelfPermission(WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {
Log.d("MainActivity::Storage", "Permission is granted"); true
} else {
Log.d("MainActivity::Storage", "Permission is revoked")
ActivityCompat.requestPermissions(
this, arrayOf(WRITE_EXTERNAL_STORAGE, READ_EXTERNAL_STORAGE), 1
); false
}
} else {
true
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ class EpubXMLFileParser(
bodyElement = document.selectFirst("div#$fragmentId")

if (bodyElement != null) {
// If the fragment ID represents a <body> tag, fetch the entire body content
// If the fragment ID represents a <div> tag, fetch the entire body content
Log.d(
"EpubXMLFileParser",
"Fragment ID: $fragmentId represents a <div> tag. Using the fragment ID."
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,19 +38,18 @@ import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.Scaffold
import androidx.compose.material.Snackbar
import androidx.compose.material.SnackbarHost
import androidx.compose.material3.Scaffold
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.ArrowBack
import androidx.compose.material.icons.outlined.Share
import androidx.compose.material.rememberScaffoldState
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.material3.VerticalDivider
import androidx.compose.material3.surfaceColorAtElevation
Expand Down Expand Up @@ -123,20 +122,11 @@ fun BookDetailScreen(
val context = LocalContext.current
val settingsVM = (context.getActivity() as MainActivity).settingsViewModel

val scaffoldState = rememberScaffoldState()
val coroutineScope = rememberCoroutineScope()

val snackBarHostState = remember { SnackbarHostState() }
Scaffold(
scaffoldState = scaffoldState,
snackbarHost = {
SnackbarHost(hostState = it) { data ->
Snackbar(
backgroundColor = MaterialTheme.colorScheme.inverseSurface,
contentColor = MaterialTheme.colorScheme.inverseOnSurface,
snackbarData = data,
)
}
},
snackbarHost = { SnackbarHost(snackBarHostState) },
content = { paddingValues ->
LaunchedEffect(key1 = true, block = {
viewModel.getBookDetails(bookId)
Expand Down Expand Up @@ -176,7 +166,9 @@ fun BookDetailScreen(
viewModel.getBookDetails(bookId)
})
} else {
val book = state.bookSet.books.first()
// Get book details for this bookId.
val book = remember { state.bookSet.books.first() }

Column(
Modifier
.fillMaxSize()
Expand Down Expand Up @@ -370,19 +362,18 @@ fun BookDetailScreen(
navController = navController
)
}

}

context.getString(R.string.download_book_button) -> {
val message = viewModel.downloadBook(
book, (context.getActivity() as MainActivity)
) { downloadProgress, downloadStatus ->
progressState = downloadProgress
updateBtnText(downloadStatus)
}
viewModel.downloadBook(
book = book,
downloadProgressListener = { downloadProgress, downloadStatus ->
progressState = downloadProgress
updateBtnText(downloadStatus)
})
coroutineScope.launch {
scaffoldState.snackbarHostState.showSnackbar(
message = message,
snackBarHostState.showSnackbar(
message = context.getString(R.string.download_started),
)
}
}
Expand Down Expand Up @@ -414,47 +405,12 @@ fun BookDetailScreen(
color = MaterialTheme.colorScheme.onBackground,
)
} else {
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
val compositionResult: LottieCompositionResult =
rememberLottieComposition(
spec = LottieCompositionSpec.RawRes(R.raw.synopis_not_found_lottie)
)
val progressAnimation by animateLottieCompositionAsState(
compositionResult.value,
isPlaying = true,
iterations = LottieConstants.IterateForever,
speed = 1f
)

Spacer(modifier = Modifier.weight(2f))
LottieAnimation(
composition = compositionResult.value,
progress = progressAnimation,
modifier = Modifier
.fillMaxWidth(0.85f)
.height(200.dp),
enableMergePaths = true
)

Text(
text = stringResource(id = R.string.book_synopsis_not_found),
modifier = Modifier.padding(14.dp),
fontFamily = figeronaFont,
fontWeight = FontWeight.Medium,
color = MaterialTheme.colorScheme.onBackground,
)
Spacer(modifier = Modifier.weight(1f))
}
NoSynopsisUI()
}
}
}
}
})


}

@ExperimentalMaterial3Api
Expand Down Expand Up @@ -663,6 +619,44 @@ fun BookDetailTopBar(
}
}

@Composable
fun NoSynopsisUI() {
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
val compositionResult: LottieCompositionResult =
rememberLottieComposition(
spec = LottieCompositionSpec.RawRes(R.raw.synopis_not_found_lottie)
)
val progressAnimation by animateLottieCompositionAsState(
compositionResult.value,
isPlaying = true,
iterations = LottieConstants.IterateForever,
speed = 1f
)

Spacer(modifier = Modifier.weight(2f))
LottieAnimation(
composition = compositionResult.value,
progress = progressAnimation,
modifier = Modifier
.fillMaxWidth(0.85f)
.height(200.dp),
enableMergePaths = true
)

Text(
text = stringResource(id = R.string.book_synopsis_not_found),
modifier = Modifier.padding(14.dp),
fontFamily = figeronaFont,
fontWeight = FontWeight.Medium,
color = MaterialTheme.colorScheme.onBackground,
)
Spacer(modifier = Modifier.weight(1f))
}
}

@ExperimentalCoilApi
@ExperimentalComposeUiApi
@ExperimentalMaterialApi
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@

package com.starry.myne.ui.screens.detail.viewmodels

import android.annotation.SuppressLint
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.getValue
Expand All @@ -26,8 +25,6 @@ import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import coil.annotation.ExperimentalCoilApi
import com.starry.myne.MainActivity
import com.starry.myne.R
import com.starry.myne.database.library.LibraryDao
import com.starry.myne.database.library.LibraryItem
import com.starry.myne.repo.BookRepository
Expand All @@ -40,6 +37,7 @@ import com.starry.myne.utils.book.BookUtils
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.io.File
import javax.inject.Inject

data class BookDetailScreenState(
Expand Down Expand Up @@ -89,29 +87,25 @@ class BookDetailViewModel @Inject constructor(
}
}

@SuppressLint("Range")
fun downloadBook(
book: Book, activity: MainActivity, downloadProgressListener: (Float, Int) -> Unit
): String {
return if (activity.checkStoragePermission()) {
bookDownloader.downloadBook(book = book,
downloadProgressListener = downloadProgressListener,
onDownloadSuccess = {
insertIntoDB(book, bookDownloader.getFilenameForBook(book))
state = state.copy(bookLibraryItem = libraryDao.getItemById(book.id))
})
activity.getString(R.string.downloading_book)
} else {
activity.getString(R.string.storage_perm_error)
}
book: Book, downloadProgressListener: (Float, Int) -> Unit
) {
bookDownloader.downloadBook(book = book,
downloadProgressListener = downloadProgressListener,
onDownloadSuccess = { fileName ->
val filepath = bookDownloader.getFilePathForBook(fileName)
insertIntoDB(book = book, filepath = filepath)
state = state.copy(bookLibraryItem = libraryDao.getItemById(book.id))
}
)
}

private fun insertIntoDB(book: Book, filename: String) {
private fun insertIntoDB(book: Book, filepath: String) {
val libraryItem = LibraryItem(
bookId = book.id,
title = book.title,
authors = BookUtils.getAuthorsAsString(book.authors),
filePath = "${BookDownloader.FILE_FOLDER_PATH}/$filename",
filePath = filepath,
createdAt = System.currentTimeMillis()
)
libraryDao.insert(libraryItem)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
Expand Down Expand Up @@ -93,7 +94,6 @@ import com.starry.myne.ui.screens.settings.viewmodels.ThemeMode
import com.starry.myne.ui.theme.figeronaFont
import com.starry.myne.utils.Utils
import com.starry.myne.utils.getActivity
import com.starry.myne.utils.toToast
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import me.saket.swipe.SwipeAction
Expand All @@ -114,6 +114,7 @@ fun LibraryScreen(navController: NavController) {
val settingsViewModel = (context.getActivity() as MainActivity).settingsViewModel

val snackBarHostState = remember { SnackbarHostState() }
val coroutineScope = rememberCoroutineScope()

Scaffold(
snackbarHost = { SnackbarHost(snackBarHostState) },
Expand Down Expand Up @@ -144,8 +145,8 @@ fun LibraryScreen(navController: NavController) {
) {
items(state.size) { i ->
val item = state[i]
if (item.fileExist()) {

if (item.fileExist()) {
val openDeleteDialog = remember { mutableStateOf(false) }

val detailsAction = SwipeAction(icon = painterResource(
Expand Down Expand Up @@ -215,9 +216,15 @@ fun LibraryScreen(navController: NavController) {
openDeleteDialog.value = false
val fileDeleted = item.deleteFile()
if (fileDeleted) {
viewModel.deleteItem(item)
viewModel.deleteItemFromDB(item)
} else {
context.getString(R.string.error).toToast(context)
coroutineScope.launch {
snackBarHostState.showSnackbar(
message = context.getString(R.string.error),
actionLabel = context.getString(R.string.ok),
duration = SnackbarDuration.Short
)
}
}
}) {
Text(stringResource(id = R.string.confirm))
Expand All @@ -232,7 +239,7 @@ fun LibraryScreen(navController: NavController) {
}

} else {
viewModel.deleteItem(item)
viewModel.deleteItemFromDB(item)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ class LibraryViewModel @Inject constructor(
) : ViewModel() {
val allItems: LiveData<List<LibraryItem>> = libraryDao.getAllItems()

fun deleteItem(item: LibraryItem) {
fun deleteItemFromDB(item: LibraryItem) {
viewModelScope.launch(Dispatchers.IO) { libraryDao.delete(item) }
}

Expand Down
Loading

0 comments on commit 1f35d7d

Please sign in to comment.