Downloads now fully support states from a Query. Language handling improved. Guava & apache commons removed

This commit is contained in:
Sean Mac Gillicuddy 2019-05-10 16:20:46 +01:00
parent f318da929c
commit 287d816304
9 changed files with 198 additions and 71 deletions

View File

@ -51,7 +51,7 @@ dependencies {
implementation "com.android.support:cardview-v7:$supportLibraryVersion"
implementation 'com.android.support:multidex:1.0.2'
compile 'com.android.support.constraint:constraint-layout:1.0.2'
implementation 'com.android.support.constraint:constraint-layout:1.0.2'
androidTestImplementation 'com.android.support.test.uiautomator:uiautomator-v18:2.1.3'
@ -77,9 +77,6 @@ dependencies {
androidTestImplementation 'com.android.support.test:runner:1.0.1'
androidTestImplementation 'com.android.support.test:rules:1.0.1'
// Guava
implementation group: 'com.google.guava', name: 'guava', version: '21.0'
// Dagger
compileOnly "javax.annotation:javax.annotation-api:$javaxAnnotationVersion"
androidTestCompileOnly "javax.annotation:javax.annotation-api:$javaxAnnotationVersion"
@ -94,9 +91,6 @@ dependencies {
implementation 'com.yahoo.squidb:squidb-annotations:2.0.0'
annotationProcessor 'com.yahoo.squidb:squidb-processor:2.0.0'
// Apache
implementation 'commons-io:commons-io:2.5'
// Square
implementation "com.squareup.okhttp3:okhttp:$okHttpVersion"
implementation "com.squareup.okhttp3:logging-interceptor:$okHttpVersion"

View File

@ -75,6 +75,11 @@ public class KiwixApplication extends MultiDexApplication implements HasActivity
@Override
public void onCreate() {
super.onCreate();
if (LeakCanary.isInAnalyzerProcess(this)) {
// This process is dedicated to LeakCanary for heap analysis.
// You should not init your app in this process.
return;
}
if (isExternalStorageWritable()) {
File appDirectory = new File(Environment.getExternalStorageDirectory() + "/Kiwix");
logFile = new File(appDirectory, "logcat.txt");
@ -105,13 +110,7 @@ public class KiwixApplication extends MultiDexApplication implements HasActivity
}
Log.d("KIWIX", "Started KiwixApplication");
applicationComponent.inject(this);
if (LeakCanary.isInAnalyzerProcess(this)) {
// This process is dedicated to LeakCanary for heap analysis.
// You should not init your app in this process.
return;
}
LeakCanary.install(this);
}

View File

@ -21,7 +21,6 @@ package org.kiwix.kiwixmobile.database;
import com.yahoo.squidb.data.SquidCursor;
import com.yahoo.squidb.sql.Query;
import com.yahoo.squidb.sql.Table;
import io.reactivex.Flowable;
import io.reactivex.processors.BehaviorProcessor;
import java.util.ArrayList;
@ -34,8 +33,6 @@ import org.kiwix.kiwixmobile.zim_manager.library_view.adapter.Language;
public class NetworkLanguageDao extends BaseDao {
private final BehaviorProcessor<List<Language>> activeLanguageProcessor =
BehaviorProcessor.create();
private final BehaviorProcessor<List<Language>> allLanguageProcessor = BehaviorProcessor.create();
@Inject
@ -45,14 +42,9 @@ public class NetworkLanguageDao extends BaseDao {
@Override
protected void onUpdateToTable() {
activeLanguageProcessor.onNext(fetchActiveLanguages());
allLanguageProcessor.onNext(fetchAllLanguages());
}
public Flowable<List<Language>> activeLanguages() {
return activeLanguageProcessor;
}
public Flowable<List<Language>> allLanguages() {
return allLanguageProcessor;
}
@ -61,10 +53,6 @@ public class NetworkLanguageDao extends BaseDao {
return fetchWith(Query.select());
}
public List<Language> fetchActiveLanguages() {
return fetchWith(Query.select().where(NetworkLanguageDatabaseEntity.ENABLED.eq(true)));
}
@NotNull private List<Language> fetchWith(Query query) {
ArrayList<Language> result = new ArrayList<>();
final NetworkLanguageDatabaseEntity databaseEntity =

View File

@ -17,15 +17,24 @@
*/
package org.kiwix.kiwixmobile.downloader
import android.content.Context
import android.support.v7.widget.RecyclerView
import android.view.View
import kotlinx.android.extensions.LayoutContainer
import kotlinx.android.synthetic.main.download_item.description
import kotlinx.android.synthetic.main.download_item.downloadProgress
import kotlinx.android.synthetic.main.download_item.downloadState
import kotlinx.android.synthetic.main.download_item.favicon
import kotlinx.android.synthetic.main.download_item.stop
import kotlinx.android.synthetic.main.download_item.title
import org.kiwix.kiwixmobile.downloader.model.DownloadItem
import org.kiwix.kiwixmobile.downloader.model.DownloadState
import org.kiwix.kiwixmobile.downloader.model.DownloadState.Failed
import org.kiwix.kiwixmobile.downloader.model.DownloadState.Paused
import org.kiwix.kiwixmobile.downloader.model.DownloadState.Pending
import org.kiwix.kiwixmobile.downloader.model.DownloadState.Running
import org.kiwix.kiwixmobile.downloader.model.DownloadState.Successful
import org.kiwix.kiwixmobile.downloader.model.FailureReason.Rfc2616HttpCode
import org.kiwix.kiwixmobile.extensions.setBitmap
class DownloadViewHolder(override val containerView: View) : RecyclerView.ViewHolder(containerView),
@ -41,5 +50,36 @@ class DownloadViewHolder(override val containerView: View) : RecyclerView.ViewHo
stop.setOnClickListener {
itemClickListener.invoke(downloadItem)
}
downloadState.text = toReadableState(
downloadItem.downloadState, containerView.context
)
}
private fun toReadableState(
downloadState: DownloadState,
context: Context
) = when (downloadState) {
is Paused -> context.getString(
downloadState.stringId,
context.getString(downloadState.reason.stringId)
)
is Failed -> context.getString(
downloadState.stringId,
getTemplateString(downloadState, context)
)
Pending,
Running,
Successful -> context.getString(downloadState.stringId)
}
private fun getTemplateString(
downloadState: Failed,
context: Context
) = when (downloadState.reason) {
is Rfc2616HttpCode -> context.getString(
downloadState.reason.stringId,
downloadState.reason.code
)
else -> context.getString(downloadState.reason.stringId)
}
}

View File

@ -24,8 +24,7 @@ data class DownloadItem(
val description: String,
val bytesDownloaded: Long,
val totalSizeBytes: Long,
val downloadState: DownloadState,
val reason: String
val downloadState: DownloadState
) {
val progress get() = ((bytesDownloaded.toFloat() / totalSizeBytes) * 100).toInt()
@ -36,7 +35,6 @@ data class DownloadItem(
downloadStatus.description,
downloadStatus.bytesDownloadedSoFar,
downloadStatus.totalSizeBytes,
downloadStatus.state,
downloadStatus.reason
downloadStatus.state
)
}

View File

@ -29,6 +29,19 @@ import android.app.DownloadManager.COLUMN_STATUS
import android.app.DownloadManager.COLUMN_TITLE
import android.app.DownloadManager.COLUMN_TOTAL_SIZE_BYTES
import android.app.DownloadManager.COLUMN_URI
import android.app.DownloadManager.ERROR_CANNOT_RESUME
import android.app.DownloadManager.ERROR_DEVICE_NOT_FOUND
import android.app.DownloadManager.ERROR_FILE_ALREADY_EXISTS
import android.app.DownloadManager.ERROR_FILE_ERROR
import android.app.DownloadManager.ERROR_HTTP_DATA_ERROR
import android.app.DownloadManager.ERROR_INSUFFICIENT_SPACE
import android.app.DownloadManager.ERROR_TOO_MANY_REDIRECTS
import android.app.DownloadManager.ERROR_UNHANDLED_HTTP_CODE
import android.app.DownloadManager.ERROR_UNKNOWN
import android.app.DownloadManager.PAUSED_QUEUED_FOR_WIFI
import android.app.DownloadManager.PAUSED_UNKNOWN
import android.app.DownloadManager.PAUSED_WAITING_FOR_NETWORK
import android.app.DownloadManager.PAUSED_WAITING_TO_RETRY
import android.app.DownloadManager.STATUS_FAILED
import android.app.DownloadManager.STATUS_PAUSED
import android.app.DownloadManager.STATUS_PENDING
@ -36,6 +49,7 @@ import android.app.DownloadManager.STATUS_RUNNING
import android.app.DownloadManager.STATUS_SUCCESSFUL
import android.database.Cursor
import android.net.Uri
import org.kiwix.kiwixmobile.R
import org.kiwix.kiwixmobile.extensions.get
import org.kiwix.kiwixmobile.library.entity.LibraryNetworkEntity.Book
import java.io.File
@ -45,7 +59,6 @@ class DownloadStatus(
val title: String,
val description: String,
val state: DownloadState,
val reason: String,
val bytesDownloadedSoFar: Long,
val totalSizeBytes: Long,
val lastModified: String,
@ -66,8 +79,7 @@ class DownloadStatus(
cursor[COLUMN_ID],
cursor[COLUMN_TITLE],
cursor[COLUMN_DESCRIPTION],
DownloadState.from(cursor[COLUMN_STATUS]),
cursor[COLUMN_REASON],
DownloadState.from(cursor[COLUMN_STATUS], cursor[COLUMN_REASON]),
cursor[COLUMN_BYTES_DOWNLOADED_SO_FAR],
cursor[COLUMN_TOTAL_SIZE_BYTES],
cursor[COLUMN_LAST_MODIFIED_TIMESTAMP],
@ -79,11 +91,14 @@ class DownloadStatus(
)
}
sealed class DownloadState {
sealed class DownloadState(val stringId: Int) {
companion object {
fun from(status: Int) = when (status) {
STATUS_FAILED -> Failed
STATUS_PAUSED -> Paused
fun from(
status: Int,
reason: Int
) = when (status) {
STATUS_PAUSED -> Paused(PausedReason.from(reason))
STATUS_FAILED -> Failed(FailureReason.from(reason))
STATUS_PENDING -> Pending
STATUS_RUNNING -> Running
STATUS_SUCCESSFUL -> Successful
@ -91,13 +106,59 @@ sealed class DownloadState {
}
}
object Paused : DownloadState()
object Failed : DownloadState()
object Pending : DownloadState()
object Running : DownloadState()
object Successful : DownloadState()
data class Paused(val reason: PausedReason) : DownloadState(R.string.paused_state)
data class Failed(val reason: FailureReason) : DownloadState(R.string.failed_state)
object Pending : DownloadState(R.string.pending_state)
object Running : DownloadState(R.string.running_state)
object Successful : DownloadState(R.string.successful_state)
override fun toString(): String {
return javaClass.simpleName
}
}
sealed class FailureReason(val stringId: Int) {
companion object {
fun from(reason: Int) = when (reason) {
in 100..505 -> Rfc2616HttpCode(reason)
ERROR_CANNOT_RESUME -> CannotResume
ERROR_DEVICE_NOT_FOUND -> StorageNotFound
ERROR_FILE_ALREADY_EXISTS -> FileAlreadyExists
ERROR_FILE_ERROR -> UnknownFileError
ERROR_HTTP_DATA_ERROR -> HttpError
ERROR_INSUFFICIENT_SPACE -> InsufficientSpace
ERROR_TOO_MANY_REDIRECTS -> TooManyRedirects
ERROR_UNHANDLED_HTTP_CODE -> UnhandledHttpCode
ERROR_UNKNOWN -> Unknown
else -> Unknown
}
}
object CannotResume : FailureReason(R.string.failed_cannot_resume)
object StorageNotFound : FailureReason(R.string.failed_storage_not_found)
object FileAlreadyExists : FailureReason(R.string.failed_file_already_exists)
object UnknownFileError : FailureReason(R.string.failed_unknown_file_error)
object HttpError : FailureReason(R.string.failed_http_error)
object InsufficientSpace : FailureReason(R.string.failed_insufficient_space)
object TooManyRedirects : FailureReason(R.string.failed_too_many_redirects)
object UnhandledHttpCode : FailureReason(R.string.failed_unhandled_http_code)
object Unknown : FailureReason(R.string.failed_unknown)
data class Rfc2616HttpCode(val code: Int) : FailureReason(R.string.failed_http_code)
}
sealed class PausedReason(val stringId: Int) {
companion object {
fun from(reason: Int) = when (reason) {
PAUSED_QUEUED_FOR_WIFI -> WaitingForWifi
PAUSED_WAITING_FOR_NETWORK -> WaitingForConnectivity
PAUSED_WAITING_TO_RETRY -> WaitingForRetry
PAUSED_UNKNOWN -> Unknown
else -> Unknown
}
}
object WaitingForWifi : PausedReason(R.string.paused_wifi)
object WaitingForConnectivity : PausedReason(R.string.paused_connectivity)
object WaitingForRetry : PausedReason(R.string.paused_retry)
object Unknown : PausedReason(R.string.paused_unknown)
}

View File

@ -29,7 +29,6 @@ import io.reactivex.functions.Function5
import io.reactivex.processors.BehaviorProcessor
import io.reactivex.processors.PublishProcessor
import io.reactivex.schedulers.Schedulers
import org.kiwix.kiwixmobile.KiwixApplication
import org.kiwix.kiwixmobile.R
import org.kiwix.kiwixmobile.database.BookDao
import org.kiwix.kiwixmobile.database.DownloadDao
@ -45,7 +44,6 @@ import org.kiwix.kiwixmobile.library.entity.LibraryNetworkEntity
import org.kiwix.kiwixmobile.library.entity.LibraryNetworkEntity.Book
import org.kiwix.kiwixmobile.network.KiwixService
import org.kiwix.kiwixmobile.utils.BookUtils
import org.kiwix.kiwixmobile.utils.NetworkUtils
import org.kiwix.kiwixmobile.zim_manager.fileselect_view.StorageObserver
import org.kiwix.kiwixmobile.zim_manager.library_view.adapter.Language
import org.kiwix.kiwixmobile.zim_manager.library_view.adapter.LibraryListItem
@ -53,6 +51,7 @@ import org.kiwix.kiwixmobile.zim_manager.library_view.adapter.LibraryListItem.Bo
import org.kiwix.kiwixmobile.zim_manager.library_view.adapter.LibraryListItem.DividerItem
import java.util.LinkedList
import java.util.Locale
import java.util.concurrent.TimeUnit.MILLISECONDS
import java.util.concurrent.TimeUnit.SECONDS
import javax.inject.Inject
@ -105,7 +104,7 @@ class ZimManageViewModel @Inject constructor(
updateBookItems(booksFromDao),
checkFileSystemForBooksOnRequest(booksFromDao),
updateLibraryItems(booksFromDao, downloads, library),
updateActiveLanguages(library),
updateLanguagesInDao(library),
updateNetworkStates(),
updateLanguageItemsForDialog()
)
@ -127,14 +126,16 @@ class ZimManageViewModel @Inject constructor(
private fun libraryFromNetwork() =
Flowable.combineLatest(
requestDownloadLibrary,
connectivityBroadcastReceiver.networkStates.distinctUntilChanged().filter(NetworkState.CONNECTED::equals),
connectivityBroadcastReceiver.networkStates.distinctUntilChanged().filter(
NetworkState.CONNECTED::equals
),
BiFunction<Unit, NetworkState, Unit> { _, _ -> Unit }
)
.subscribeOn(Schedulers.io())
.doOnNext { libraryListIsRefreshing.postValue(true) }
.switchMap { kiwixService.library }
.onErrorResumeNext(Flowable.just(LibraryNetworkEntity().apply { book = LinkedList() }))
.doOnError(Throwable::printStackTrace)
.onErrorResumeNext(Flowable.just(LibraryNetworkEntity().apply { book = LinkedList() }))
private fun updateLibraryItems(
booksFromDao: Flowable<List<Book>>,
@ -143,7 +144,9 @@ class ZimManageViewModel @Inject constructor(
) = Flowable.combineLatest(
booksFromDao,
downloads,
languageDao.activeLanguages().filter { it.isNotEmpty() },
languageDao.allLanguages()
.debounce(100, MILLISECONDS)
.filter { it.isNotEmpty() },
library,
requestFiltering
.doOnNext { libraryListIsRefreshing.postValue(true) }
@ -158,11 +161,13 @@ class ZimManageViewModel @Inject constructor(
Throwable::printStackTrace
)
private fun updateActiveLanguages(library: Flowable<LibraryNetworkEntity>) = library
private fun updateLanguagesInDao(
library: Flowable<LibraryNetworkEntity>
) = library
.subscribeOn(Schedulers.io())
.map { it.books }
.withLatestFrom(
languageDao.activeLanguages(),
languageDao.allLanguages(),
BiFunction(this::combineToLanguageList)
)
.subscribe(
@ -172,47 +177,68 @@ class ZimManageViewModel @Inject constructor(
private fun combineToLanguageList(
booksFromNetwork: List<Book>,
activeLanguages: List<Language>
allLanguages: List<Language>
): List<Language> {
val languageCounts = booksFromNetwork.mapNotNull { it.language }
val networkLanguageCounts = booksFromNetwork.mapNotNull { it.language }
.fold(
mutableMapOf<String, Int>(),
{ acc, language ->
acc[language] = acc.getOrElse(language, { 0 }) + 1
acc
}
{ acc, language -> acc.increment(language) }
)
return when {
booksFromNetwork.isEmpty() && allLanguages.isEmpty() -> defaultLanguage()
booksFromNetwork.isEmpty() && allLanguages.isNotEmpty() -> allLanguages
booksFromNetwork.isNotEmpty() && allLanguages.isEmpty() ->
fromLocalesWithNetworkMatchesSetActiveBy(networkLanguageCounts, defaultLanguage())
booksFromNetwork.isNotEmpty() && allLanguages.isNotEmpty() ->
fromLocalesWithNetworkMatchesSetActiveBy(networkLanguageCounts, allLanguages)
else -> throw RuntimeException("Impossible state")
}
}
private fun <K> MutableMap<K, Int>.increment(key: K) =
apply { set(key, getOrElse(key, { 0 }) + 1) }
private fun fromLocalesWithNetworkMatchesSetActiveBy(
networkLanguageCounts: MutableMap<String, Int>,
listToActivateBy: List<Language>
): List<Language> {
return Locale.getISOLanguages()
.map { Locale(it) }
.filter { languageCounts.containsKey(it.isO3Language) }
.filter { networkLanguageCounts.containsKey(it.isO3Language) }
.map { locale ->
Language(
locale.isO3Language,
languageWasPreviouslyActiveOrIsPrimaryLanguage(activeLanguages, locale),
languageCounts.getOrElse(locale.isO3Language, { 0 })
languageIsActive(listToActivateBy, locale),
networkLanguageCounts.getOrElse(locale.isO3Language, { 0 })
)
}
}
private fun languageWasPreviouslyActiveOrIsPrimaryLanguage(
activeLanguages: List<Language>,
locale: Locale
) = activeLanguages.firstOrNull { it.languageCode == locale.isO3Language }?.let { true }
?: isPrimaryLocale(locale)
private fun defaultLanguage() =
listOf(
Language(
context.resources.configuration.locale.isO3Language,
true,
1
)
)
private fun isPrimaryLocale(locale: Locale) =
context.resources.configuration.locale.isO3Language == locale.isO3Language
private fun languageIsActive(
allLanguages: List<Language>,
locale: Locale
) = allLanguages.firstOrNull { it.languageCode == locale.isO3Language }?.active == true
private fun combineLibrarySources(
booksOnFileSystem: List<Book>,
activeDownloads: List<DownloadModel>,
activeLanguages: List<Language>,
allLanguages: List<Language>,
libraryNetworkEntity: LibraryNetworkEntity,
filter: String
): List<LibraryListItem> {
val downloadedBooksIds = booksOnFileSystem.map { it.id }
val downloadingBookIds = activeDownloads.map { it.book.id }
val activeLanguageCodes = activeLanguages.map { it.languageCode }
val activeLanguageCodes = allLanguages.filter(Language::active)
.map { it.languageCode }
val booksUnfilteredByLanguage =
applyUserFilter(
libraryNetworkEntity.books
@ -341,3 +367,5 @@ class ZimManageViewModel @Inject constructor(
.distinctUntilChanged()
}

View File

@ -58,12 +58,12 @@
/>
<TextView
android:id="@+id/time_remaining"
android:id="@+id/downloadState"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:layout_gravity="start"
android:textAppearance="?android:attr/textAppearanceSmall"
tools:text="3hrs 45 mins"
tools:text="In Progress"
/>
</LinearLayout>

View File

@ -181,4 +181,23 @@
<string name="crash_checkbox_device">Device Details</string>
<string name="crash_button_confirm">SEND DETAILS</string>
<string name="crash_button_decline">NO THANKS</string>
<string name="pending_state">Pending</string>
<string name="running_state">In Progress</string>
<string name="successful_state">Complete</string>
<string name="paused_state">Paused: %s</string>
<string name="failed_state">Failed: %s</string>
<string name="paused_wifi">Waiting for Wifi</string>
<string name="paused_connectivity">Waiting to connect to a network</string>
<string name="paused_retry">Waiting to retry</string>
<string name="paused_unknown">Unknown</string>
<string name="failed_cannot_resume">Cannot resume due to unknown reason</string>
<string name="failed_storage_not_found">Could not find storage</string>
<string name="failed_file_already_exists">File already exists</string>
<string name="failed_unknown_file_error">Unknown file system error</string>
<string name="failed_http_error">HTTP data error</string>
<string name="failed_insufficient_space">Not enough space on storage</string>
<string name="failed_too_many_redirects">Too many redirects</string>
<string name="failed_unhandled_http_code">Unknown HTTP code received</string>
<string name="failed_unknown">Unknown</string>
<string name="failed_http_code">HTTP code %s</string>
</resources>