Skip to content

Commit

Permalink
Merge pull request #165 from NatLibFi/ekir-326-inform-user-of-device-…
Browse files Browse the repository at this point in the history
…space-if-too-little

Add an information popup when not enough space
  • Loading branch information
natlibfi-burger authored Oct 14, 2024
2 parents d6cef65 + 81228a6 commit 6c3ed3c
Show file tree
Hide file tree
Showing 7 changed files with 264 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package org.librarysimplified.ui.catalog

import android.os.Bundle
import android.os.Environment
import android.os.StatFs
import android.text.Html
import android.view.View
import android.view.ViewGroup
Expand All @@ -9,6 +11,7 @@ import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.ProgressBar
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import androidx.core.os.bundleOf
import androidx.fragment.app.Fragment
Expand Down Expand Up @@ -41,6 +44,7 @@ import org.nypl.simplified.profiles.controller.api.ProfilesControllerType
import org.nypl.simplified.ui.screen.ScreenSizeInformationType
import org.slf4j.LoggerFactory
import org.thepalaceproject.theme.core.PalaceToolbar
import java.io.File
import java.net.URI

/**
Expand Down Expand Up @@ -945,7 +949,6 @@ class CatalogBookDetailFragment : Fragment(R.layout.book_detail) {
)
)
} else {
this.buttons.addView(this.buttonCreator.createButtonSizedSpace(), 0)
this.buttons.addView(this.buttonCreator.createButtonSizedSpace())
}

Expand Down Expand Up @@ -994,6 +997,111 @@ class CatalogBookDetailFragment : Fragment(R.layout.book_detail) {
this.buttons.addView(this.buttonCreator.createCenteredTextForButtons(R.string.catalogDownloading))
this.checkButtonViewCount()
}
//Check file size, and show popup if file is too big

//Check the size expected from one of the first packets
//bookStatus.currentTotalBytes == 0L does not work, sometimes skips
if (bookStatus.currentTotalBytes!! < 10000L) {
//Expected size of the book that is downloading
val expectedSize = bookStatus.expectedTotalBytes
//How much space is free on the device
val freeSpace = getInternalMem()
this.logger.debug("Assumed size of file: {}", formatSize(expectedSize))

//Null check the expected size
if (expectedSize != null) {
//If size smaller than internal memory, it should technically fit to memory
if (expectedSize < freeSpace) {
logger.debug("Enough space for download")
logger.debug(
"Remaining space: {}",
formatSize(freeSpace - expectedSize)
)
} else {
logger.debug("Not enough space for download")
//To avoid showing multiples of the same popup on top of one another,
// show only if there currently is no popup showing
if (!popUpShown) {
//Inform user with a popup when file doesn't fit
onFileTooBigToStore(freeSpace, expectedSize - freeSpace)
//Cancel the download
this.viewModel.cancelDownload()
}
}
}
}
}

//A boolean lock used for showing only one copy of the low memory popup at a time
private var popUpShown = false

/**
* Returns the amount of free internal memory there is on the device.
*/
private fun getInternalMem() : Long {
// Fetching internal memory information
val iPath: File = Environment.getDataDirectory()
val iStat = StatFs(iPath.path)
val iBlockSize = iStat.blockSizeLong
val iAvailableBlocks = iStat.availableBlocksLong

//Count and return the available internal memory
return iAvailableBlocks * iBlockSize
}

/**
* Change the bit presentation of a value to
* a better understandable form.
* Returns a string with the size suffix added.
*/
private fun formatSize(number : Long?) : String {
//Get the value that needs formatting
var expSize: Long = number?: 0L
//Create a variable that is made into the suffix
var suffix: String? = null
//Format the expSize to readable form, either kilo or megabytes
if (expSize >= 1024) {
suffix = "KB"
expSize /= 1024
if (expSize >= 1024) {
suffix = "MB"
expSize /= 1024
}
}
//Make the long value into a string
val expSizeString = StringBuilder(expSize.toString())
//If there is a suffix, add it to the end of the expSize
if (suffix != null) {
expSizeString.append(suffix)
}

//Return a string form of the formatted variable
return expSizeString.toString()
}

/**
* If there is no space for the book on the device, show a popup that informs the user about the
* required space.
*/
private fun onFileTooBigToStore(deviceSpace: Long, neededSpace : Long) {
//Mark that a popup is currently shown
popUpShown = true
logger.debug("Showing 'Not enough space' popup")
val builder: AlertDialog.Builder = AlertDialog.Builder(this.requireContext())
builder
.setMessage(getString(
R.string.bookNotEnoughSpaceMessage,
formatSize(deviceSpace),
formatSize(neededSpace)))
.setTitle(R.string.bookNotEnoughSpaceTitle)
.setPositiveButton(R.string.bookNotEnoughSpaceButton) { dialog, which ->
//Set the popup as closed
popUpShown = false
}

val dialog: AlertDialog = builder.create()
dialog.show()

}

private fun onBookStatusDownloadWaitingForExternalAuthentication() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,13 @@ class CatalogBookDetailViewModel(
this.borrowViewModel.tryDelete(feedEntry.accountID, feedEntry.bookID)
}

/**
* Function for cancelling download. Needed because class extends
* CatalogPagedViewListener. Function is not used but is functional.
*/
override fun cancelDownload(feedEntry: FeedEntry.FeedEntryOPDS) {
this.borrowViewModel.tryCancelDownload(feedEntry.accountID, feedEntry.bookID)
}
override fun resetInitialBookStatus(feedEntry: FeedEntry.FeedEntryOPDS) {
val initialBookStatus = synthesizeBookWithStatus(feedEntry)
this.bookRegistry.update(initialBookStatus)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -281,12 +281,19 @@ class CatalogBorrowViewModel(
this.booksController.bookRevokeFailedDismiss(accountID, bookID)
}

/**
* Try cancelling the current download. Cancelling deletes the book from the database,
* so after that is done, we sync current books with backend, so a new entry for the
* cancelled book is added to database.
*/
fun tryCancelDownload(
accountID: AccountID,
bookID: BookID
) {
this.logger.debug("cancelling download: {}", bookID)
this.booksController.bookCancelDownloadAndDelete(accountID, bookID)
//In order for the loan view to show books correctly, sync with the backend
this.booksController.booksSync(accountID)
}

fun tryDelete(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1046,6 +1046,16 @@ class CatalogFeedViewModel(
this.borrowViewModel.tryDelete(feedEntry.accountID, feedEntry.bookID)
}

/**
* Try to cancel the current download of a book
*/
override fun cancelDownload(feedEntry: FeedEntry.FeedEntryOPDS) {
this.borrowViewModel.tryCancelDownload(feedEntry.accountID, feedEntry.bookID)
//Since canceling download deletes the book from local database, we need to sync with
//the circulation so that the loan is shown correctly
this.syncAccounts()
}

override fun borrowMaybeAuthenticated(book: Book) {
this.openLoginDialogIfNecessary(book.account)
this.borrowViewModel.tryBorrowMaybeAuthenticated(book)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
package org.librarysimplified.ui.catalog

import android.content.Context
import android.os.Environment
import android.os.StatFs
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.ImageView
import android.widget.ProgressBar
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import androidx.recyclerview.widget.RecyclerView
import com.google.common.base.Preconditions
Expand All @@ -31,6 +34,8 @@ import org.nypl.simplified.opds.core.OPDSAvailabilityMatcherType
import org.nypl.simplified.opds.core.OPDSAvailabilityOpenAccess
import org.nypl.simplified.opds.core.OPDSAvailabilityRevoked
import org.nypl.simplified.profiles.controller.api.ProfilesControllerType
import org.slf4j.LoggerFactory
import java.io.File

/**
* A view holder for a single cell in an infinitely-scrolling feed.
Expand All @@ -45,6 +50,8 @@ class CatalogPagedViewHolder(
private val profilesController: ProfilesControllerType
) : RecyclerView.ViewHolder(parent) {

private val logger = LoggerFactory.getLogger(CatalogPagedViewHolder::class.java)

private var thumbnailLoading: FluentFuture<Unit>? = null

private val idle =
Expand Down Expand Up @@ -544,6 +551,54 @@ class CatalogPagedViewHolder(
this.setVisibilityIfNecessary(this.progress, View.VISIBLE)

this.progressText.text = book.book.entry.title
//Add an onClick listener to the book cell
//that links to the book's detail view
val onClick: (View) -> Unit = {
logger.debug("Open book detail view")
this.listener.openBookDetail(this.feedEntry as FeedEntryOPDS)
}
//Set the clickable area as the whole cell
this.progress.setOnClickListener(onClick)


//Check file size, and show popup if file is too big
//Check is done only once for each book download

//Did somehow skip every now and then the case of totalBytes == 0L
//So the check is now done on one of the first packets
if (status.currentTotalBytes!! < 10000L) {
//Expected size of the book that is downloading
val expectedSize = status.expectedTotalBytes
//How much space is free on the device
val freeSpace = getInternalMem()
this.logger.debug("Assumed size of file: {}", formatSize(expectedSize))

//Never should be null, but needs to be checked
if (expectedSize != null) {
//If size smaller than internal memory, it should technically fit to memory
//If doesn't, user gets shown a popup and download is cancelled
if (expectedSize < freeSpace) {
logger.debug("Enough space for download")
logger.debug("Expected size: {}", expectedSize)
logger.debug(
"Remaining space: {}",
formatSize(freeSpace - expectedSize)
)
} else {
logger.debug("Not enough space for download")
logger.debug("Already a popup showing: {}", popUpShown)
//We don't want to show multiple popups ontop of one another, so we
//Show one if one is not already shown
if (!popUpShown) {
//Show the popup
onFileTooBigToStore(freeSpace, expectedSize - freeSpace)
//Cancel the download
this.listener.cancelDownload(this.feedEntry as FeedEntryOPDS)
}
}
}
}


val progressPercent = status.progressPercent?.toInt()
if (progressPercent != null) {
Expand All @@ -554,6 +609,78 @@ class CatalogPagedViewHolder(
}
}

/**
* Returns the amount of free internal memory there is on the device.
*/
private fun getInternalMem() : Long {
// Fetching internal memory information
val iPath: File = Environment.getDataDirectory()
val iStat = StatFs(iPath.path)
val iBlockSize = iStat.blockSizeLong
val iAvailableBlocks = iStat.availableBlocksLong

//Count and return the available internal memory
return iAvailableBlocks * iBlockSize
}

/**
* Change the bit presentation of a number to
* a better understandable form.
* Returns a string with the size suffix added.
*/
private fun formatSize(number : Long?) : String {
//Expected size in bits that gets changed to the kilobyte or megabyte presentations
var expSize: Long = number?: 0L
//Suffix, that is either KB or MB
var suffix: String? = null
//Divide with 1024 to ge the kilobyte presentation, set the suffix
if (expSize >= 1024) {
suffix = "KB"
expSize /= 1024
//If possible, divide again to get megabyte presentation, set the suffix
if (expSize >= 1024) {
suffix = "MB"
expSize /= 1024
}
}
//Make the long value into a string
val expSizeString = StringBuilder(expSize.toString())
//If there is a suffix, add it to the end of the expSize
if (suffix != null) {
expSizeString.append(suffix)
}
//Return the size as string
return expSizeString.toString()
}

//Boolean that is used to only show one popup at a time
//Only true when there is a popup that is currently shown
private var popUpShown = false

/**
* If there is no space for the book on the device, show a popup that informs the user about the
* required space.
*/
private fun onFileTooBigToStore(deviceSpace: Long, neededSpace : Long) {
//Set the popup as shown
popUpShown = true
logger.debug("Showing size info")
//Show a popup with the device space and needed space
val builder: AlertDialog.Builder = AlertDialog.Builder(this.context)
builder
.setMessage(this.context.getString(
R.string.bookNotEnoughSpaceMessage,
formatSize(deviceSpace),
formatSize(neededSpace)))
.setTitle(R.string.bookNotEnoughSpaceTitle)
.setPositiveButton(R.string.bookNotEnoughSpaceButton) { dialog, which ->
//Set the popup as closed
popUpShown = false
}

val dialog: AlertDialog = builder.create()
dialog.show()
}
private fun onBookStatusDownloadWaitingForExternalAuthentication(
book: Book
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ interface CatalogPagedViewListener {

fun delete(feedEntry: FeedEntry.FeedEntryOPDS)

fun cancelDownload(feedEntry: FeedEntry.FeedEntryOPDS)
fun borrowMaybeAuthenticated(book: Book)

fun resetInitialBookStatus(feedEntry: FeedEntry.FeedEntryOPDS)
Expand Down
3 changes: 3 additions & 0 deletions simplified-ui-catalog/src/main/res/values/stringsCatalog.xml
Original file line number Diff line number Diff line change
Expand Up @@ -95,4 +95,7 @@
<string name="bookReachedLoanLimitDialogMessage">You have reached your loan limit. You cannot borrow anything further until you return something.</string>
<string name="bookReachedLoanLimitDialogTitle">Loan limit reached.</string>
<string name="bookReachedLoanLimitDialogButton">OK</string>
<string name="bookNotEnoughSpaceTitle">Not enough space!</string>
<string name="bookNotEnoughSpaceMessage">File is too big to store on device. Current free space on device: %1$s. \nFree at least %2$s of space to download the book.</string>
<string name="bookNotEnoughSpaceButton">Close</string>
</resources>

0 comments on commit 6c3ed3c

Please sign in to comment.