Skip to content

Commit

Permalink
Add download state cache. (#590)
Browse files Browse the repository at this point in the history
* Stop erroneously tracking non-ula downloads.

* Update asset preferences to contain additional methods for tracking download progress.

* Use asset preferences to check asset download times.

* Move download tracking to utility, and stub using cached state to restore if app is closed.

* Sync session startup state with previous downloads.

* Encapsulate each session state stage in own sealed class.

* Refactor to check requirements only on session fsm transition events.

* Handle downloads through process death.

* Trick tests to pass.

* Update session fsm tests.

* Handle and test situation where non-userland download intent is received.

* Update download utility tests.

* Update comment.
  • Loading branch information
MatthewTighe authored Feb 5, 2019
1 parent 2e46604 commit e257c71
Show file tree
Hide file tree
Showing 11 changed files with 617 additions and 290 deletions.
14 changes: 8 additions & 6 deletions app/src/main/java/tech/ula/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -104,16 +104,15 @@ class MainActivity : AppCompatActivity(), SessionListFragment.SessionSelection,
private val viewModel: MainActivityViewModel by lazy {
val ulaDatabase = UlaDatabase.getInstance(this)

val timestampPreferences = TimestampPreferences(this.getSharedPreferences("file_timestamps", Context.MODE_PRIVATE))
val assetPreferences = AssetPreferences(this.getSharedPreferences("assetLists", Context.MODE_PRIVATE))
val assetRepository = AssetRepository(filesDir.path, timestampPreferences, assetPreferences)
val assetRepository = AssetRepository(filesDir.path, assetPreferences)

val execUtility = ExecUtility(filesDir.path, Environment.getExternalStorageDirectory().absolutePath, DefaultPreferences(defaultSharedPreferences))
val filesystemUtility = FilesystemUtility(filesDir.path, execUtility)

val downloadManager = getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
val downloadManagerWrapper = DownloadManagerWrapper(downloadManager)
val downloadUtility = DownloadUtility(timestampPreferences, downloadManagerWrapper, filesDir)
val downloadUtility = DownloadUtility(assetPreferences, downloadManagerWrapper, filesDir)

val appsPreferences = AppsPreferences(this.getSharedPreferences("apps", Context.MODE_PRIVATE))

Expand Down Expand Up @@ -342,7 +341,7 @@ class MainActivity : AppCompatActivity(), SessionListFragment.SessionSelection,
is NoAppSelectedWhenPreferenceSubmitted -> {
getString(R.string.illegal_state_no_app_selected_when_preference_submitted)
}
is NoAppSelectedWhenPreparationStarted -> {
is NoAppSelectedWhenTransitionNecessary -> {
getString(R.string.illegal_state_no_app_selected_when_preparation_started)
}
is ErrorFetchingAppDatabaseEntries -> {
Expand All @@ -351,7 +350,7 @@ class MainActivity : AppCompatActivity(), SessionListFragment.SessionSelection,
is ErrorCopyingAppScript -> {
getString(R.string.illegal_state_error_copying_app_script)
}
is NoSessionSelectedWhenPreparationStarted -> {
is NoSessionSelectedWhenTransitionNecessary -> {
getString(R.string.illegal_state_no_session_selected_when_preparation_started)
}
is ErrorFetchingAssetLists -> {
Expand All @@ -366,6 +365,9 @@ class MainActivity : AppCompatActivity(), SessionListFragment.SessionSelection,
is AssetsHaveNotBeenDownloaded -> {
getString(R.string.illegal_state_assets_have_not_been_downloaded)
}
is DownloadCacheAccessedWhileEmpty -> {
getString(R.string.illegal_state_empty_download_cache_access)
}
is FailedToCopyAssetsToFilesystem -> {
getString(R.string.illegal_state_failed_to_copy_assets_to_filesystem)
}
Expand Down Expand Up @@ -488,7 +490,7 @@ class MainActivity : AppCompatActivity(), SessionListFragment.SessionSelection,
val step = getString(R.string.progress_copying_downloads)
updateProgressBar(step, "")
}
is FilesystemExtraction -> {
is FilesystemExtractionStep -> {
val step = getString(R.string.progress_setting_up_filesystem)
val details = getString(R.string.progress_extraction_details, state.extractionTarget)
updateProgressBar(step, details)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,13 @@ import tech.ula.model.entities.Asset
import tech.ula.model.entities.Filesystem
import tech.ula.utils.AssetPreferences
import tech.ula.utils.ConnectionUtility
import tech.ula.utils.TimestampPreferences
import java.io.BufferedReader
import java.io.File
import java.io.InputStreamReader
import java.lang.Exception

class AssetRepository(
private val applicationFilesDirPath: String,
private val timestampPreferences: TimestampPreferences,
private val assetPreferences: AssetPreferences,
private val connectionUtility: ConnectionUtility = ConnectionUtility()
) {
Expand All @@ -22,7 +20,7 @@ class AssetRepository(

if (!assetFile.exists()) return true

val localTimestamp = timestampPreferences.getSavedTimestampForFile(asset.concatenatedName)
val localTimestamp = assetPreferences.getLastUpdatedTimestampForAsset(asset)
return localTimestamp < asset.remoteTimestamp
}

Expand Down
114 changes: 62 additions & 52 deletions app/src/main/java/tech/ula/model/state/SessionStartupFsm.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,7 @@ import tech.ula.model.entities.Filesystem
import tech.ula.model.entities.Session
import tech.ula.model.repositories.AssetRepository
import tech.ula.model.repositories.UlaDatabase
import tech.ula.utils.CrashlyticsWrapper
import tech.ula.utils.DownloadUtility
import tech.ula.utils.FilesystemUtility
import tech.ula.utils.TimeUtility
import tech.ula.utils.* // ktlint-disable no-wildcard-imports

class SessionStartupFsm(
ulaDatabase: UlaDatabase,
Expand All @@ -35,14 +32,15 @@ class SessionStartupFsm(
private val filesystemsLiveData = filesystemDao.getAllFilesystems()
private val filesystems = mutableListOf<Filesystem>()

private val downloadingIds = mutableListOf<Long>()
private val downloadedIds = mutableListOf<Long>()

private val extractionLogger: (String) -> Unit = { line ->
state.postValue(ExtractingFilesystem(line))
}

init {
if (downloadUtility.downloadStateHasBeenCached()) {
state.postValue(DownloadingAssets(0, 0)) // Reset state so events can be submitted
handleAssetDownloadState(downloadUtility.syncStateWithCache())
}
activeSessionsLiveData.observeForever {
it?.let { list ->
activeSessions.clear()
Expand Down Expand Up @@ -77,7 +75,11 @@ class SessionStartupFsm(
is RetrieveAssetLists -> currentState is SessionIsReadyForPreparation
is GenerateDownloads -> currentState is AssetListsRetrievalSucceeded
is DownloadAssets -> currentState is DownloadsRequired
is AssetDownloadComplete -> currentState is DownloadingRequirements
is AssetDownloadComplete -> {
// If we are currently downloading assets, we can handle completed downloads that
// don't belong to us. Otherwise, we still don't want to post an illegal transition.
currentState is DownloadingAssets || !downloadUtility.downloadIsForUserland(event.downloadAssetId)
}
is CopyDownloadsToLocalStorage -> currentState is DownloadsHaveSucceeded
is VerifyFilesystemAssets -> currentState is NoDownloadsRequired || currentState is LocalDirectoryCopySucceeded
is ExtractFilesystem -> currentState is FilesystemAssetVerificationSucceeded
Expand Down Expand Up @@ -164,38 +166,29 @@ class SessionStartupFsm(
}

private fun handleDownloadAssets(assetsToDownload: List<Asset>) {
downloadingIds.clear()
downloadedIds.clear()

// If the state isn't updated first, AssetDownloadComplete events will be submitted before
// the transition is acceptable.
state.postValue(DownloadingRequirements(0, assetsToDownload.size))
val newDownloads = downloadUtility.downloadRequirements(assetsToDownload)
downloadingIds.addAll(newDownloads)
state.postValue(DownloadingAssets(0, assetsToDownload.size))
downloadUtility.downloadRequirements(assetsToDownload)
}

private fun handleAssetsDownloadComplete(downloadId: Long) {
if (!downloadUtility.downloadedSuccessfully(downloadId)) {
val reason = downloadUtility.getReasonForDownloadFailure(downloadId)
state.postValue(DownloadsHaveFailed(reason))
return
}

downloadedIds.add(downloadId)
downloadUtility.setTimestampForDownloadedFile(downloadId)
if (downloadingIds.size != downloadedIds.size) {
state.postValue(DownloadingRequirements(downloadedIds.size, downloadingIds.size))
return
}
val result = downloadUtility.handleDownloadComplete(downloadId)
handleAssetDownloadState(result)
}

downloadedIds.sort()
downloadingIds.sort()
if (downloadedIds != downloadingIds) {
state.postValue(DownloadsHaveFailed("Downloads completed with non-enqueued downloads"))
return
private fun handleAssetDownloadState(assetDownloadState: AssetDownloadState) {
return when (assetDownloadState) {
// We don't care if some other app has downloaded something, though we may intercept the
// broadcast from the Download Manager.
is NonUserlandDownloadFound -> {}
is CacheSyncAttemptedWhileCacheIsEmpty -> state.postValue(AttemptedCacheAccessWhileEmpty)
is AllDownloadsCompletedSuccessfully -> state.postValue(DownloadsHaveSucceeded)
is CompletedDownloadsUpdate -> {
state.postValue(DownloadingAssets(assetDownloadState.numCompleted, assetDownloadState.numTotal))
}
is AssetDownloadFailure -> state.postValue(DownloadsHaveFailed(assetDownloadState.reason))
}

state.postValue(DownloadsHaveSucceeded)
}

private suspend fun handleCopyDownloadsToLocalDirectories(filesystem: Filesystem) = withContext(Dispatchers.IO) {
Expand Down Expand Up @@ -261,30 +254,47 @@ class SessionStartupFsm(
}

sealed class SessionStartupState
// One-off events
data class IncorrectSessionTransition(val event: SessionStartupEvent, val state: SessionStartupState) : SessionStartupState()
object WaitingForSessionSelection : SessionStartupState()
object SingleSessionSupported : SessionStartupState()
data class SessionIsRestartable(val session: Session) : SessionStartupState()
data class SessionIsReadyForPreparation(val session: Session, val filesystem: Filesystem) : SessionStartupState()
object RetrievingAssetLists : SessionStartupState()
data class AssetListsRetrievalSucceeded(val assetLists: List<List<Asset>>) : SessionStartupState()
object AssetListsRetrievalFailed : SessionStartupState()
object GeneratingDownloadRequirements : SessionStartupState()
data class DownloadsRequired(val requiredDownloads: List<Asset>, val largeDownloadRequired: Boolean) : SessionStartupState()
object NoDownloadsRequired : SessionStartupState()
data class DownloadingRequirements(val numCompleted: Int, val numTotal: Int) : SessionStartupState()
object DownloadsHaveSucceeded : SessionStartupState()
data class DownloadsHaveFailed(val reason: String) : SessionStartupState()
object CopyingFilesToLocalDirectories : SessionStartupState()
object LocalDirectoryCopySucceeded : SessionStartupState()
object LocalDirectoryCopyFailed : SessionStartupState()
object VerifyingFilesystemAssets : SessionStartupState()
object FilesystemAssetVerificationSucceeded : SessionStartupState()
object AssetsAreMissingFromSupportDirectories : SessionStartupState()
object FilesystemAssetCopyFailed : SessionStartupState()
data class ExtractingFilesystem(val extractionTarget: String) : SessionStartupState()
object ExtractionHasCompletedSuccessfully : SessionStartupState()
object ExtractionFailed : SessionStartupState()

// Asset retrieval states
sealed class AssetRetrievalState : SessionStartupState()
object RetrievingAssetLists : AssetRetrievalState()
data class AssetListsRetrievalSucceeded(val assetLists: List<List<Asset>>) : AssetRetrievalState()
object AssetListsRetrievalFailed : AssetRetrievalState()

// Download requirements generation state
sealed class DownloadRequirementsGenerationState : SessionStartupState()
object GeneratingDownloadRequirements : DownloadRequirementsGenerationState()
data class DownloadsRequired(val requiredDownloads: List<Asset>, val largeDownloadRequired: Boolean) : DownloadRequirementsGenerationState()
object NoDownloadsRequired : DownloadRequirementsGenerationState()

// Downloading asset states
sealed class DownloadingAssetsState : SessionStartupState()
data class DownloadingAssets(val numCompleted: Int, val numTotal: Int) : DownloadingAssetsState()
object DownloadsHaveSucceeded : DownloadingAssetsState()
data class DownloadsHaveFailed(val reason: String) : DownloadingAssetsState()
object AttemptedCacheAccessWhileEmpty : DownloadingAssetsState()

sealed class CopyingFilesLocallyState : SessionStartupState()
object CopyingFilesToLocalDirectories : CopyingFilesLocallyState()
object LocalDirectoryCopySucceeded : CopyingFilesLocallyState()
object LocalDirectoryCopyFailed : CopyingFilesLocallyState()

sealed class AssetVerificationState : SessionStartupState()
object VerifyingFilesystemAssets : AssetVerificationState()
object FilesystemAssetVerificationSucceeded : AssetVerificationState()
object AssetsAreMissingFromSupportDirectories : AssetVerificationState()
object FilesystemAssetCopyFailed : AssetVerificationState()

sealed class ExtractionState : SessionStartupState()
data class ExtractingFilesystem(val extractionTarget: String) : ExtractionState()
object ExtractionHasCompletedSuccessfully : ExtractionState()
object ExtractionFailed : ExtractionState()

sealed class SessionStartupEvent
data class SessionSelected(val session: Session) : SessionStartupEvent()
Expand Down
67 changes: 58 additions & 9 deletions app/src/main/java/tech/ula/utils/AndroidUtility.kt
Original file line number Diff line number Diff line change
Expand Up @@ -73,20 +73,55 @@ class DefaultPreferences(private val prefs: SharedPreferences) {
}
}

class TimestampPreferences(private val prefs: SharedPreferences) {
fun getSavedTimestampForFile(assetConcatenatedName: String): Long {
return prefs.getLong(assetConcatenatedName, 0)
class AssetPreferences(private val prefs: SharedPreferences) {
private fun String.addTimestampPrefix(): String {
return "timestamp-" + this
}

fun getLastUpdatedTimestampForAsset(asset: Asset): Long {
return prefs.getLong(asset.concatenatedName.addTimestampPrefix(), -1)
}

fun setSavedTimestampForFileToNow(assetConcatenatedName: String) {
fun setLastUpdatedTimestampForAssetUsingConcatenatedName(assetConcatenatedName: String, currentTimeSeconds: Long) {
with(prefs.edit()) {
putLong(assetConcatenatedName, currentTimeSeconds())
putLong(assetConcatenatedName.addTimestampPrefix(), currentTimeSeconds)
apply()
}
}

private val downloadsAreInProgressKey = "downloadsAreInProgress"
fun getDownloadsAreInProgress(): Boolean {
return prefs.getBoolean(downloadsAreInProgressKey, false)
}

fun setDownloadsAreInProgress(inProgress: Boolean) {
with(prefs.edit()) {
putBoolean(downloadsAreInProgressKey, inProgress)
apply()
}
}

private val enqueuedDownloadsKey = "currentlyEnqueuedDownloads"
fun getEnqueuedDownloads(): Set<Long> {
val enqueuedDownloadsAsStrings = prefs.getStringSet(enqueuedDownloadsKey, setOf()) ?: setOf<String>()
return enqueuedDownloadsAsStrings.map { it.toLong() }.toSet()
}

fun setEnqueuedDownloads(downloads: Set<Long>) {
val enqueuedDownloadsAsStrings = downloads.map { it.toString() }.toSet()
with(prefs.edit()) {
putStringSet(enqueuedDownloadsKey, enqueuedDownloadsAsStrings)
apply()
}
}

fun clearEnqueuedDownloadsCache() {
with(prefs.edit()) {
remove(enqueuedDownloadsKey)
apply()
}
}
}

class AssetPreferences(private val prefs: SharedPreferences) {
fun getAssetLists(allAssetListTypes: List<Pair<String, String>>): List<List<Asset>> {
val assetLists = ArrayList<List<Asset>>()
allAssetListTypes.forEach {
Expand Down Expand Up @@ -277,12 +312,22 @@ class DownloadManagerWrapper(private val downloadManager: DownloadManager) {
return ""
}

fun downloadHasNotFailed(id: Long): Boolean {
fun downloadHasSucceeded(id: Long): Boolean {
val query = generateQuery(id)
val cursor = generateCursor(query)
if (cursor.moveToFirst()) {
val status = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_STATUS))
return status != DownloadManager.STATUS_FAILED
return status == DownloadManager.STATUS_SUCCESSFUL
}
return false
}

fun downloadHasFailed(id: Long): Boolean {
val query = generateQuery(id)
val cursor = generateCursor(query)
if (cursor.moveToFirst()) {
val status = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_STATUS))
return status == DownloadManager.STATUS_FAILED
}
return false
}
Expand Down Expand Up @@ -331,6 +376,10 @@ class LocalFileLocator(private val applicationFilesDir: String, private val reso
}

class TimeUtility {
fun getCurrentTimeSeconds(): Long {
return currentTimeSeconds()
}

fun getCurrentTimeMillis(): Long {
return System.currentTimeMillis()
}
Expand Down
Loading

0 comments on commit e257c71

Please sign in to comment.